Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { frodo } from '@rockcarver/frodo-lib';
import { Option } from 'commander';

import { configManagerImportJourneys } from '../../../configManagerOps/FrConfigJourneysOps';
import { getTokens } from '../../../ops/AuthenticateOps';
import { printMessage, verboseMessage } from '../../../utils/Console';
import { FrodoCommand } from '../../FrodoCommand';

const { CLOUD_DEPLOYMENT_TYPE_KEY, FORGEOPS_DEPLOYMENT_TYPE_KEY } =
frodo.utils.constants;

const deploymentTypes = [
CLOUD_DEPLOYMENT_TYPE_KEY,
FORGEOPS_DEPLOYMENT_TYPE_KEY,
];

export default function setup() {
const program = new FrodoCommand(
'frodo config-manager push journeys',
[],
deploymentTypes
);

program
.description('Import journeys.')
.addOption(
new Option(
'-n, --name <name>',
'Journey name, imports the specified Journey.'
)
)
.addOption(
new Option(
'-r, --realm <realm>',
'Imports the journeys to the specified realm'
)
)
.addOption(new Option('-d, --push-dependencies', 'Push dependencies.'))
// TO DO: implementing for 'check'
// .addOption(
// new Option('-c, --check Check first if ESVs changed')
// )
.action(async (host, realm, user, password, options, command) => {
command.handleDefaultArgsAndOpts(
host,
realm,
user,
password,
options,
command
);

if (await getTokens(false, true, deploymentTypes)) {
verboseMessage('Importing config entity journeys');
const outcome = await configManagerImportJourneys(
options.name,
options.realm,
options.pushDependencies
);
if (!outcome) process.exitCode = 1;
}
// unrecognized combination of options or no options
else {
printMessage(
'Unrecognized combination of options or no options...',
'error'
);
program.help();
process.exitCode = 1;
}
});

return program;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import EmailProvider from './config-manager-push-email-provider';
import EmailTemplates from './config-manager-push-email-templates';
import Endpoints from './config-manager-push-endpoints';
import InternalRoles from './config-manager-push-internal-roles';
import Journeys from './config-manager-push-journeys';
import Kba from './config-manager-push-kba';
import Locales from './config-manager-push-locales';
import ManagedObjects from './config-manager-push-managed-objects';
Expand Down Expand Up @@ -43,6 +44,7 @@ export default function setup() {
program.addCommand(UiConfig().name('ui-config'));
program.addCommand(Authentication().name('authentication'));
program.addCommand(ConnectorDefinitions().name('connector-definitions'));
program.addCommand(Journeys().name('journeys'));

return program;
}
214 changes: 213 additions & 1 deletion src/configManagerOps/FrConfigJourneysOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import {
MultiTreeExportInterface,
TreeExportOptions,
} from '@rockcarver/frodo-lib/types/ops/JourneyOps';
import fs from 'fs';

import { extractFrConfigDataToFile } from '../utils/Config';
import { printError, verboseMessage } from '../utils/Console';
import { existScript, realmList, safeFileName } from '../utils/FrConfig';

const { readRealms } = frodo.realm;

const { saveJsonToFile, getFilePath } = frodo.utils;
const { exportJourneys } = frodo.authn.journey;
const { exportJourneys, importJourneys } = frodo.authn.journey;

export async function configManagerExportJourneys(
name?,
Expand Down Expand Up @@ -203,3 +206,212 @@ function journeyNodeNeedsScript(node) {
(!node.hasOwnProperty('useScript') || node.useScript)
);
}

/**
* Process a journey directory for configManagerImportJourneys
* @param journeyDir path to the journey directory
* @param journeyName name of the journey
* @param dependencies if true, recursively include inner tree dependencies
* @param journeysBaseDir base directory containing all journeys for the realm
* @param processedJourneys set of already-processed journey names to prevent circular references
* @returns map of journey names to their import data
*/
function processJourney(
journeyDir: string,
journeyName: string,
dependencies: boolean,
journeysBaseDir: string,
processedJourneys: Set<string> = new Set()
): Record<string, any> {
if (processedJourneys.has(journeyName)) {
return {};
}
processedJourneys.add(journeyName);

const treeJsonPath = `${journeyDir}/${journeyName}.json`;
const treeData = fs.readFileSync(treeJsonPath, 'utf8');
const tree = JSON.parse(treeData);

const journeyData = {
circlesOfTrust: {},
emailTemplates: {},
innerNodes: {},
nodes: {},
saml2Entities: {},
scripts: {},
socialIdentityProviders: {},
themes: [],
tree,
variable: {},
};

const innerTreeNames: string[] = [];

const nodesDir = `${journeyDir}/nodes`;
if (fs.existsSync(nodesDir)) {
const entries = fs.readdirSync(nodesDir, { withFileTypes: true });

for (const entry of entries) {
if (entry.isFile() && entry.name.endsWith('.json')) {
const nodeData = fs.readFileSync(`${nodesDir}/${entry.name}`, 'utf8');
const node = JSON.parse(nodeData);
journeyData.nodes[node._id] = node;

if (dependencies && node._type?._id === 'InnerTreeEvaluatorNode') {
innerTreeNames.push(node.tree);
}
}

if (entry.isDirectory()) {
const pageNodeDir = `${nodesDir}/${entry.name}`;
const innerFiles = fs.readdirSync(pageNodeDir);
for (const innerFile of innerFiles) {
if (!innerFile.endsWith('.json')) continue;
const innerData = fs.readFileSync(
`${pageNodeDir}/${innerFile}`,
'utf8'
);
const innerNode = JSON.parse(innerData);
journeyData.innerNodes[innerNode._id] = innerNode;
}
}
}
}

const trees: Record<string, any> = {
[journeyName]: journeyData,
};

if (dependencies) {
for (const innerTreeName of innerTreeNames) {
const innerJourneyDir = `${journeysBaseDir}/${innerTreeName}`;

const innerTrees = processJourney(
innerJourneyDir,
innerTreeName,
dependencies,
journeysBaseDir,
processedJourneys
);
Object.assign(trees, innerTrees);
}
}

return trees;
}

/**
* Import journeys from fr-config-manager file structure
* @param name optional journey name to import
* @param realm optional realm to import to
* @param dependencies if true, push inner tree dependencies
* @returns true if successful, false otherwise
*/
export async function configManagerImportJourneys(
name?: string,
realm?: string,
dependencies?: boolean
): Promise<boolean> {
try {
if (realm === '/' || realm === '__default__realm__') {
return true;
}

const options = { deps: dependencies ?? false, reUuid: false };

if (realm) {
const journeysBaseDir = getFilePath(`realms/${realm}/journeys`);

if (name) {
const journeyDir = `${journeysBaseDir}/${name}`;
const trees = processJourney(
journeyDir,
name,
dependencies ?? false,
journeysBaseDir
);
await importJourneys({ trees }, options);
} else {
const journeyDirs = fs
.readdirSync(journeysBaseDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);

const trees: Record<string, any> = {};
const processed = new Set<string>();
for (const journeyName of journeyDirs) {
const journeyDir = `${journeysBaseDir}/${journeyName}`;
const journeyTrees = processJourney(
journeyDir,
journeyName,
dependencies ?? false,
journeysBaseDir,
processed
);
Object.assign(trees, journeyTrees);
}
await importJourneys({ trees }, options);
}
} else if (name) {
const readRealmNames = await readRealms();
for (const realmName of readRealmNames) {
if (realmName.name === '/' || realmName.name === '__default__realm__')
continue;
state.setRealm(realmName.name);

const journeysDir = getFilePath(
`realms${realmName.parentPath + realmName.name}/journeys`
);
if (!fs.existsSync(journeysDir)) continue;

const journeyDir = `${journeysDir}/${name}`;
if (!fs.existsSync(journeyDir)) continue;

const trees = processJourney(
journeyDir,
name,
dependencies ?? false,
journeysDir
);
await importJourneys({ trees }, options);
}
} else {
const readRealmNames = await readRealms();
for (const realmName of readRealmNames) {
if (realmName.name === '/' || realmName.name === '__default__realm__')
continue;
state.setRealm(realmName.name);

const journeysDir = getFilePath(
`realms${realmName.parentPath + realmName.name}/journeys`
);
if (!fs.existsSync(journeysDir)) continue;

const journeyDirs = fs
.readdirSync(journeysDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);

const trees: Record<string, any> = {};
const processed = new Set<string>();
for (const journeyName of journeyDirs) {
const journeyDir = `${journeysDir}/${journeyName}`;
const journeyTrees = processJourney(
journeyDir,
journeyName,
dependencies ?? false,
journeysDir,
processed
);
Object.assign(trees, journeyTrees);
}
await importJourneys({ trees }, options);
}
}

return true;
} catch (error) {
printError(error, `Error importing journeys`);
}
return false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CLI help interface for 'config-manager push journeys' should be expected english 1`] = `
"Usage: frodo config-manager push journeys [options] [host] [realm] [username] [password]

[Experimental] Import journeys.

Arguments:
host AM base URL, e.g.: https://cdk.iam.example.com/am. To
use a connection profile, just specify a unique
substring or alias.
realm Realm. Specify realm as '/' for the root realm or
'realm' or '/parent/child' otherwise. (default:
"alpha" for Identity Cloud tenants, "/" otherwise.)
username Username to login with. Must be an admin user with
appropriate rights to manage authentication
journeys/trees.
password Password.

Options:
-d, --push-dependencies Push dependencies.
-n, --name <name> Journey name, imports the specified Journey.
-r, --realm <realm> Imports the journeys to the specified realm
-h, --help Help
-hh, --help-more Help with all options.
-hhh, --help-all Help with all options, environment variables, and
usage examples.
"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Commands:
endpoints [Experimental] Import custom endpoints objects.
help display help for command
internal-roles [Experimental] Import internal roles.
journeys [Experimental] Import journeys.
kba [Experimental] Import kba configuration.
locales [Experimental] Import custom locales objects.
managed-objects [Experimental] Import managed objects.
Expand Down
10 changes: 10 additions & 0 deletions test/client_cli/en/config-manager-push-journeys.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import cp from 'child_process';
import { promisify } from 'util';

const exec = promisify(cp.exec);
const CMD = 'frodo config-manager push journeys --help';
const { stdout } = await exec(CMD);

test("CLI help interface for 'config-manager push journeys' should be expected english", async () => {
expect(stdout).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`frodo config-manager push journeys "frodo config-manager push journeys -D test/e2e/exports/fr-config-manager/forgeops -m forgeops": should import the journeys into forgeops" 1`] = `""`;

exports[`frodo config-manager push journeys "frodo config-manager push journeys -d -D test/e2e/exports/fr-config-manager/forgeops -m forgeops": should resolve dependencies when importing to forgeops" 1`] = `""`;

exports[`frodo config-manager push journeys "frodo config-manager push journeys -n testJourney -D test/e2e/exports/fr-config-manager/forgeops -m forgeops": should import a specific journey by name into forgeops" 1`] = `""`;

exports[`frodo config-manager push journeys "frodo config-manager push journeys -r alpha -D test/e2e/exports/fr-config-manager/forgeops -m forgeops": should import a journey to a specific realm" 1`] = `""`;
Loading