@rockcarver/frodo-lib 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +32 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +30 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/README.md +121 -0
- package/.github/workflows/pipeline.yml +287 -0
- package/.prettierrc +6 -0
- package/CHANGELOG.md +512 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +8 -0
- package/docs/CONTRIBUTE.md +96 -0
- package/docs/PIPELINE.md +169 -0
- package/docs/images/npm_versioning_guidelines.png +0 -0
- package/docs/images/release_pipeline.png +0 -0
- package/jsconfig.json +6 -0
- package/package.json +95 -0
- package/resources/sampleEntitiesFile.json +8 -0
- package/resources/sampleEnvFile.env +2 -0
- package/src/api/AuthenticateApi.js +33 -0
- package/src/api/BaseApi.js +242 -0
- package/src/api/CirclesOfTrustApi.js +87 -0
- package/src/api/EmailTemplateApi.js +37 -0
- package/src/api/IdmConfigApi.js +88 -0
- package/src/api/LogApi.js +45 -0
- package/src/api/ManagedObjectApi.js +62 -0
- package/src/api/OAuth2ClientApi.js +69 -0
- package/src/api/OAuth2OIDCApi.js +73 -0
- package/src/api/OAuth2ProviderApi.js +32 -0
- package/src/api/RealmApi.js +99 -0
- package/src/api/Saml2Api.js +176 -0
- package/src/api/ScriptApi.js +84 -0
- package/src/api/SecretsApi.js +151 -0
- package/src/api/ServerInfoApi.js +41 -0
- package/src/api/SocialIdentityProvidersApi.js +114 -0
- package/src/api/StartupApi.js +45 -0
- package/src/api/ThemeApi.js +181 -0
- package/src/api/TreeApi.js +207 -0
- package/src/api/VariablesApi.js +104 -0
- package/src/api/utils/ApiUtils.js +77 -0
- package/src/api/utils/ApiUtils.test.js +96 -0
- package/src/api/utils/Base64.js +62 -0
- package/src/index.js +32 -0
- package/src/index.test.js +13 -0
- package/src/ops/AdminOps.js +901 -0
- package/src/ops/AuthenticateOps.js +342 -0
- package/src/ops/CirclesOfTrustOps.js +350 -0
- package/src/ops/ConnectionProfileOps.js +254 -0
- package/src/ops/EmailTemplateOps.js +326 -0
- package/src/ops/IdmOps.js +227 -0
- package/src/ops/IdpOps.js +342 -0
- package/src/ops/JourneyOps.js +2026 -0
- package/src/ops/LogOps.js +357 -0
- package/src/ops/ManagedObjectOps.js +34 -0
- package/src/ops/OAuth2ClientOps.js +151 -0
- package/src/ops/OrganizationOps.js +85 -0
- package/src/ops/RealmOps.js +139 -0
- package/src/ops/SamlOps.js +541 -0
- package/src/ops/ScriptOps.js +211 -0
- package/src/ops/SecretsOps.js +288 -0
- package/src/ops/StartupOps.js +114 -0
- package/src/ops/ThemeOps.js +379 -0
- package/src/ops/VariablesOps.js +185 -0
- package/src/ops/templates/OAuth2ClientTemplate.json +270 -0
- package/src/ops/templates/OrgModelUserAttributesTemplate.json +149 -0
- package/src/ops/templates/cloud/GenericExtensionAttributesTemplate.json +392 -0
- package/src/ops/templates/cloud/managed.json +4119 -0
- package/src/ops/utils/Console.js +434 -0
- package/src/ops/utils/DataProtection.js +92 -0
- package/src/ops/utils/DataProtection.test.js +28 -0
- package/src/ops/utils/ExportImportUtils.js +146 -0
- package/src/ops/utils/ExportImportUtils.test.js +119 -0
- package/src/ops/utils/OpsUtils.js +76 -0
- package/src/ops/utils/Wordwrap.js +11 -0
- package/src/storage/SessionStorage.js +45 -0
- package/src/storage/StaticStorage.js +15 -0
- package/test/e2e/journey/baseline/ForgottenUsername.journey.json +216 -0
- package/test/e2e/journey/baseline/Login.journey.json +205 -0
- package/test/e2e/journey/baseline/PasswordGrant.journey.json +139 -0
- package/test/e2e/journey/baseline/ProgressiveProfile.journey.json +198 -0
- package/test/e2e/journey/baseline/Registration.journey.json +249 -0
- package/test/e2e/journey/baseline/ResetPassword.journey.json +268 -0
- package/test/e2e/journey/baseline/UpdatePassword.journey.json +323 -0
- package/test/e2e/journey/baseline/allAlphaJourneys.journeys.json +1520 -0
- package/test/e2e/journey/delete/ForgottenUsername.journey.json +216 -0
- package/test/e2e/journey/delete/Login.journey.json +205 -0
- package/test/e2e/journey/delete/PasswordGrant.journey.json +139 -0
- package/test/e2e/journey/delete/ProgressiveProfile.journey.json +198 -0
- package/test/e2e/journey/delete/Registration.journey.json +249 -0
- package/test/e2e/journey/delete/ResetPassword.journey.json +268 -0
- package/test/e2e/journey/delete/UpdatePassword.journey.json +323 -0
- package/test/e2e/journey/delete/deleteMe.journey.json +230 -0
- package/test/e2e/journey/list/Disabled.journey.json +43 -0
- package/test/e2e/journey/list/ForgottenUsername.journey.json +216 -0
- package/test/e2e/journey/list/Login.journey.json +205 -0
- package/test/e2e/journey/list/PasswordGrant.journey.json +139 -0
- package/test/e2e/journey/list/ProgressiveProfile.journey.json +198 -0
- package/test/e2e/journey/list/Registration.journey.json +249 -0
- package/test/e2e/journey/list/ResetPassword.journey.json +268 -0
- package/test/e2e/journey/list/UpdatePassword.journey.json +323 -0
- package/test/e2e/setup.js +107 -0
- package/test/e2e/theme/baseline/Contrast.theme.json +95 -0
- package/test/e2e/theme/baseline/Highlander.theme.json +95 -0
- package/test/e2e/theme/baseline/Robroy.theme.json +95 -0
- package/test/e2e/theme/baseline/Starter-Theme.theme.json +94 -0
- package/test/e2e/theme/baseline/Zardoz.theme.json +95 -0
- package/test/e2e/theme/import/Contrast.theme.json +95 -0
- package/test/e2e/theme/import/Highlander.theme.json +95 -0
- package/test/e2e/theme/import/Robroy.theme.json +95 -0
- package/test/e2e/theme/import/Starter-Theme.theme.json +94 -0
- package/test/e2e/theme/import/Zardoz.default.theme.json +95 -0
- package/test/fs_tmp/.gitkeep +2 -0
- package/test/global/setup.js +65 -0
|
@@ -0,0 +1,2026 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import yesno from 'yesno';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import {
|
|
7
|
+
convertBase64TextToArray,
|
|
8
|
+
getTypedFilename,
|
|
9
|
+
saveJsonToFile,
|
|
10
|
+
getRealmString,
|
|
11
|
+
convertTextArrayToBase64,
|
|
12
|
+
convertTextArrayToBase64Url,
|
|
13
|
+
} from './utils/ExportImportUtils.js';
|
|
14
|
+
import { getRealmManagedUser, replaceAll } from './utils/OpsUtils.js';
|
|
15
|
+
import storage from '../storage/SessionStorage.js';
|
|
16
|
+
import {
|
|
17
|
+
getNode,
|
|
18
|
+
putNode,
|
|
19
|
+
deleteNode,
|
|
20
|
+
getTrees,
|
|
21
|
+
getTree,
|
|
22
|
+
putTree,
|
|
23
|
+
getNodeTypes,
|
|
24
|
+
getNodesByType,
|
|
25
|
+
deleteTree,
|
|
26
|
+
} from '../api/TreeApi.js';
|
|
27
|
+
import { getEmailTemplate, putEmailTemplate } from '../api/EmailTemplateApi.js';
|
|
28
|
+
import { getScript } from '../api/ScriptApi.js';
|
|
29
|
+
import * as global from '../storage/StaticStorage.js';
|
|
30
|
+
import {
|
|
31
|
+
printMessage,
|
|
32
|
+
createProgressBar,
|
|
33
|
+
updateProgressBar,
|
|
34
|
+
stopProgressBar,
|
|
35
|
+
showSpinner,
|
|
36
|
+
succeedSpinner,
|
|
37
|
+
createTable,
|
|
38
|
+
spinSpinner,
|
|
39
|
+
failSpinner,
|
|
40
|
+
stopSpinner,
|
|
41
|
+
warnSpinner,
|
|
42
|
+
} from './utils/Console.js';
|
|
43
|
+
import wordwrap from './utils/Wordwrap.js';
|
|
44
|
+
import {
|
|
45
|
+
getProviderByLocationAndId,
|
|
46
|
+
getProviders,
|
|
47
|
+
getProviderMetadata,
|
|
48
|
+
createProvider,
|
|
49
|
+
findProviders,
|
|
50
|
+
updateProvider,
|
|
51
|
+
} from '../api/Saml2Api.js';
|
|
52
|
+
import {
|
|
53
|
+
createCircleOfTrust,
|
|
54
|
+
getCirclesOfTrust,
|
|
55
|
+
updateCircleOfTrust,
|
|
56
|
+
} from '../api/CirclesOfTrustApi.js';
|
|
57
|
+
import {
|
|
58
|
+
decode,
|
|
59
|
+
encode,
|
|
60
|
+
encodeBase64Url,
|
|
61
|
+
isBase64Encoded,
|
|
62
|
+
} from '../api/utils/Base64.js';
|
|
63
|
+
import {
|
|
64
|
+
getSocialIdentityProviders,
|
|
65
|
+
putProviderByTypeAndId,
|
|
66
|
+
} from '../api/SocialIdentityProvidersApi.js';
|
|
67
|
+
import { getThemes, putThemes } from '../api/ThemeApi.js';
|
|
68
|
+
import { createOrUpdateScript } from './ScriptOps.js';
|
|
69
|
+
|
|
70
|
+
const containerNodes = ['PageNode', 'CustomPageNode'];
|
|
71
|
+
|
|
72
|
+
const scriptedNodes = [
|
|
73
|
+
'ConfigProviderNode',
|
|
74
|
+
'ScriptedDecisionNode',
|
|
75
|
+
'ClientScriptNode',
|
|
76
|
+
'SocialProviderHandlerNode',
|
|
77
|
+
'CustomScriptNode',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const emailTemplateNodes = ['EmailSuspendNode', 'EmailTemplateNode'];
|
|
81
|
+
|
|
82
|
+
// use a function vs a template variable to avoid problems in loops
|
|
83
|
+
function getSingleTreeFileDataTemplate() {
|
|
84
|
+
return {
|
|
85
|
+
meta: {},
|
|
86
|
+
innerNodes: {},
|
|
87
|
+
nodes: {},
|
|
88
|
+
scripts: {},
|
|
89
|
+
emailTemplates: {},
|
|
90
|
+
socialIdentityProviders: {},
|
|
91
|
+
themes: [],
|
|
92
|
+
saml2Entities: {},
|
|
93
|
+
circlesOfTrust: {},
|
|
94
|
+
tree: {},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// use a function vs a template variable to avoid problems in loops
|
|
99
|
+
function getMultipleTreesFileDataTemplate() {
|
|
100
|
+
return {
|
|
101
|
+
meta: {},
|
|
102
|
+
trees: {},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Helper to get all SAML2 dependencies for a given node object
|
|
108
|
+
* @param {Object} nodeObject node object
|
|
109
|
+
* @param {[Object]} allProviders array of all saml2 providers objects
|
|
110
|
+
* @param {[Object]} allCirclesOfTrust array of all circle of trust objects
|
|
111
|
+
* @returns {Promise} a promise that resolves to an object containing a saml2 dependencies
|
|
112
|
+
*/
|
|
113
|
+
async function getSaml2NodeDependencies(
|
|
114
|
+
nodeObject,
|
|
115
|
+
allProviders,
|
|
116
|
+
allCirclesOfTrust
|
|
117
|
+
) {
|
|
118
|
+
const samlProperties = ['metaAlias', 'idpEntityId'];
|
|
119
|
+
const saml2EntityPromises = [];
|
|
120
|
+
samlProperties.forEach(async (samlProperty) => {
|
|
121
|
+
// In the following line nodeObject[samlProperty] will look like '/alpha/iSPAzure'.
|
|
122
|
+
const entityId =
|
|
123
|
+
samlProperty === 'metaAlias'
|
|
124
|
+
? _.last(nodeObject[samlProperty].split('/'))
|
|
125
|
+
: nodeObject[samlProperty];
|
|
126
|
+
const entity = _.find(allProviders, { entityId });
|
|
127
|
+
if (entity) {
|
|
128
|
+
saml2EntityPromises.push(
|
|
129
|
+
getProviderByLocationAndId(entity.location, entity._id).then(
|
|
130
|
+
(providerResponse) => {
|
|
131
|
+
/**
|
|
132
|
+
* Adding entityLocation here to the entityResponse because the import tool
|
|
133
|
+
* needs to know whether the saml2 entity is remote or not (this will be removed
|
|
134
|
+
* from the config before importing see updateSaml2Entity and createSaml2Entity functions).
|
|
135
|
+
* Importing a remote saml2 entity is a slightly different request (see createSaml2Entity).
|
|
136
|
+
*/
|
|
137
|
+
providerResponse.data.entityLocation = entity.location;
|
|
138
|
+
|
|
139
|
+
if (entity.location === 'remote') {
|
|
140
|
+
// get the xml representation of this entity and add it to the entityResponse;
|
|
141
|
+
return getProviderMetadata(providerResponse.data.entityId).then(
|
|
142
|
+
(metaDataResponse) => {
|
|
143
|
+
providerResponse.data.base64EntityXML = encodeBase64Url(
|
|
144
|
+
metaDataResponse.data
|
|
145
|
+
);
|
|
146
|
+
return providerResponse;
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return providerResponse;
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return Promise.all(saml2EntityPromises).then(
|
|
157
|
+
(saml2EntitiesPromisesResults) => {
|
|
158
|
+
const saml2Entities = [];
|
|
159
|
+
saml2EntitiesPromisesResults.forEach((saml2Entity) => {
|
|
160
|
+
if (saml2Entity) {
|
|
161
|
+
saml2Entities.push(saml2Entity.data);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
const samlEntityIds = _.map(
|
|
165
|
+
saml2Entities,
|
|
166
|
+
(saml2EntityConfig) => `${saml2EntityConfig.entityId}|saml2`
|
|
167
|
+
);
|
|
168
|
+
const circlesOfTrust = _.filter(allCirclesOfTrust, (circleOfTrust) => {
|
|
169
|
+
let hasEntityId = false;
|
|
170
|
+
circleOfTrust.trustedProviders.forEach((trustedProvider) => {
|
|
171
|
+
if (!hasEntityId && samlEntityIds.includes(trustedProvider)) {
|
|
172
|
+
hasEntityId = true;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return hasEntityId;
|
|
176
|
+
});
|
|
177
|
+
const saml2NodeDependencies = {
|
|
178
|
+
saml2Entities,
|
|
179
|
+
circlesOfTrust,
|
|
180
|
+
};
|
|
181
|
+
return saml2NodeDependencies;
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Helper method to create export data for a tree with all its
|
|
188
|
+
* dependencies. The export data can be written to a file as is
|
|
189
|
+
* (but it doesn't contain meta data).
|
|
190
|
+
* @param {Object} treeObject tree object
|
|
191
|
+
* @param {Object} exportData export data
|
|
192
|
+
* @param {Object} options options object
|
|
193
|
+
*/
|
|
194
|
+
async function exportTree(treeObject, exportData, options) {
|
|
195
|
+
const { useStringArrays } = options;
|
|
196
|
+
const { deps } = options;
|
|
197
|
+
const { verbose } = options;
|
|
198
|
+
|
|
199
|
+
if (verbose) printMessage(`\n- ${treeObject._id}\n`, 'info', false);
|
|
200
|
+
|
|
201
|
+
// Process tree
|
|
202
|
+
if (verbose) printMessage(' - Flow');
|
|
203
|
+
exportData.tree = treeObject;
|
|
204
|
+
if (verbose && treeObject.identityResource)
|
|
205
|
+
printMessage(
|
|
206
|
+
` - identityResource: ${treeObject.identityResource}`,
|
|
207
|
+
'info'
|
|
208
|
+
);
|
|
209
|
+
if (verbose) printMessage(` - Done`, 'info');
|
|
210
|
+
|
|
211
|
+
const nodePromises = [];
|
|
212
|
+
const scriptPromises = [];
|
|
213
|
+
const emailTemplatePromises = [];
|
|
214
|
+
const innerNodePromises = [];
|
|
215
|
+
const saml2ConfigPromises = [];
|
|
216
|
+
let socialProviderPromise = null;
|
|
217
|
+
const themePromise =
|
|
218
|
+
deps &&
|
|
219
|
+
storage.session.getDeploymentType() !== global.CLASSIC_DEPLOYMENT_TYPE_KEY
|
|
220
|
+
? getThemes().catch((error) => {
|
|
221
|
+
printMessage(error, 'error');
|
|
222
|
+
})
|
|
223
|
+
: null;
|
|
224
|
+
|
|
225
|
+
let allSaml2Providers = null;
|
|
226
|
+
let allCirclesOfTrust = null;
|
|
227
|
+
let filteredSocialProviders = null;
|
|
228
|
+
const themes = [];
|
|
229
|
+
|
|
230
|
+
// get all the nodes
|
|
231
|
+
for (const [nodeId, nodeInfo] of Object.entries(treeObject.nodes)) {
|
|
232
|
+
nodePromises.push(
|
|
233
|
+
getNode(nodeId, nodeInfo.nodeType).then((response) => response.data)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (verbose && nodePromises.length > 0) printMessage(' - Nodes:');
|
|
237
|
+
const nodeObjects = await Promise.all(nodePromises);
|
|
238
|
+
|
|
239
|
+
// iterate over every node in tree
|
|
240
|
+
for (const nodeObject of nodeObjects) {
|
|
241
|
+
const nodeId = nodeObject._id;
|
|
242
|
+
const nodeType = nodeObject._type._id;
|
|
243
|
+
if (verbose) printMessage(` - ${nodeId} (${nodeType})`, 'info', true);
|
|
244
|
+
exportData.nodes[nodeObject._id] = nodeObject;
|
|
245
|
+
|
|
246
|
+
// handle script node types
|
|
247
|
+
if (deps && scriptedNodes.includes(nodeType)) {
|
|
248
|
+
scriptPromises.push(getScript(nodeObject.script));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// frodo supports email templates in platform deployments
|
|
252
|
+
if (
|
|
253
|
+
(deps &&
|
|
254
|
+
storage.session.getDeploymentType() ===
|
|
255
|
+
global.CLOUD_DEPLOYMENT_TYPE_KEY) ||
|
|
256
|
+
storage.session.getDeploymentType() ===
|
|
257
|
+
global.FORGEOPS_DEPLOYMENT_TYPE_KEY
|
|
258
|
+
) {
|
|
259
|
+
if (emailTemplateNodes.includes(nodeType)) {
|
|
260
|
+
emailTemplatePromises.push(
|
|
261
|
+
getEmailTemplate(nodeObject.emailTemplateName).catch((error) => {
|
|
262
|
+
let message = `${error}`;
|
|
263
|
+
if (error.isAxiosError && error.response.status) {
|
|
264
|
+
message = error.response.statusText;
|
|
265
|
+
}
|
|
266
|
+
printMessage(
|
|
267
|
+
`\n${message}: Email Template "${nodeObject.emailTemplateName}"`,
|
|
268
|
+
'error'
|
|
269
|
+
);
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// handle SAML2 node dependencies
|
|
276
|
+
if (deps && nodeType === 'product-Saml2Node') {
|
|
277
|
+
if (!allSaml2Providers) {
|
|
278
|
+
// eslint-disable-next-line no-await-in-loop
|
|
279
|
+
allSaml2Providers = (await getProviders()).data.result;
|
|
280
|
+
}
|
|
281
|
+
if (!allCirclesOfTrust) {
|
|
282
|
+
// eslint-disable-next-line no-await-in-loop
|
|
283
|
+
allCirclesOfTrust = (await getCirclesOfTrust()).data.result;
|
|
284
|
+
}
|
|
285
|
+
saml2ConfigPromises.push(
|
|
286
|
+
getSaml2NodeDependencies(
|
|
287
|
+
nodeObject,
|
|
288
|
+
allSaml2Providers,
|
|
289
|
+
allCirclesOfTrust
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// If this is a SocialProviderHandlerNode get each enabled social identity provider.
|
|
295
|
+
if (
|
|
296
|
+
deps &&
|
|
297
|
+
!socialProviderPromise &&
|
|
298
|
+
nodeType === 'SocialProviderHandlerNode'
|
|
299
|
+
) {
|
|
300
|
+
socialProviderPromise = getSocialIdentityProviders();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// If this is a SelectIdPNode and filteredProviters is not already set to empty array set filteredSocialProviers.
|
|
304
|
+
if (deps && !filteredSocialProviders && nodeType === 'SelectIdPNode') {
|
|
305
|
+
filteredSocialProviders = filteredSocialProviders || [];
|
|
306
|
+
for (const filteredProvider of nodeObject.filteredProviders) {
|
|
307
|
+
if (!filteredSocialProviders.includes(filteredProvider)) {
|
|
308
|
+
filteredSocialProviders.push(filteredProvider);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// get inner nodes (nodes inside container nodes)
|
|
314
|
+
if (containerNodes.includes(nodeType)) {
|
|
315
|
+
for (const innerNode of nodeObject.nodes) {
|
|
316
|
+
innerNodePromises.push(
|
|
317
|
+
getNode(innerNode._id, innerNode.nodeType).then(
|
|
318
|
+
(response) => response.data
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
// frodo supports themes in platform deployments
|
|
323
|
+
if (
|
|
324
|
+
(deps &&
|
|
325
|
+
storage.session.getDeploymentType() ===
|
|
326
|
+
global.CLOUD_DEPLOYMENT_TYPE_KEY) ||
|
|
327
|
+
storage.session.getDeploymentType() ===
|
|
328
|
+
global.FORGEOPS_DEPLOYMENT_TYPE_KEY
|
|
329
|
+
) {
|
|
330
|
+
let themeId = false;
|
|
331
|
+
|
|
332
|
+
if (nodeObject.stage) {
|
|
333
|
+
// see if themeId is part of the stage object
|
|
334
|
+
try {
|
|
335
|
+
themeId = JSON.parse(nodeObject.stage).themeId;
|
|
336
|
+
} catch (e) {
|
|
337
|
+
themeId = false;
|
|
338
|
+
}
|
|
339
|
+
// if the page node's themeId is set the "old way" set themeId accordingly
|
|
340
|
+
if (!themeId && nodeObject.stage.indexOf('themeId=') === 0) {
|
|
341
|
+
// eslint-disable-next-line prefer-destructuring
|
|
342
|
+
themeId = nodeObject.stage.split('=')[1];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (themeId) {
|
|
347
|
+
if (!themes.includes(themeId)) themes.push(themeId);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Process inner nodes
|
|
354
|
+
if (verbose && innerNodePromises.length > 0) printMessage(' - Inner nodes:');
|
|
355
|
+
const innerNodeDataResults = await Promise.all(innerNodePromises);
|
|
356
|
+
for (const innerNodeObject of innerNodeDataResults) {
|
|
357
|
+
const innerNodeId = innerNodeObject._id;
|
|
358
|
+
const innerNodeType = innerNodeObject._type._id;
|
|
359
|
+
if (verbose)
|
|
360
|
+
printMessage(` - ${innerNodeId} (${innerNodeType})`, 'info', true);
|
|
361
|
+
exportData.innerNodes[innerNodeId] = innerNodeObject;
|
|
362
|
+
|
|
363
|
+
// handle script node types
|
|
364
|
+
if (deps && scriptedNodes.includes(innerNodeType)) {
|
|
365
|
+
scriptPromises.push(getScript(innerNodeObject.script));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// frodo supports email templates in platform deployments
|
|
369
|
+
if (
|
|
370
|
+
(deps &&
|
|
371
|
+
storage.session.getDeploymentType() ===
|
|
372
|
+
global.CLOUD_DEPLOYMENT_TYPE_KEY) ||
|
|
373
|
+
storage.session.getDeploymentType() ===
|
|
374
|
+
global.FORGEOPS_DEPLOYMENT_TYPE_KEY
|
|
375
|
+
) {
|
|
376
|
+
if (emailTemplateNodes.includes(innerNodeType)) {
|
|
377
|
+
emailTemplatePromises.push(
|
|
378
|
+
getEmailTemplate(innerNodeObject.emailTemplateName).catch((error) => {
|
|
379
|
+
let message = `${error}`;
|
|
380
|
+
if (error.isAxiosError && error.response.status) {
|
|
381
|
+
message = error.response.statusText;
|
|
382
|
+
}
|
|
383
|
+
printMessage(
|
|
384
|
+
`\n${message}: Email Template "${innerNodeObject.emailTemplateName}"`,
|
|
385
|
+
'error'
|
|
386
|
+
);
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// handle SAML2 node dependencies
|
|
393
|
+
if (deps && innerNodeType === 'product-Saml2Node') {
|
|
394
|
+
printMessage('SAML2 inner node', 'error');
|
|
395
|
+
if (!allSaml2Providers) {
|
|
396
|
+
// eslint-disable-next-line no-await-in-loop
|
|
397
|
+
allSaml2Providers = (await getProviders()).data.result;
|
|
398
|
+
}
|
|
399
|
+
if (!allCirclesOfTrust) {
|
|
400
|
+
// eslint-disable-next-line no-await-in-loop
|
|
401
|
+
allCirclesOfTrust = (await getCirclesOfTrust()).data.result;
|
|
402
|
+
}
|
|
403
|
+
saml2ConfigPromises.push(
|
|
404
|
+
getSaml2NodeDependencies(
|
|
405
|
+
innerNodeObject,
|
|
406
|
+
allSaml2Providers,
|
|
407
|
+
allCirclesOfTrust
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// If this is a SocialProviderHandlerNode get each enabled social identity provider.
|
|
413
|
+
if (
|
|
414
|
+
deps &&
|
|
415
|
+
!socialProviderPromise &&
|
|
416
|
+
innerNodeType === 'SocialProviderHandlerNode'
|
|
417
|
+
) {
|
|
418
|
+
socialProviderPromise = getSocialIdentityProviders();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// If this is a SelectIdPNode and filteredProviters is not already set to empty array set filteredSocialProviers.
|
|
422
|
+
if (
|
|
423
|
+
deps &&
|
|
424
|
+
!filteredSocialProviders &&
|
|
425
|
+
innerNodeType === 'SelectIdPNode' &&
|
|
426
|
+
innerNodeObject.filteredProviders
|
|
427
|
+
) {
|
|
428
|
+
filteredSocialProviders = filteredSocialProviders || [];
|
|
429
|
+
for (const filteredProvider of innerNodeObject.filteredProviders) {
|
|
430
|
+
if (!filteredSocialProviders.includes(filteredProvider)) {
|
|
431
|
+
filteredSocialProviders.push(filteredProvider);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Process email templates
|
|
438
|
+
if (verbose && emailTemplatePromises.length > 0)
|
|
439
|
+
printMessage(' - Email templates:');
|
|
440
|
+
const settledEmailTemplatePromises = await Promise.allSettled(
|
|
441
|
+
emailTemplatePromises
|
|
442
|
+
);
|
|
443
|
+
settledEmailTemplatePromises.forEach((settledPromise) => {
|
|
444
|
+
if (settledPromise.status === 'fulfilled' && settledPromise.value) {
|
|
445
|
+
if (verbose)
|
|
446
|
+
printMessage(
|
|
447
|
+
` - ${settledPromise.value.data._id.split('/')[1]}${
|
|
448
|
+
settledPromise.value.data.displayName
|
|
449
|
+
? ` (${settledPromise.value.data.displayName})`
|
|
450
|
+
: ''
|
|
451
|
+
}`,
|
|
452
|
+
'info',
|
|
453
|
+
true
|
|
454
|
+
);
|
|
455
|
+
exportData.emailTemplates[settledPromise.value.data._id.split('/')[1]] =
|
|
456
|
+
settledPromise.value.data;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Process SAML2 providers and circles of trust
|
|
461
|
+
const saml2NodeDependencies = await Promise.all(saml2ConfigPromises);
|
|
462
|
+
saml2NodeDependencies.forEach((saml2NodeDependency) => {
|
|
463
|
+
if (saml2NodeDependency) {
|
|
464
|
+
if (verbose) printMessage(' - SAML2 entity providers:');
|
|
465
|
+
saml2NodeDependency.saml2Entities.forEach((saml2Entity) => {
|
|
466
|
+
if (verbose)
|
|
467
|
+
printMessage(
|
|
468
|
+
` - ${saml2Entity.entityLocation} ${saml2Entity.entityId}`,
|
|
469
|
+
'info'
|
|
470
|
+
);
|
|
471
|
+
exportData.saml2Entities[saml2Entity._id] = saml2Entity;
|
|
472
|
+
});
|
|
473
|
+
if (verbose) printMessage(' - SAML2 circles of trust:');
|
|
474
|
+
saml2NodeDependency.circlesOfTrust.forEach((circleOfTrust) => {
|
|
475
|
+
if (verbose) printMessage(` - ${circleOfTrust._id}`, 'info');
|
|
476
|
+
exportData.circlesOfTrust[circleOfTrust._id] = circleOfTrust;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Process socialIdentityProviders
|
|
482
|
+
const socialProvidersResponse = await Promise.resolve(socialProviderPromise);
|
|
483
|
+
if (socialProvidersResponse) {
|
|
484
|
+
if (verbose) printMessage(' - OAuth2/OIDC (social) identity providers:');
|
|
485
|
+
socialProvidersResponse.data.result.forEach((socialProvider) => {
|
|
486
|
+
// If the list of socialIdentityProviders needs to be filtered based on the
|
|
487
|
+
// filteredProviders property of a SelectIdPNode do it here.
|
|
488
|
+
if (
|
|
489
|
+
socialProvider &&
|
|
490
|
+
(!filteredSocialProviders ||
|
|
491
|
+
filteredSocialProviders.length === 0 ||
|
|
492
|
+
filteredSocialProviders.includes(socialProvider._id))
|
|
493
|
+
) {
|
|
494
|
+
if (verbose) printMessage(` - ${socialProvider._id}`, 'info');
|
|
495
|
+
scriptPromises.push(getScript(socialProvider.transform));
|
|
496
|
+
exportData.socialIdentityProviders[socialProvider._id] = socialProvider;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Process scripts
|
|
502
|
+
if (verbose && scriptPromises.length > 0) printMessage(' - Scripts:');
|
|
503
|
+
const scripts = await Promise.all(scriptPromises);
|
|
504
|
+
scripts.forEach((scriptResultObject) => {
|
|
505
|
+
const scriptObject = _.get(scriptResultObject, 'data');
|
|
506
|
+
if (scriptObject) {
|
|
507
|
+
if (verbose)
|
|
508
|
+
printMessage(
|
|
509
|
+
` - ${scriptObject._id} (${scriptObject.name})`,
|
|
510
|
+
'info',
|
|
511
|
+
true
|
|
512
|
+
);
|
|
513
|
+
if (useStringArrays) {
|
|
514
|
+
scriptObject.script = convertBase64TextToArray(scriptObject.script);
|
|
515
|
+
} else {
|
|
516
|
+
scriptObject.script = JSON.stringify(decode(scriptObject.script));
|
|
517
|
+
}
|
|
518
|
+
exportData.scripts[scriptObject._id] = scriptObject;
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Process themes
|
|
523
|
+
if (themePromise) {
|
|
524
|
+
if (verbose) printMessage(' - Themes:');
|
|
525
|
+
await Promise.resolve(themePromise).then((themePromiseResults) => {
|
|
526
|
+
themePromiseResults.forEach((themeObject) => {
|
|
527
|
+
if (
|
|
528
|
+
themeObject &&
|
|
529
|
+
// has the theme been specified by id or name in a page node?
|
|
530
|
+
(themes.includes(themeObject._id) ||
|
|
531
|
+
themes.includes(themeObject.name) ||
|
|
532
|
+
// has this journey been linked to a theme?
|
|
533
|
+
themeObject.linkedTrees.includes(treeObject._id))
|
|
534
|
+
) {
|
|
535
|
+
if (verbose)
|
|
536
|
+
printMessage(
|
|
537
|
+
` - ${themeObject._id} (${themeObject.name})`,
|
|
538
|
+
'info'
|
|
539
|
+
);
|
|
540
|
+
exportData.themes.push(themeObject);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Export journey by id/name to file
|
|
549
|
+
* @param {String} journeyId journey id/name
|
|
550
|
+
* @param {String} file optional export file name
|
|
551
|
+
* @param {Object} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output, deps:boolean: include dependencies
|
|
552
|
+
*/
|
|
553
|
+
export async function exportJourneyToFile(journeyId, file, options) {
|
|
554
|
+
const { verbose } = options;
|
|
555
|
+
let fileName = file;
|
|
556
|
+
if (!fileName) {
|
|
557
|
+
fileName = getTypedFilename(journeyId, 'journey');
|
|
558
|
+
}
|
|
559
|
+
if (!verbose) showSpinner(`${journeyId}`);
|
|
560
|
+
getTree(journeyId)
|
|
561
|
+
.then(async (response) => {
|
|
562
|
+
const treeData = response.data;
|
|
563
|
+
const fileData = getSingleTreeFileDataTemplate();
|
|
564
|
+
try {
|
|
565
|
+
await exportTree(treeData, fileData, options);
|
|
566
|
+
if (verbose) showSpinner(`${journeyId}`);
|
|
567
|
+
saveJsonToFile(fileData, fileName);
|
|
568
|
+
succeedSpinner(
|
|
569
|
+
`Exported ${journeyId.brightCyan} to ${fileName.brightCyan}.`
|
|
570
|
+
);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (verbose) showSpinner(`${journeyId}`);
|
|
573
|
+
failSpinner(`Error exporting journey ${journeyId}: ${error}`);
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
.catch((err) => {
|
|
577
|
+
failSpinner(err.message);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Export all journeys to file
|
|
583
|
+
* @param {String} file optional export file name
|
|
584
|
+
*/
|
|
585
|
+
export async function exportJourneysToFile(file, options) {
|
|
586
|
+
let fileName = file;
|
|
587
|
+
if (!fileName) {
|
|
588
|
+
fileName = getTypedFilename(`all${getRealmString()}Journeys`, 'journeys');
|
|
589
|
+
}
|
|
590
|
+
const trees = (await getTrees()).data.result;
|
|
591
|
+
const fileData = getMultipleTreesFileDataTemplate();
|
|
592
|
+
createProgressBar(trees.length, 'Exporting journeys...');
|
|
593
|
+
for (const tree of trees) {
|
|
594
|
+
updateProgressBar(`${tree._id}`);
|
|
595
|
+
try {
|
|
596
|
+
// eslint-disable-next-line no-await-in-loop
|
|
597
|
+
const treeData = (await getTree(tree._id)).data;
|
|
598
|
+
const exportData = getSingleTreeFileDataTemplate();
|
|
599
|
+
delete exportData.meta;
|
|
600
|
+
// eslint-disable-next-line no-await-in-loop
|
|
601
|
+
await exportTree(treeData, exportData, options);
|
|
602
|
+
fileData.trees[tree._id] = exportData;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
printMessage(`Error exporting journey ${tree._id}: ${error}`, 'error');
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
saveJsonToFile(fileData, fileName);
|
|
608
|
+
stopProgressBar(`Exported to ${fileName}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Export all journeys to separate files
|
|
613
|
+
*/
|
|
614
|
+
export async function exportJourneysToFiles(options) {
|
|
615
|
+
const trees = (await getTrees()).data.result;
|
|
616
|
+
createProgressBar(trees.length, 'Exporting journeys...');
|
|
617
|
+
for (const tree of trees) {
|
|
618
|
+
updateProgressBar(`${tree._id}`);
|
|
619
|
+
const fileName = getTypedFilename(`${tree._id}`, 'journey');
|
|
620
|
+
// eslint-disable-next-line no-await-in-loop
|
|
621
|
+
const treeData = (await getTree(tree._id)).data;
|
|
622
|
+
const exportData = getSingleTreeFileDataTemplate();
|
|
623
|
+
// eslint-disable-next-line no-await-in-loop
|
|
624
|
+
await exportTree(treeData, exportData, options);
|
|
625
|
+
saveJsonToFile(exportData, fileName);
|
|
626
|
+
}
|
|
627
|
+
stopProgressBar('Done');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Get data for journey by id/name
|
|
632
|
+
* @param {String} journeyId journey id/name
|
|
633
|
+
* @returns {Object} object containing all journey data
|
|
634
|
+
*/
|
|
635
|
+
export async function getJourneyData(journeyId) {
|
|
636
|
+
showSpinner(`${journeyId}`);
|
|
637
|
+
const journeyData = getSingleTreeFileDataTemplate();
|
|
638
|
+
const treeData = (
|
|
639
|
+
await getTree(journeyId).catch((err) => {
|
|
640
|
+
succeedSpinner();
|
|
641
|
+
printMessage(err, 'error');
|
|
642
|
+
})
|
|
643
|
+
).data;
|
|
644
|
+
spinSpinner();
|
|
645
|
+
await exportTree(treeData, journeyData, { useStringArrays: true });
|
|
646
|
+
succeedSpinner();
|
|
647
|
+
return journeyData;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Helper to import a tree with all dependencies from an import data object (typically read from a file)
|
|
652
|
+
* @param {Object} treeObject tree object containing tree and all its dependencies
|
|
653
|
+
* @param {Object} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
|
|
654
|
+
*/
|
|
655
|
+
async function importTree(treeObject, options) {
|
|
656
|
+
const { reUuid } = options;
|
|
657
|
+
const { deps } = options;
|
|
658
|
+
const { verbose } = options;
|
|
659
|
+
if (verbose) printMessage(`\n- ${treeObject.tree._id}\n`, 'info', false);
|
|
660
|
+
let newUuid = '';
|
|
661
|
+
const uuidMap = {};
|
|
662
|
+
const treeId = treeObject.tree._id;
|
|
663
|
+
|
|
664
|
+
// Process scripts
|
|
665
|
+
if (
|
|
666
|
+
deps &&
|
|
667
|
+
treeObject.scripts &&
|
|
668
|
+
Object.entries(treeObject.scripts).length > 0
|
|
669
|
+
) {
|
|
670
|
+
if (verbose) printMessage(' - Scripts:');
|
|
671
|
+
for (const [scriptId, scriptObject] of Object.entries(treeObject.scripts)) {
|
|
672
|
+
if (verbose)
|
|
673
|
+
printMessage(` - ${scriptId} (${scriptObject.name})`, 'info', false);
|
|
674
|
+
// is the script stored as an array of strings or just b64 blob?
|
|
675
|
+
if (Array.isArray(scriptObject.script)) {
|
|
676
|
+
scriptObject.script = convertTextArrayToBase64(scriptObject.script);
|
|
677
|
+
} else if (!isBase64Encoded(scriptObject.script)) {
|
|
678
|
+
scriptObject.script = encode(JSON.parse(scriptObject.script));
|
|
679
|
+
}
|
|
680
|
+
// eslint-disable-next-line no-await-in-loop
|
|
681
|
+
if ((await createOrUpdateScript(scriptId, scriptObject)) == null) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`Error importing script ${scriptObject.name} (${scriptId}) in journey ${treeId}`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (verbose) printMessage('');
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Process email templates
|
|
691
|
+
if (
|
|
692
|
+
deps &&
|
|
693
|
+
treeObject.emailTemplates &&
|
|
694
|
+
Object.entries(treeObject.emailTemplates).length > 0
|
|
695
|
+
) {
|
|
696
|
+
if (verbose) printMessage(' - Email templates:');
|
|
697
|
+
for (const [templateId, templateData] of Object.entries(
|
|
698
|
+
treeObject.emailTemplates
|
|
699
|
+
)) {
|
|
700
|
+
if (verbose) printMessage(` - ${templateId}`, 'info', false);
|
|
701
|
+
try {
|
|
702
|
+
// eslint-disable-next-line no-await-in-loop
|
|
703
|
+
await putEmailTemplate(templateId, templateData);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
printMessage(error.response.data, 'error');
|
|
706
|
+
throw new Error(`Error importing email templates: ${error.message}`);
|
|
707
|
+
}
|
|
708
|
+
if (verbose) printMessage('');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Process themes
|
|
713
|
+
if (deps && treeObject.themes && treeObject.themes.length > 0) {
|
|
714
|
+
if (verbose) printMessage(' - Themes:');
|
|
715
|
+
const themes = {};
|
|
716
|
+
for (const theme of treeObject.themes) {
|
|
717
|
+
if (verbose) printMessage(` - ${theme._id} (${theme.name})`, 'info');
|
|
718
|
+
themes[theme._id] = theme;
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
await putThemes(themes);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
throw new Error(`Error importing themes: ${error.message}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Process social providers
|
|
728
|
+
if (
|
|
729
|
+
deps &&
|
|
730
|
+
treeObject.socialIdentityProviders &&
|
|
731
|
+
Object.entries(treeObject.socialIdentityProviders).length > 0
|
|
732
|
+
) {
|
|
733
|
+
if (verbose) printMessage(' - OAuth2/OIDC (social) identity providers:');
|
|
734
|
+
for (const [providerId, providerData] of Object.entries(
|
|
735
|
+
treeObject.socialIdentityProviders
|
|
736
|
+
)) {
|
|
737
|
+
if (verbose) printMessage(` - ${providerId}`, 'info');
|
|
738
|
+
try {
|
|
739
|
+
// eslint-disable-next-line no-await-in-loop
|
|
740
|
+
await putProviderByTypeAndId(
|
|
741
|
+
providerData._type._id,
|
|
742
|
+
providerId,
|
|
743
|
+
providerData
|
|
744
|
+
);
|
|
745
|
+
} catch (importError) {
|
|
746
|
+
if (
|
|
747
|
+
importError.response.status === 500 &&
|
|
748
|
+
importError.response.data.message ===
|
|
749
|
+
'Unable to update SMS config: Data validation failed for the attribute, Redirect after form post URL'
|
|
750
|
+
) {
|
|
751
|
+
providerData.redirectAfterFormPostURI = '';
|
|
752
|
+
try {
|
|
753
|
+
// eslint-disable-next-line no-await-in-loop
|
|
754
|
+
await putProviderByTypeAndId(
|
|
755
|
+
providerData._type._id,
|
|
756
|
+
providerId,
|
|
757
|
+
providerData
|
|
758
|
+
);
|
|
759
|
+
} catch (importError2) {
|
|
760
|
+
printMessage(importError.response.data, 'error');
|
|
761
|
+
throw new Error(
|
|
762
|
+
`Error importing provider ${providerId} in journey ${treeId}: ${importError}`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
printMessage(importError.response.data, 'error');
|
|
767
|
+
throw new Error(
|
|
768
|
+
`\nError importing provider ${providerId} in journey ${treeId}: ${importError}`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Process saml providers
|
|
776
|
+
if (
|
|
777
|
+
deps &&
|
|
778
|
+
treeObject.saml2Entities &&
|
|
779
|
+
Object.entries(treeObject.saml2Entities).length > 0
|
|
780
|
+
) {
|
|
781
|
+
if (verbose) printMessage(' - SAML2 entity providers:');
|
|
782
|
+
for (const [, providerData] of Object.entries(treeObject.saml2Entities)) {
|
|
783
|
+
delete providerData._rev;
|
|
784
|
+
const { entityId } = providerData;
|
|
785
|
+
const { entityLocation } = providerData;
|
|
786
|
+
if (verbose) printMessage(` - ${entityLocation} ${entityId}`, 'info');
|
|
787
|
+
let metaData = null;
|
|
788
|
+
if (entityLocation === 'remote') {
|
|
789
|
+
if (Array.isArray(providerData.base64EntityXML)) {
|
|
790
|
+
metaData = convertTextArrayToBase64Url(providerData.base64EntityXML);
|
|
791
|
+
} else {
|
|
792
|
+
metaData = providerData.base64EntityXML;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
delete providerData.entityLocation;
|
|
796
|
+
delete providerData.base64EntityXML;
|
|
797
|
+
// create the provider if it doesn't already exist, or just update it
|
|
798
|
+
if (
|
|
799
|
+
// eslint-disable-next-line no-await-in-loop
|
|
800
|
+
(await findProviders(`entityId eq '${entityId}'`, 'location')).data
|
|
801
|
+
.resultCount === 0
|
|
802
|
+
) {
|
|
803
|
+
// eslint-disable-next-line no-await-in-loop
|
|
804
|
+
await createProvider(entityLocation, providerData, metaData).catch(
|
|
805
|
+
(createProviderErr) => {
|
|
806
|
+
printMessage(createProviderErr.response.data, 'error');
|
|
807
|
+
throw new Error(`Error creating provider ${entityId}`);
|
|
808
|
+
}
|
|
809
|
+
);
|
|
810
|
+
} else {
|
|
811
|
+
// eslint-disable-next-line no-await-in-loop
|
|
812
|
+
await updateProvider(entityLocation, providerData).catch(
|
|
813
|
+
(updateProviderErr) => {
|
|
814
|
+
printMessage(updateProviderErr.response.data, 'error');
|
|
815
|
+
throw new Error(`Error updating provider ${entityId}`);
|
|
816
|
+
}
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Process circles of trust
|
|
823
|
+
if (
|
|
824
|
+
deps &&
|
|
825
|
+
treeObject.circlesOfTrust &&
|
|
826
|
+
Object.entries(treeObject.circlesOfTrust).length > 0
|
|
827
|
+
) {
|
|
828
|
+
if (verbose) printMessage(' - SAML2 circles of trust:');
|
|
829
|
+
for (const [cotId, cotData] of Object.entries(treeObject.circlesOfTrust)) {
|
|
830
|
+
delete cotData._rev;
|
|
831
|
+
if (verbose) printMessage(` - ${cotId}`, 'info');
|
|
832
|
+
// eslint-disable-next-line no-await-in-loop
|
|
833
|
+
await createCircleOfTrust(cotData)
|
|
834
|
+
// eslint-disable-next-line no-unused-vars
|
|
835
|
+
.catch(async (createCotErr) => {
|
|
836
|
+
if (
|
|
837
|
+
createCotErr.response.status === 409 ||
|
|
838
|
+
createCotErr.response.status === 500
|
|
839
|
+
) {
|
|
840
|
+
await updateCircleOfTrust(cotId, cotData).catch(
|
|
841
|
+
async (updateCotErr) => {
|
|
842
|
+
printMessage(createCotErr.response.data, 'error');
|
|
843
|
+
printMessage(updateCotErr.response.data, 'error');
|
|
844
|
+
throw new Error(
|
|
845
|
+
`Error creating/updating circle of trust ${cotId}`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
} else {
|
|
850
|
+
printMessage(createCotErr.response.data, 'error');
|
|
851
|
+
throw new Error(`Error creating circle of trust ${cotId}`);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Process inner nodes
|
|
858
|
+
let innerNodes = {};
|
|
859
|
+
if (
|
|
860
|
+
treeObject.innerNodes &&
|
|
861
|
+
Object.entries(treeObject.innerNodes).length > 0
|
|
862
|
+
) {
|
|
863
|
+
innerNodes = treeObject.innerNodes;
|
|
864
|
+
}
|
|
865
|
+
// old export file format
|
|
866
|
+
else if (
|
|
867
|
+
treeObject.innernodes &&
|
|
868
|
+
Object.entries(treeObject.innernodes).length > 0
|
|
869
|
+
) {
|
|
870
|
+
innerNodes = treeObject.innernodes;
|
|
871
|
+
}
|
|
872
|
+
if (Object.entries(innerNodes).length > 0) {
|
|
873
|
+
if (verbose) printMessage(' - Inner nodes:', 'text', true);
|
|
874
|
+
for (const [innerNodeId, innerNodeData] of Object.entries(innerNodes)) {
|
|
875
|
+
delete innerNodeData._rev;
|
|
876
|
+
const nodeType = innerNodeData._type._id;
|
|
877
|
+
if (!reUuid) {
|
|
878
|
+
newUuid = innerNodeId;
|
|
879
|
+
} else {
|
|
880
|
+
newUuid = uuidv4();
|
|
881
|
+
uuidMap[innerNodeId] = newUuid;
|
|
882
|
+
}
|
|
883
|
+
innerNodeData._id = newUuid;
|
|
884
|
+
|
|
885
|
+
if (verbose)
|
|
886
|
+
printMessage(
|
|
887
|
+
` - ${newUuid}${reUuid ? '*' : ''} (${nodeType})`,
|
|
888
|
+
'info',
|
|
889
|
+
false
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
// If the node has an identityResource config setting
|
|
893
|
+
// and the identityResource ends in 'user'
|
|
894
|
+
// and the node's identityResource is the same as the tree's identityResource
|
|
895
|
+
// change it to the current realm managed user identityResource otherwise leave it alone.
|
|
896
|
+
if (
|
|
897
|
+
innerNodeData.identityResource &&
|
|
898
|
+
innerNodeData.identityResource.endsWith('user') &&
|
|
899
|
+
innerNodeData.identityResource === treeObject.tree.identityResource
|
|
900
|
+
) {
|
|
901
|
+
innerNodeData.identityResource = `managed/${getRealmManagedUser()}`;
|
|
902
|
+
if (verbose)
|
|
903
|
+
printMessage(
|
|
904
|
+
`\n - identityResource: ${innerNodeData.identityResource}`,
|
|
905
|
+
'info',
|
|
906
|
+
false
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
// eslint-disable-next-line no-await-in-loop
|
|
911
|
+
await putNode(newUuid, nodeType, innerNodeData);
|
|
912
|
+
} catch (nodeImportError) {
|
|
913
|
+
if (
|
|
914
|
+
nodeImportError.response.status === 400 &&
|
|
915
|
+
nodeImportError.response.data.message ===
|
|
916
|
+
'Data validation failed for the attribute, Script'
|
|
917
|
+
) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
`Missing script ${
|
|
920
|
+
innerNodeData.script
|
|
921
|
+
} referenced by inner node ${innerNodeId}${
|
|
922
|
+
innerNodeId === newUuid ? '' : ` [${newUuid}]`
|
|
923
|
+
} (${innerNodeData._type._id}) in journey ${treeId}.`
|
|
924
|
+
);
|
|
925
|
+
} else {
|
|
926
|
+
printMessage(nodeImportError.response.data, 'error');
|
|
927
|
+
throw new Error(
|
|
928
|
+
`Error importing inner node ${innerNodeId}${
|
|
929
|
+
innerNodeId === newUuid ? '' : ` [${newUuid}]`
|
|
930
|
+
} in journey ${treeId}`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (verbose) printMessage('');
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Process nodes
|
|
939
|
+
if (treeObject.nodes && Object.entries(treeObject.nodes).length > 0) {
|
|
940
|
+
if (verbose) printMessage(' - Nodes:');
|
|
941
|
+
// eslint-disable-next-line prefer-const
|
|
942
|
+
for (let [nodeId, nodeData] of Object.entries(treeObject.nodes)) {
|
|
943
|
+
delete nodeData._rev;
|
|
944
|
+
const nodeType = nodeData._type._id;
|
|
945
|
+
if (!reUuid) {
|
|
946
|
+
newUuid = nodeId;
|
|
947
|
+
} else {
|
|
948
|
+
newUuid = uuidv4();
|
|
949
|
+
uuidMap[nodeId] = newUuid;
|
|
950
|
+
}
|
|
951
|
+
nodeData._id = newUuid;
|
|
952
|
+
|
|
953
|
+
if (nodeType === 'PageNode' && reUuid) {
|
|
954
|
+
for (const [, inPageNodeData] of Object.entries(nodeData.nodes)) {
|
|
955
|
+
const currentId = inPageNodeData._id;
|
|
956
|
+
nodeData = JSON.parse(
|
|
957
|
+
replaceAll(JSON.stringify(nodeData), currentId, uuidMap[currentId])
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (verbose)
|
|
963
|
+
printMessage(
|
|
964
|
+
` - ${newUuid}${reUuid ? '*' : ''} (${nodeType})`,
|
|
965
|
+
'info',
|
|
966
|
+
false
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// If the node has an identityResource config setting
|
|
970
|
+
// and the identityResource ends in 'user'
|
|
971
|
+
// and the node's identityResource is the same as the tree's identityResource
|
|
972
|
+
// change it to the current realm managed user identityResource otherwise leave it alone.
|
|
973
|
+
if (
|
|
974
|
+
nodeData.identityResource &&
|
|
975
|
+
nodeData.identityResource.endsWith('user') &&
|
|
976
|
+
nodeData.identityResource === treeObject.tree.identityResource
|
|
977
|
+
) {
|
|
978
|
+
nodeData.identityResource = `managed/${getRealmManagedUser()}`;
|
|
979
|
+
if (verbose)
|
|
980
|
+
printMessage(
|
|
981
|
+
`\n - identityResource: ${nodeData.identityResource}`,
|
|
982
|
+
'info',
|
|
983
|
+
false
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
// eslint-disable-next-line no-await-in-loop
|
|
988
|
+
await putNode(newUuid, nodeType, nodeData);
|
|
989
|
+
} catch (nodeImportError) {
|
|
990
|
+
if (
|
|
991
|
+
nodeImportError.response.status === 400 &&
|
|
992
|
+
nodeImportError.response.data.message ===
|
|
993
|
+
'Data validation failed for the attribute, Script'
|
|
994
|
+
) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
`Missing script ${nodeData.script} referenced by node ${nodeId}${
|
|
997
|
+
nodeId === newUuid ? '' : ` [${newUuid}]`
|
|
998
|
+
} (${nodeData._type._id}) in journey ${treeId}.`
|
|
999
|
+
);
|
|
1000
|
+
} else {
|
|
1001
|
+
printMessage(nodeImportError.response.data, 'error');
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
`Error importing node ${nodeId}${
|
|
1004
|
+
nodeId === newUuid ? '' : ` [${newUuid}]`
|
|
1005
|
+
} in journey ${treeId}`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (verbose) printMessage('');
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Process tree
|
|
1014
|
+
if (verbose) printMessage(' - Flow');
|
|
1015
|
+
|
|
1016
|
+
if (reUuid) {
|
|
1017
|
+
let journeyText = JSON.stringify(treeObject.tree, null, 2);
|
|
1018
|
+
for (const [oldId, newId] of Object.entries(uuidMap)) {
|
|
1019
|
+
journeyText = replaceAll(journeyText, oldId, newId);
|
|
1020
|
+
}
|
|
1021
|
+
treeObject.tree = JSON.parse(journeyText);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// If the tree has an identityResource config setting
|
|
1025
|
+
// and the identityResource ends in 'user'
|
|
1026
|
+
// Set the identityResource for the tree to the selected resource.
|
|
1027
|
+
if (
|
|
1028
|
+
treeObject.tree.identityResource &&
|
|
1029
|
+
treeObject.tree.identityResource.endsWith('user')
|
|
1030
|
+
) {
|
|
1031
|
+
treeObject.tree.identityResource = `managed/${getRealmManagedUser()}`;
|
|
1032
|
+
if (verbose)
|
|
1033
|
+
printMessage(
|
|
1034
|
+
` - identityResource: ${treeObject.tree.identityResource}`,
|
|
1035
|
+
'info',
|
|
1036
|
+
false
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
delete treeObject.tree._rev;
|
|
1041
|
+
try {
|
|
1042
|
+
await putTree(treeObject.tree._id, treeObject.tree);
|
|
1043
|
+
if (verbose) printMessage(`\n - Done`, 'info', true);
|
|
1044
|
+
} catch (importError) {
|
|
1045
|
+
if (
|
|
1046
|
+
importError.response.status === 400 &&
|
|
1047
|
+
importError.response.data.message === 'Invalid attribute specified.'
|
|
1048
|
+
) {
|
|
1049
|
+
const { validAttributes } = importError.response.data.detail;
|
|
1050
|
+
validAttributes.push('_id');
|
|
1051
|
+
Object.keys(treeObject.tree).forEach((attribute) => {
|
|
1052
|
+
if (!validAttributes.includes(attribute)) {
|
|
1053
|
+
if (verbose)
|
|
1054
|
+
printMessage(
|
|
1055
|
+
`\n - Removing invalid attribute: ${attribute}`,
|
|
1056
|
+
'info',
|
|
1057
|
+
false
|
|
1058
|
+
);
|
|
1059
|
+
delete treeObject.tree[attribute];
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
try {
|
|
1063
|
+
await putTree(treeObject.tree._id, treeObject.tree);
|
|
1064
|
+
if (verbose) printMessage(`\n - Done`, 'info', true);
|
|
1065
|
+
} catch (importError2) {
|
|
1066
|
+
printMessage(importError2.response.data, 'error');
|
|
1067
|
+
throw new Error(`Error importing journey flow ${treeId}`);
|
|
1068
|
+
}
|
|
1069
|
+
} else {
|
|
1070
|
+
printMessage(importError.response.data, 'error');
|
|
1071
|
+
throw new Error(`\nError importing journey flow ${treeId}`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Resolve journey dependencies
|
|
1078
|
+
* @param {Map} installedJorneys Map of installed journeys
|
|
1079
|
+
* @param {Map} journeyMap Map of journeys to resolve dependencies for
|
|
1080
|
+
* @param {[String]} unresolvedJourneys Map to hold the names of unresolved journeys and their dependencies
|
|
1081
|
+
* @param {[String]} resolvedJourneys Array to hold the names of resolved journeys
|
|
1082
|
+
* @param {int} index Depth of recursion
|
|
1083
|
+
*/
|
|
1084
|
+
async function resolveDependencies(
|
|
1085
|
+
installedJorneys,
|
|
1086
|
+
journeyMap,
|
|
1087
|
+
unresolvedJourneys,
|
|
1088
|
+
resolvedJourneys,
|
|
1089
|
+
index = -1
|
|
1090
|
+
) {
|
|
1091
|
+
let before = -1;
|
|
1092
|
+
let after = index;
|
|
1093
|
+
if (index !== -1) {
|
|
1094
|
+
before = index;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
for (const tree in journeyMap) {
|
|
1098
|
+
if ({}.hasOwnProperty.call(journeyMap, tree)) {
|
|
1099
|
+
const dependencies = [];
|
|
1100
|
+
for (const node in journeyMap[tree].nodes) {
|
|
1101
|
+
if (
|
|
1102
|
+
journeyMap[tree].nodes[node]._type._id === 'InnerTreeEvaluatorNode'
|
|
1103
|
+
) {
|
|
1104
|
+
dependencies.push(journeyMap[tree].nodes[node].tree);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
let allResolved = true;
|
|
1108
|
+
for (const dependency of dependencies) {
|
|
1109
|
+
if (
|
|
1110
|
+
!resolvedJourneys.includes(dependency) &&
|
|
1111
|
+
!installedJorneys.includes(dependency)
|
|
1112
|
+
) {
|
|
1113
|
+
allResolved = false;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (allResolved) {
|
|
1117
|
+
if (resolvedJourneys.indexOf(tree) === -1) resolvedJourneys.push(tree);
|
|
1118
|
+
// remove from unresolvedJourneys array
|
|
1119
|
+
// for (let i = 0; i < unresolvedJourneys.length; i += 1) {
|
|
1120
|
+
// if (unresolvedJourneys[i] === tree) {
|
|
1121
|
+
// unresolvedJourneys.splice(i, 1);
|
|
1122
|
+
// i -= 1;
|
|
1123
|
+
// }
|
|
1124
|
+
// }
|
|
1125
|
+
delete unresolvedJourneys[tree];
|
|
1126
|
+
// } else if (!unresolvedJourneys.includes(tree)) {
|
|
1127
|
+
} else {
|
|
1128
|
+
// unresolvedJourneys.push(tree);
|
|
1129
|
+
unresolvedJourneys[tree] = dependencies;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
after = Object.keys(unresolvedJourneys).length;
|
|
1134
|
+
if (index !== -1 && after === before) {
|
|
1135
|
+
// This is the end, no progress was made since the last recursion
|
|
1136
|
+
// printMessage(
|
|
1137
|
+
// `Journeys with unresolved dependencies: ${unresolvedJourneys}`,
|
|
1138
|
+
// 'error'
|
|
1139
|
+
// );
|
|
1140
|
+
} else if (after > 0) {
|
|
1141
|
+
resolveDependencies(
|
|
1142
|
+
installedJorneys,
|
|
1143
|
+
journeyMap,
|
|
1144
|
+
unresolvedJourneys,
|
|
1145
|
+
resolvedJourneys,
|
|
1146
|
+
after
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Import a journey from file
|
|
1153
|
+
* @param {String} journeyId journey id/name
|
|
1154
|
+
* @param {String} file import file name
|
|
1155
|
+
* @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
|
|
1156
|
+
*/
|
|
1157
|
+
export async function importJourneyFromFile(journeyId, file, options) {
|
|
1158
|
+
const { verbose } = options;
|
|
1159
|
+
fs.readFile(file, 'utf8', async (err, data) => {
|
|
1160
|
+
if (err) throw err;
|
|
1161
|
+
let journeyData = JSON.parse(data);
|
|
1162
|
+
// check if this is a file with multiple trees and get journey by id
|
|
1163
|
+
if (journeyData.trees && journeyData.trees[journeyId]) {
|
|
1164
|
+
journeyData = journeyData.trees[journeyId];
|
|
1165
|
+
} else if (journeyData.trees) {
|
|
1166
|
+
journeyData = null;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// if a journeyId was specified, only import the matching journey
|
|
1170
|
+
if (journeyData && journeyId === journeyData.tree._id) {
|
|
1171
|
+
// attempt dependency resolution for single tree import
|
|
1172
|
+
const installedJourneys = (await getTrees()).data.result.map(
|
|
1173
|
+
(x) => x._id
|
|
1174
|
+
);
|
|
1175
|
+
const unresolvedJourneys = {};
|
|
1176
|
+
const resolvedJourneys = [];
|
|
1177
|
+
showSpinner('Resolving dependencies');
|
|
1178
|
+
await resolveDependencies(
|
|
1179
|
+
installedJourneys,
|
|
1180
|
+
{ [journeyId]: journeyData },
|
|
1181
|
+
unresolvedJourneys,
|
|
1182
|
+
resolvedJourneys
|
|
1183
|
+
);
|
|
1184
|
+
if (Object.keys(unresolvedJourneys).length === 0) {
|
|
1185
|
+
succeedSpinner(`Resolved all dependencies.`);
|
|
1186
|
+
|
|
1187
|
+
if (!verbose) showSpinner(`Importing ${journeyId}...`);
|
|
1188
|
+
importTree(journeyData, options)
|
|
1189
|
+
.then(() => {
|
|
1190
|
+
if (verbose) showSpinner(`Importing ${journeyId}...`);
|
|
1191
|
+
succeedSpinner(`Imported ${journeyId}.`);
|
|
1192
|
+
})
|
|
1193
|
+
.catch((importError) => {
|
|
1194
|
+
if (verbose) showSpinner(`Importing ${journeyId}...`);
|
|
1195
|
+
failSpinner(`${importError}`);
|
|
1196
|
+
});
|
|
1197
|
+
} else {
|
|
1198
|
+
failSpinner(`Unresolved dependencies:`);
|
|
1199
|
+
for (const journey of Object.keys(unresolvedJourneys)) {
|
|
1200
|
+
printMessage(
|
|
1201
|
+
` ${journey} requires ${unresolvedJourneys[journey]}`,
|
|
1202
|
+
'error'
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
// end dependency resolution for single tree import
|
|
1207
|
+
} else {
|
|
1208
|
+
showSpinner(`Importing ${journeyId}...`);
|
|
1209
|
+
failSpinner(`${journeyId} not found!`);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Import first journey from file
|
|
1216
|
+
* @param {String} file import file name
|
|
1217
|
+
* @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
|
|
1218
|
+
*/
|
|
1219
|
+
export async function importFirstJourneyFromFile(file, options) {
|
|
1220
|
+
const { verbose } = options;
|
|
1221
|
+
fs.readFile(file, 'utf8', async (err, data) => {
|
|
1222
|
+
if (err) throw err;
|
|
1223
|
+
let journeyData = _.cloneDeep(JSON.parse(data));
|
|
1224
|
+
let journeyId = null;
|
|
1225
|
+
// single tree
|
|
1226
|
+
if (journeyData.tree) {
|
|
1227
|
+
journeyId = _.cloneDeep(journeyData.tree._id);
|
|
1228
|
+
}
|
|
1229
|
+
// multiple trees, so get the first tree
|
|
1230
|
+
else if (journeyData.trees) {
|
|
1231
|
+
for (const treeId in journeyData.trees) {
|
|
1232
|
+
if (Object.hasOwnProperty.call(journeyData.trees, treeId)) {
|
|
1233
|
+
journeyId = treeId;
|
|
1234
|
+
journeyData = journeyData.trees[treeId];
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// if a journeyId was specified, only import the matching journey
|
|
1241
|
+
if (journeyData && journeyId) {
|
|
1242
|
+
// attempt dependency resolution for single tree import
|
|
1243
|
+
const installedJourneys = (await getTrees()).data.result.map(
|
|
1244
|
+
(x) => x._id
|
|
1245
|
+
);
|
|
1246
|
+
const unresolvedJourneys = {};
|
|
1247
|
+
const resolvedJourneys = [];
|
|
1248
|
+
showSpinner('Resolving dependencies');
|
|
1249
|
+
await resolveDependencies(
|
|
1250
|
+
installedJourneys,
|
|
1251
|
+
{ [journeyId]: journeyData },
|
|
1252
|
+
unresolvedJourneys,
|
|
1253
|
+
resolvedJourneys
|
|
1254
|
+
);
|
|
1255
|
+
if (Object.keys(unresolvedJourneys).length === 0) {
|
|
1256
|
+
succeedSpinner(`Resolved all dependencies.`);
|
|
1257
|
+
|
|
1258
|
+
if (!verbose) showSpinner(`Importing ${journeyId}...`);
|
|
1259
|
+
importTree(journeyData, options)
|
|
1260
|
+
.then(() => {
|
|
1261
|
+
if (verbose) showSpinner(`Importing ${journeyId}...`);
|
|
1262
|
+
succeedSpinner(`Imported ${journeyId}.`);
|
|
1263
|
+
})
|
|
1264
|
+
.catch((importError) => {
|
|
1265
|
+
if (verbose) showSpinner(`Importing ${journeyId}...`);
|
|
1266
|
+
failSpinner(`${importError}`);
|
|
1267
|
+
});
|
|
1268
|
+
} else {
|
|
1269
|
+
failSpinner(`Unresolved dependencies:`);
|
|
1270
|
+
for (const journey of Object.keys(unresolvedJourneys)) {
|
|
1271
|
+
printMessage(
|
|
1272
|
+
` ${journey} requires ${unresolvedJourneys[journey]}`,
|
|
1273
|
+
'error'
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
} else {
|
|
1278
|
+
showSpinner(`Importing...`);
|
|
1279
|
+
failSpinner(`No journeys found!`);
|
|
1280
|
+
}
|
|
1281
|
+
// end dependency resolution for single tree import
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Helper to import multiple trees from a tree map
|
|
1287
|
+
* @param {Object} treesMap map of trees object
|
|
1288
|
+
* @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
|
|
1289
|
+
*/
|
|
1290
|
+
async function importAllTrees(treesMap, options) {
|
|
1291
|
+
const installedJourneys = (await getTrees()).data.result.map((x) => x._id);
|
|
1292
|
+
const unresolvedJourneys = {};
|
|
1293
|
+
const resolvedJourneys = [];
|
|
1294
|
+
showSpinner('Resolving dependencies');
|
|
1295
|
+
await resolveDependencies(
|
|
1296
|
+
installedJourneys,
|
|
1297
|
+
treesMap,
|
|
1298
|
+
unresolvedJourneys,
|
|
1299
|
+
resolvedJourneys
|
|
1300
|
+
);
|
|
1301
|
+
if (Object.keys(unresolvedJourneys).length === 0) {
|
|
1302
|
+
succeedSpinner(`Resolved all dependencies.`);
|
|
1303
|
+
} else {
|
|
1304
|
+
failSpinner(
|
|
1305
|
+
`${
|
|
1306
|
+
Object.keys(unresolvedJourneys).length
|
|
1307
|
+
} journeys with unresolved dependencies:`
|
|
1308
|
+
);
|
|
1309
|
+
for (const journey of Object.keys(unresolvedJourneys)) {
|
|
1310
|
+
printMessage(
|
|
1311
|
+
` - ${journey} requires ${unresolvedJourneys[journey]}`,
|
|
1312
|
+
'info'
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
createProgressBar(resolvedJourneys.length, 'Importing');
|
|
1317
|
+
for (const tree of resolvedJourneys) {
|
|
1318
|
+
try {
|
|
1319
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1320
|
+
await importTree(treesMap[tree], options);
|
|
1321
|
+
updateProgressBar(`${tree}`);
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
printMessage(`\n${error.message}`, 'error');
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
stopProgressBar('Done');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Import all journeys from file
|
|
1331
|
+
* @param {*} file import file name
|
|
1332
|
+
* @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
|
|
1333
|
+
*/
|
|
1334
|
+
export async function importJourneysFromFile(file, options) {
|
|
1335
|
+
fs.readFile(file, 'utf8', (err, data) => {
|
|
1336
|
+
if (err) throw err;
|
|
1337
|
+
const fileData = JSON.parse(data);
|
|
1338
|
+
importAllTrees(fileData.trees, options);
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Import all journeys from separate files
|
|
1344
|
+
* @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
|
|
1345
|
+
*/
|
|
1346
|
+
export async function importJourneysFromFiles(options) {
|
|
1347
|
+
const names = fs.readdirSync('.');
|
|
1348
|
+
const jsonFiles = names.filter((name) =>
|
|
1349
|
+
name.toLowerCase().endsWith('.journey.json')
|
|
1350
|
+
);
|
|
1351
|
+
const allJourneysData = { trees: {} };
|
|
1352
|
+
for (const file of jsonFiles) {
|
|
1353
|
+
const journeyData = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1354
|
+
allJourneysData.trees[journeyData.tree._id] = journeyData;
|
|
1355
|
+
}
|
|
1356
|
+
importAllTrees(allJourneysData.trees, options);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Describe a tree
|
|
1361
|
+
* @param {Object} treeData tree
|
|
1362
|
+
* @returns {Object} an object describing the tree
|
|
1363
|
+
*/
|
|
1364
|
+
export function describeTree(treeData) {
|
|
1365
|
+
const treeMap = {};
|
|
1366
|
+
const nodeTypeMap = {};
|
|
1367
|
+
const scriptsMap = {};
|
|
1368
|
+
const emailTemplatesMap = {};
|
|
1369
|
+
treeMap.treeName = treeData.tree._id;
|
|
1370
|
+
for (const [, nodeData] of Object.entries(treeData.nodes)) {
|
|
1371
|
+
if (nodeTypeMap[nodeData._type._id]) {
|
|
1372
|
+
nodeTypeMap[nodeData._type._id] += 1;
|
|
1373
|
+
} else {
|
|
1374
|
+
nodeTypeMap[nodeData._type._id] = 1;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
for (const [, nodeData] of Object.entries(treeData.innerNodes)) {
|
|
1379
|
+
if (nodeTypeMap[nodeData._type._id]) {
|
|
1380
|
+
nodeTypeMap[nodeData._type._id] += 1;
|
|
1381
|
+
} else {
|
|
1382
|
+
nodeTypeMap[nodeData._type._id] = 1;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
for (const [, scriptData] of Object.entries(treeData.scripts)) {
|
|
1387
|
+
scriptsMap[scriptData.name] = scriptData.description;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
for (const [id, data] of Object.entries(treeData.emailTemplates)) {
|
|
1391
|
+
emailTemplatesMap[id] = data.displayName;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
treeMap.nodeTypes = nodeTypeMap;
|
|
1395
|
+
treeMap.scripts = scriptsMap;
|
|
1396
|
+
treeMap.emailTemplates = emailTemplatesMap;
|
|
1397
|
+
return treeMap;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Find all node configuration objects that are no longer referenced by any tree
|
|
1402
|
+
*/
|
|
1403
|
+
async function findOrphanedNodes() {
|
|
1404
|
+
const allNodes = [];
|
|
1405
|
+
const orphanedNodes = [];
|
|
1406
|
+
let types = [];
|
|
1407
|
+
const allJourneys = (await getTrees()).data.result;
|
|
1408
|
+
let errorMessage = '';
|
|
1409
|
+
const errorTypes = [];
|
|
1410
|
+
|
|
1411
|
+
showSpinner(`Counting total nodes...`);
|
|
1412
|
+
try {
|
|
1413
|
+
types = (await getNodeTypes()).data.result;
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
printMessage('Error retrieving all available node types:', 'error');
|
|
1416
|
+
printMessage(error.response.data, 'error');
|
|
1417
|
+
return [];
|
|
1418
|
+
}
|
|
1419
|
+
for (const type of types) {
|
|
1420
|
+
try {
|
|
1421
|
+
// eslint-disable-next-line no-await-in-loop, no-loop-func
|
|
1422
|
+
(await getNodesByType(type._id)).data.result.forEach((node) => {
|
|
1423
|
+
allNodes.push(node);
|
|
1424
|
+
spinSpinner(`${allNodes.length} total nodes${errorMessage}`);
|
|
1425
|
+
});
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
errorTypes.push(type._id);
|
|
1428
|
+
errorMessage = ` (Skipped type(s): ${errorTypes})`.yellow;
|
|
1429
|
+
spinSpinner(`${allNodes.length} total nodes${errorMessage}`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (errorTypes.length > 0) {
|
|
1433
|
+
warnSpinner(`${allNodes.length} total nodes${errorMessage}`);
|
|
1434
|
+
} else {
|
|
1435
|
+
succeedSpinner(`${allNodes.length} total nodes`);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
showSpinner('Counting active nodes...');
|
|
1439
|
+
const activeNodes = [];
|
|
1440
|
+
for (const journey of allJourneys) {
|
|
1441
|
+
for (const nodeId in journey.nodes) {
|
|
1442
|
+
if ({}.hasOwnProperty.call(journey.nodes, nodeId)) {
|
|
1443
|
+
activeNodes.push(nodeId);
|
|
1444
|
+
spinSpinner(`${activeNodes.length} active nodes`);
|
|
1445
|
+
const node = journey.nodes[nodeId];
|
|
1446
|
+
if (containerNodes.includes(node.nodeType)) {
|
|
1447
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1448
|
+
const containerNode = (await getNode(nodeId, node.nodeType)).data;
|
|
1449
|
+
containerNode.nodes.forEach((n) => {
|
|
1450
|
+
activeNodes.push(n._id);
|
|
1451
|
+
spinSpinner(`${activeNodes.length} active nodes`);
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
succeedSpinner(`${activeNodes.length} active nodes`);
|
|
1458
|
+
|
|
1459
|
+
showSpinner('Calculating orphaned nodes...');
|
|
1460
|
+
const diff = allNodes.filter((x) => !activeNodes.includes(x._id));
|
|
1461
|
+
diff.forEach((x) => orphanedNodes.push(x));
|
|
1462
|
+
succeedSpinner(`${orphanedNodes.length} orphaned nodes`);
|
|
1463
|
+
return orphanedNodes;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* Remove orphaned nodes
|
|
1468
|
+
* @param {[Object]} orphanedNodes Pass in an array of orphaned node configuration objects to remove
|
|
1469
|
+
*/
|
|
1470
|
+
async function removeOrphanedNodes(orphanedNodes) {
|
|
1471
|
+
createProgressBar(orphanedNodes.length, 'Removing orphaned nodes...');
|
|
1472
|
+
for (const node of orphanedNodes) {
|
|
1473
|
+
updateProgressBar(`Removing ${node._id}...`);
|
|
1474
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1475
|
+
await deleteNode(node._id, node._type._id).catch((deleteError) => {
|
|
1476
|
+
printMessage(`${deleteError}`, 'error');
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
stopProgressBar(`Removed ${orphanedNodes.length} orphaned nodes.`);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Prune orphaned nodes
|
|
1484
|
+
*/
|
|
1485
|
+
export async function prune() {
|
|
1486
|
+
const orphanedNodes = await findOrphanedNodes();
|
|
1487
|
+
if (orphanedNodes.length > 0) {
|
|
1488
|
+
const ok = await yesno({
|
|
1489
|
+
question: 'Prune (permanently delete) orphaned nodes? (y|n):',
|
|
1490
|
+
});
|
|
1491
|
+
if (ok) {
|
|
1492
|
+
await removeOrphanedNodes(orphanedNodes);
|
|
1493
|
+
}
|
|
1494
|
+
} else {
|
|
1495
|
+
printMessage('No orphaned nodes found.');
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const OOTB_NODE_TYPES_7 = [
|
|
1500
|
+
'AcceptTermsAndConditionsNode',
|
|
1501
|
+
'AccountActiveDecisionNode',
|
|
1502
|
+
'AccountLockoutNode',
|
|
1503
|
+
'AgentDataStoreDecisionNode',
|
|
1504
|
+
'AnonymousSessionUpgradeNode',
|
|
1505
|
+
'AnonymousUserNode',
|
|
1506
|
+
'AttributeCollectorNode',
|
|
1507
|
+
'AttributePresentDecisionNode',
|
|
1508
|
+
'AttributeValueDecisionNode',
|
|
1509
|
+
'AuthLevelDecisionNode',
|
|
1510
|
+
'ChoiceCollectorNode',
|
|
1511
|
+
'ConsentNode',
|
|
1512
|
+
'CookiePresenceDecisionNode',
|
|
1513
|
+
'CreateObjectNode',
|
|
1514
|
+
'CreatePasswordNode',
|
|
1515
|
+
'DataStoreDecisionNode',
|
|
1516
|
+
'DeviceGeoFencingNode',
|
|
1517
|
+
'DeviceLocationMatchNode',
|
|
1518
|
+
'DeviceMatchNode',
|
|
1519
|
+
'DeviceProfileCollectorNode',
|
|
1520
|
+
'DeviceSaveNode',
|
|
1521
|
+
'DeviceTamperingVerificationNode',
|
|
1522
|
+
'DisplayUserNameNode',
|
|
1523
|
+
'EmailSuspendNode',
|
|
1524
|
+
'EmailTemplateNode',
|
|
1525
|
+
'IdentifyExistingUserNode',
|
|
1526
|
+
'IncrementLoginCountNode',
|
|
1527
|
+
'InnerTreeEvaluatorNode',
|
|
1528
|
+
'IotAuthenticationNode',
|
|
1529
|
+
'IotRegistrationNode',
|
|
1530
|
+
'KbaCreateNode',
|
|
1531
|
+
'KbaDecisionNode',
|
|
1532
|
+
'KbaVerifyNode',
|
|
1533
|
+
'LdapDecisionNode',
|
|
1534
|
+
'LoginCountDecisionNode',
|
|
1535
|
+
'MessageNode',
|
|
1536
|
+
'MetadataNode',
|
|
1537
|
+
'MeterNode',
|
|
1538
|
+
'ModifyAuthLevelNode',
|
|
1539
|
+
'OneTimePasswordCollectorDecisionNode',
|
|
1540
|
+
'OneTimePasswordGeneratorNode',
|
|
1541
|
+
'OneTimePasswordSmsSenderNode',
|
|
1542
|
+
'OneTimePasswordSmtpSenderNode',
|
|
1543
|
+
'PageNode',
|
|
1544
|
+
'PasswordCollectorNode',
|
|
1545
|
+
'PatchObjectNode',
|
|
1546
|
+
'PersistentCookieDecisionNode',
|
|
1547
|
+
'PollingWaitNode',
|
|
1548
|
+
'ProfileCompletenessDecisionNode',
|
|
1549
|
+
'ProvisionDynamicAccountNode',
|
|
1550
|
+
'ProvisionIdmAccountNode',
|
|
1551
|
+
'PushAuthenticationSenderNode',
|
|
1552
|
+
'PushResultVerifierNode',
|
|
1553
|
+
'QueryFilterDecisionNode',
|
|
1554
|
+
'RecoveryCodeCollectorDecisionNode',
|
|
1555
|
+
'RecoveryCodeDisplayNode',
|
|
1556
|
+
'RegisterLogoutWebhookNode',
|
|
1557
|
+
'RemoveSessionPropertiesNode',
|
|
1558
|
+
'RequiredAttributesDecisionNode',
|
|
1559
|
+
'RetryLimitDecisionNode',
|
|
1560
|
+
'ScriptedDecisionNode',
|
|
1561
|
+
'SelectIdPNode',
|
|
1562
|
+
'SessionDataNode',
|
|
1563
|
+
'SetFailureUrlNode',
|
|
1564
|
+
'SetPersistentCookieNode',
|
|
1565
|
+
'SetSessionPropertiesNode',
|
|
1566
|
+
'SetSuccessUrlNode',
|
|
1567
|
+
'SocialFacebookNode',
|
|
1568
|
+
'SocialGoogleNode',
|
|
1569
|
+
'SocialNode',
|
|
1570
|
+
'SocialOAuthIgnoreProfileNode',
|
|
1571
|
+
'SocialOpenIdConnectNode',
|
|
1572
|
+
'SocialProviderHandlerNode',
|
|
1573
|
+
'TermsAndConditionsDecisionNode',
|
|
1574
|
+
'TimeSinceDecisionNode',
|
|
1575
|
+
'TimerStartNode',
|
|
1576
|
+
'TimerStopNode',
|
|
1577
|
+
'UsernameCollectorNode',
|
|
1578
|
+
'ValidatedPasswordNode',
|
|
1579
|
+
'ValidatedUsernameNode',
|
|
1580
|
+
'WebAuthnAuthenticationNode',
|
|
1581
|
+
'WebAuthnDeviceStorageNode',
|
|
1582
|
+
'WebAuthnRegistrationNode',
|
|
1583
|
+
'ZeroPageLoginNode',
|
|
1584
|
+
'product-CertificateCollectorNode',
|
|
1585
|
+
'product-CertificateUserExtractorNode',
|
|
1586
|
+
'product-CertificateValidationNode',
|
|
1587
|
+
'product-KerberosNode',
|
|
1588
|
+
'product-ReCaptchaNode',
|
|
1589
|
+
'product-Saml2Node',
|
|
1590
|
+
'product-WriteFederationInformationNode',
|
|
1591
|
+
];
|
|
1592
|
+
|
|
1593
|
+
const OOTB_NODE_TYPES_7_1 = [
|
|
1594
|
+
'PushRegistrationNode',
|
|
1595
|
+
'GetAuthenticatorAppNode',
|
|
1596
|
+
'MultiFactorRegistrationOptionsNode',
|
|
1597
|
+
'OptOutMultiFactorAuthenticationNode',
|
|
1598
|
+
].concat(OOTB_NODE_TYPES_7);
|
|
1599
|
+
|
|
1600
|
+
const OOTB_NODE_TYPES_7_2 = [
|
|
1601
|
+
'OathRegistrationNode',
|
|
1602
|
+
'OathTokenVerifierNode',
|
|
1603
|
+
'PassthroughAuthenticationNode',
|
|
1604
|
+
'ConfigProviderNode',
|
|
1605
|
+
'DebugNode',
|
|
1606
|
+
].concat(OOTB_NODE_TYPES_7_1);
|
|
1607
|
+
|
|
1608
|
+
const OOTB_NODE_TYPES_6_5 = [
|
|
1609
|
+
'AbstractSocialAuthLoginNode',
|
|
1610
|
+
'AccountLockoutNode',
|
|
1611
|
+
'AgentDataStoreDecisionNode',
|
|
1612
|
+
'AnonymousUserNode',
|
|
1613
|
+
'AuthLevelDecisionNode',
|
|
1614
|
+
'ChoiceCollectorNode',
|
|
1615
|
+
'CookiePresenceDecisionNode',
|
|
1616
|
+
'CreatePasswordNode',
|
|
1617
|
+
'DataStoreDecisionNode',
|
|
1618
|
+
'InnerTreeEvaluatorNode',
|
|
1619
|
+
'LdapDecisionNode',
|
|
1620
|
+
'MessageNode',
|
|
1621
|
+
'MetadataNode',
|
|
1622
|
+
'MeterNode',
|
|
1623
|
+
'ModifyAuthLevelNode',
|
|
1624
|
+
'OneTimePasswordCollectorDecisionNode',
|
|
1625
|
+
'OneTimePasswordGeneratorNode',
|
|
1626
|
+
'OneTimePasswordSmsSenderNode',
|
|
1627
|
+
'OneTimePasswordSmtpSenderNode',
|
|
1628
|
+
'PageNode',
|
|
1629
|
+
'PasswordCollectorNode',
|
|
1630
|
+
'PersistentCookieDecisionNode',
|
|
1631
|
+
'PollingWaitNode',
|
|
1632
|
+
'ProvisionDynamicAccountNode',
|
|
1633
|
+
'ProvisionIdmAccountNode',
|
|
1634
|
+
'PushAuthenticationSenderNode',
|
|
1635
|
+
'PushResultVerifierNode',
|
|
1636
|
+
'RecoveryCodeCollectorDecisionNode',
|
|
1637
|
+
'RecoveryCodeDisplayNode',
|
|
1638
|
+
'RegisterLogoutWebhookNode',
|
|
1639
|
+
'RemoveSessionPropertiesNode',
|
|
1640
|
+
'RetryLimitDecisionNode',
|
|
1641
|
+
'ScriptedDecisionNode',
|
|
1642
|
+
'SessionDataNode',
|
|
1643
|
+
'SetFailureUrlNode',
|
|
1644
|
+
'SetPersistentCookieNode',
|
|
1645
|
+
'SetSessionPropertiesNode',
|
|
1646
|
+
'SetSuccessUrlNode',
|
|
1647
|
+
'SocialFacebookNode',
|
|
1648
|
+
'SocialGoogleNode',
|
|
1649
|
+
'SocialNode',
|
|
1650
|
+
'SocialOAuthIgnoreProfileNode',
|
|
1651
|
+
'SocialOpenIdConnectNode',
|
|
1652
|
+
'TimerStartNode',
|
|
1653
|
+
'TimerStopNode',
|
|
1654
|
+
'UsernameCollectorNode',
|
|
1655
|
+
'WebAuthnAuthenticationNode',
|
|
1656
|
+
'WebAuthnRegistrationNode',
|
|
1657
|
+
'ZeroPageLoginNode',
|
|
1658
|
+
];
|
|
1659
|
+
|
|
1660
|
+
const OOTB_NODE_TYPES_6 = [
|
|
1661
|
+
'AbstractSocialAuthLoginNode',
|
|
1662
|
+
'AccountLockoutNode',
|
|
1663
|
+
'AgentDataStoreDecisionNode',
|
|
1664
|
+
'AnonymousUserNode',
|
|
1665
|
+
'AuthLevelDecisionNode',
|
|
1666
|
+
'ChoiceCollectorNode',
|
|
1667
|
+
'CookiePresenceDecisionNode',
|
|
1668
|
+
'CreatePasswordNode',
|
|
1669
|
+
'DataStoreDecisionNode',
|
|
1670
|
+
'InnerTreeEvaluatorNode',
|
|
1671
|
+
'LdapDecisionNode',
|
|
1672
|
+
'MessageNode',
|
|
1673
|
+
'MetadataNode',
|
|
1674
|
+
'MeterNode',
|
|
1675
|
+
'ModifyAuthLevelNode',
|
|
1676
|
+
'OneTimePasswordCollectorDecisionNode',
|
|
1677
|
+
'OneTimePasswordGeneratorNode',
|
|
1678
|
+
'OneTimePasswordSmsSenderNode',
|
|
1679
|
+
'OneTimePasswordSmtpSenderNode',
|
|
1680
|
+
'PageNode',
|
|
1681
|
+
'PasswordCollectorNode',
|
|
1682
|
+
'PersistentCookieDecisionNode',
|
|
1683
|
+
'PollingWaitNode',
|
|
1684
|
+
'ProvisionDynamicAccountNode',
|
|
1685
|
+
'ProvisionIdmAccountNode',
|
|
1686
|
+
'PushAuthenticationSenderNode',
|
|
1687
|
+
'PushResultVerifierNode',
|
|
1688
|
+
'RecoveryCodeCollectorDecisionNode',
|
|
1689
|
+
'RecoveryCodeDisplayNode',
|
|
1690
|
+
'RegisterLogoutWebhookNode',
|
|
1691
|
+
'RemoveSessionPropertiesNode',
|
|
1692
|
+
'RetryLimitDecisionNode',
|
|
1693
|
+
'ScriptedDecisionNode',
|
|
1694
|
+
'SessionDataNode',
|
|
1695
|
+
'SetFailureUrlNode',
|
|
1696
|
+
'SetPersistentCookieNode',
|
|
1697
|
+
'SetSessionPropertiesNode',
|
|
1698
|
+
'SetSuccessUrlNode',
|
|
1699
|
+
'SocialFacebookNode',
|
|
1700
|
+
'SocialGoogleNode',
|
|
1701
|
+
'SocialNode',
|
|
1702
|
+
'SocialOAuthIgnoreProfileNode',
|
|
1703
|
+
'SocialOpenIdConnectNode',
|
|
1704
|
+
'TimerStartNode',
|
|
1705
|
+
'TimerStopNode',
|
|
1706
|
+
'UsernameCollectorNode',
|
|
1707
|
+
'WebAuthnAuthenticationNode',
|
|
1708
|
+
'WebAuthnRegistrationNode',
|
|
1709
|
+
'ZeroPageLoginNode',
|
|
1710
|
+
];
|
|
1711
|
+
|
|
1712
|
+
/**
|
|
1713
|
+
* Analyze if a journey contains any custom nodes considering the detected or the overridden version.
|
|
1714
|
+
* @param {Object} journey Journey/tree configuration object
|
|
1715
|
+
* @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.
|
|
1716
|
+
*/
|
|
1717
|
+
async function isCustom(journey) {
|
|
1718
|
+
let ootbNodeTypes = [];
|
|
1719
|
+
const nodeList = journey.nodes;
|
|
1720
|
+
// console.log(nodeList);
|
|
1721
|
+
// console.log(storage.session.getAmVersion());
|
|
1722
|
+
switch (storage.session.getAmVersion()) {
|
|
1723
|
+
case '7.1.0':
|
|
1724
|
+
ootbNodeTypes = OOTB_NODE_TYPES_7_1.slice(0);
|
|
1725
|
+
break;
|
|
1726
|
+
case '7.2.0':
|
|
1727
|
+
// console.log("here");
|
|
1728
|
+
ootbNodeTypes = OOTB_NODE_TYPES_7_2.slice(0);
|
|
1729
|
+
break;
|
|
1730
|
+
case '7.0.0':
|
|
1731
|
+
case '7.0.1':
|
|
1732
|
+
case '7.0.2':
|
|
1733
|
+
ootbNodeTypes = OOTB_NODE_TYPES_7.slice(0);
|
|
1734
|
+
break;
|
|
1735
|
+
case '6.5.3':
|
|
1736
|
+
case '6.5.2.3':
|
|
1737
|
+
case '6.5.2.2':
|
|
1738
|
+
case '6.5.2.1':
|
|
1739
|
+
case '6.5.2':
|
|
1740
|
+
case '6.5.1':
|
|
1741
|
+
case '6.5.0.2':
|
|
1742
|
+
case '6.5.0.1':
|
|
1743
|
+
ootbNodeTypes = OOTB_NODE_TYPES_6_5.slice(0);
|
|
1744
|
+
break;
|
|
1745
|
+
case '6.0.0.7':
|
|
1746
|
+
case '6.0.0.6':
|
|
1747
|
+
case '6.0.0.5':
|
|
1748
|
+
case '6.0.0.4':
|
|
1749
|
+
case '6.0.0.3':
|
|
1750
|
+
case '6.0.0.2':
|
|
1751
|
+
case '6.0.0.1':
|
|
1752
|
+
case '6.0.0':
|
|
1753
|
+
ootbNodeTypes = OOTB_NODE_TYPES_6.slice(0);
|
|
1754
|
+
break;
|
|
1755
|
+
default:
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1758
|
+
const results = [];
|
|
1759
|
+
for (const node in nodeList) {
|
|
1760
|
+
if ({}.hasOwnProperty.call(nodeList, node)) {
|
|
1761
|
+
if (!ootbNodeTypes.includes(nodeList[node].nodeType)) {
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
if (containerNodes.includes(nodeList[node].nodeType)) {
|
|
1765
|
+
results.push(
|
|
1766
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1767
|
+
(await getNode(node, nodeList[node].nodeType)).then(
|
|
1768
|
+
(response) => response.data
|
|
1769
|
+
)
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const pageNodes = await Promise.all(results);
|
|
1775
|
+
let custom = false;
|
|
1776
|
+
pageNodes.forEach((pageNode) => {
|
|
1777
|
+
if (pageNode != null) {
|
|
1778
|
+
for (const pnode of pageNode.nodes) {
|
|
1779
|
+
if (!ootbNodeTypes.includes(pnode.nodeType)) {
|
|
1780
|
+
custom = true;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
} else {
|
|
1784
|
+
printMessage(
|
|
1785
|
+
`isCustom ERROR: can't get ${nodeList[pageNode].nodeType} with id ${pageNode} in ${journey._id}`,
|
|
1786
|
+
'error'
|
|
1787
|
+
);
|
|
1788
|
+
custom = false;
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
return custom;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* List all the journeys/trees
|
|
1796
|
+
* @param {boolean} long Long version, all the fields
|
|
1797
|
+
* @param {boolean} analyze Analyze journeys/trees for custom nodes (expensive)
|
|
1798
|
+
*/
|
|
1799
|
+
export async function listJourneys(long = false, analyze = false) {
|
|
1800
|
+
let journeys = [];
|
|
1801
|
+
try {
|
|
1802
|
+
journeys = (await getTrees()).data.result;
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
printMessage(`${error.message}`, 'error');
|
|
1805
|
+
printMessage(error.response.data, 'error');
|
|
1806
|
+
}
|
|
1807
|
+
journeys.sort((a, b) => a._id.localeCompare(b._id));
|
|
1808
|
+
let customTrees = Array(journeys.length).fill(false);
|
|
1809
|
+
if (analyze) {
|
|
1810
|
+
const results = [];
|
|
1811
|
+
for (const journey of journeys) {
|
|
1812
|
+
results.push(isCustom(journey));
|
|
1813
|
+
}
|
|
1814
|
+
customTrees = await Promise.all(results);
|
|
1815
|
+
}
|
|
1816
|
+
if (!long) {
|
|
1817
|
+
for (const [i, journey] of journeys.entries()) {
|
|
1818
|
+
printMessage(`${customTrees[i] ? '*' : ''}${journey._id}`, 'data');
|
|
1819
|
+
}
|
|
1820
|
+
} else {
|
|
1821
|
+
const table = createTable([
|
|
1822
|
+
'Name'.brightCyan,
|
|
1823
|
+
'Status'.brightCyan,
|
|
1824
|
+
'Tags'.brightCyan,
|
|
1825
|
+
]);
|
|
1826
|
+
journeys.forEach((journey, i) => {
|
|
1827
|
+
table.push([
|
|
1828
|
+
`${customTrees[i] ? '*'.brightRed : ''}${journey._id}`,
|
|
1829
|
+
journey.enabled === false
|
|
1830
|
+
? 'disabled'.brightRed
|
|
1831
|
+
: 'enabled'.brightGreen,
|
|
1832
|
+
journey.uiConfig && journey.uiConfig.categories
|
|
1833
|
+
? wordwrap(JSON.parse(journey.uiConfig.categories).join(', '), 60)
|
|
1834
|
+
: '',
|
|
1835
|
+
]);
|
|
1836
|
+
});
|
|
1837
|
+
printMessage(table.toString(), 'data');
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Delete a journey
|
|
1843
|
+
* @param {String} journeyId journey id/name
|
|
1844
|
+
* @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info
|
|
1845
|
+
*/
|
|
1846
|
+
export async function deleteJourney(journeyId, options, spinner = true) {
|
|
1847
|
+
const { deep } = options;
|
|
1848
|
+
const { verbose } = options;
|
|
1849
|
+
const status = { nodes: {} };
|
|
1850
|
+
if (spinner) showSpinner(`Deleting ${journeyId}...`);
|
|
1851
|
+
if (spinner && verbose) stopSpinner();
|
|
1852
|
+
return deleteTree(journeyId)
|
|
1853
|
+
.then(async (deleteTreeResponse) => {
|
|
1854
|
+
status.status = 'success';
|
|
1855
|
+
const nodePromises = [];
|
|
1856
|
+
if (verbose) printMessage(`Deleted ${journeyId} (tree)`, 'info');
|
|
1857
|
+
if (deep) {
|
|
1858
|
+
for (const [nodeId, nodeObject] of Object.entries(
|
|
1859
|
+
deleteTreeResponse.data.nodes
|
|
1860
|
+
)) {
|
|
1861
|
+
// delete inner nodes (nodes inside container nodes)
|
|
1862
|
+
if (containerNodes.includes(nodeObject.nodeType)) {
|
|
1863
|
+
try {
|
|
1864
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1865
|
+
const pageNode = (await getNode(nodeId, nodeObject.nodeType))
|
|
1866
|
+
.data;
|
|
1867
|
+
if (verbose)
|
|
1868
|
+
printMessage(
|
|
1869
|
+
`Read ${nodeId} (${nodeObject.nodeType}) from ${journeyId}`,
|
|
1870
|
+
'info'
|
|
1871
|
+
);
|
|
1872
|
+
for (const innerNodeObject of pageNode.nodes) {
|
|
1873
|
+
nodePromises.push(
|
|
1874
|
+
deleteNode(innerNodeObject._id, innerNodeObject.nodeType)
|
|
1875
|
+
.then((response2) => {
|
|
1876
|
+
status.nodes[innerNodeObject._id] = { status: 'success' };
|
|
1877
|
+
if (verbose)
|
|
1878
|
+
printMessage(
|
|
1879
|
+
`Deleted ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}`,
|
|
1880
|
+
'info'
|
|
1881
|
+
);
|
|
1882
|
+
return response2.data;
|
|
1883
|
+
})
|
|
1884
|
+
.catch((error) => {
|
|
1885
|
+
status.nodes[innerNodeObject._id] = {
|
|
1886
|
+
status: 'error',
|
|
1887
|
+
error,
|
|
1888
|
+
};
|
|
1889
|
+
if (verbose)
|
|
1890
|
+
printMessage(
|
|
1891
|
+
`Error deleting inner node ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}: ${error}`,
|
|
1892
|
+
'error'
|
|
1893
|
+
);
|
|
1894
|
+
})
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
// finally delete the container node
|
|
1898
|
+
nodePromises.push(
|
|
1899
|
+
deleteNode(nodeId, nodeObject.nodeType)
|
|
1900
|
+
.then((response2) => {
|
|
1901
|
+
status.nodes[nodeId] = { status: 'success' };
|
|
1902
|
+
if (verbose)
|
|
1903
|
+
printMessage(
|
|
1904
|
+
`Deleted ${nodeId} (${nodeObject.nodeType}) from ${journeyId}`,
|
|
1905
|
+
'info'
|
|
1906
|
+
);
|
|
1907
|
+
return response2.data;
|
|
1908
|
+
})
|
|
1909
|
+
.catch((error) => {
|
|
1910
|
+
status.nodes[nodeId] = { status: 'error', error };
|
|
1911
|
+
if (verbose)
|
|
1912
|
+
printMessage(
|
|
1913
|
+
`Error deleting container node ${nodeId} (${nodeObject.nodeType}) from ${journeyId}: ${error}`,
|
|
1914
|
+
'error'
|
|
1915
|
+
);
|
|
1916
|
+
})
|
|
1917
|
+
);
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
if (verbose)
|
|
1920
|
+
printMessage(
|
|
1921
|
+
`Error getting container node ${nodeId} (${nodeObject.nodeType}) from ${journeyId}: ${error}`,
|
|
1922
|
+
'error'
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
} else {
|
|
1926
|
+
// delete the node
|
|
1927
|
+
nodePromises.push(
|
|
1928
|
+
deleteNode(nodeId, nodeObject.nodeType)
|
|
1929
|
+
.then((response) => {
|
|
1930
|
+
status.nodes[nodeId] = { status: 'success' };
|
|
1931
|
+
if (verbose)
|
|
1932
|
+
printMessage(
|
|
1933
|
+
`Deleted ${nodeId} (${nodeObject.nodeType}) from ${journeyId}`,
|
|
1934
|
+
'info'
|
|
1935
|
+
);
|
|
1936
|
+
return response.data;
|
|
1937
|
+
})
|
|
1938
|
+
.catch((error) => {
|
|
1939
|
+
status.nodes[nodeId] = { status: 'error', error };
|
|
1940
|
+
if (verbose)
|
|
1941
|
+
printMessage(
|
|
1942
|
+
`Error deleting node ${nodeId} (${nodeObject.nodeType}) from ${journeyId}: ${error}`,
|
|
1943
|
+
'error'
|
|
1944
|
+
);
|
|
1945
|
+
})
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
// wait until all the node calls are complete
|
|
1951
|
+
await Promise.allSettled(nodePromises);
|
|
1952
|
+
|
|
1953
|
+
// report status
|
|
1954
|
+
if (spinner) {
|
|
1955
|
+
let nodeCount = 0;
|
|
1956
|
+
let errorCount = 0;
|
|
1957
|
+
for (const node of Object.keys(status.nodes)) {
|
|
1958
|
+
nodeCount += 1;
|
|
1959
|
+
if (status.nodes[node].status === 'error') errorCount += 1;
|
|
1960
|
+
}
|
|
1961
|
+
if (errorCount === 0) {
|
|
1962
|
+
succeedSpinner(
|
|
1963
|
+
`Deleted ${journeyId} and ${
|
|
1964
|
+
nodeCount - errorCount
|
|
1965
|
+
}/${nodeCount} nodes.`
|
|
1966
|
+
);
|
|
1967
|
+
} else {
|
|
1968
|
+
failSpinner(
|
|
1969
|
+
`Deleted ${journeyId} and ${
|
|
1970
|
+
nodeCount - errorCount
|
|
1971
|
+
}/${nodeCount} nodes.`
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return status;
|
|
1976
|
+
})
|
|
1977
|
+
.catch((error) => {
|
|
1978
|
+
status.status = 'error';
|
|
1979
|
+
status.error = error;
|
|
1980
|
+
failSpinner(`Error deleting ${journeyId}.`);
|
|
1981
|
+
if (verbose)
|
|
1982
|
+
printMessage(`Error deleting tree ${journeyId}: ${error}`, 'error');
|
|
1983
|
+
return status;
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* Delete all journeys
|
|
1989
|
+
* @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info
|
|
1990
|
+
*/
|
|
1991
|
+
export async function deleteJourneys(options) {
|
|
1992
|
+
const { verbose } = options;
|
|
1993
|
+
const status = {};
|
|
1994
|
+
const trees = (await getTrees()).data.result;
|
|
1995
|
+
createProgressBar(trees.length, 'Deleting journeys...');
|
|
1996
|
+
for (const tree of trees) {
|
|
1997
|
+
if (verbose) printMessage('');
|
|
1998
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1999
|
+
status[tree._id] = await deleteJourney(tree._id, options, false);
|
|
2000
|
+
updateProgressBar(`${tree._id}`);
|
|
2001
|
+
// introduce a 100ms wait to allow the progress bar to update before the next verbose message prints from the async function
|
|
2002
|
+
if (verbose)
|
|
2003
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2004
|
+
await new Promise((r) => {
|
|
2005
|
+
setTimeout(r, 100);
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
let journeyCount = 0;
|
|
2009
|
+
let journeyErrorCount = 0;
|
|
2010
|
+
let nodeCount = 0;
|
|
2011
|
+
let nodeErrorCount = 0;
|
|
2012
|
+
for (const journey of Object.keys(status)) {
|
|
2013
|
+
journeyCount += 1;
|
|
2014
|
+
if (status[journey].status === 'error') journeyErrorCount += 1;
|
|
2015
|
+
for (const node of Object.keys(status[journey].nodes)) {
|
|
2016
|
+
nodeCount += 1;
|
|
2017
|
+
if (status[journey].nodes[node].status === 'error') nodeErrorCount += 1;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
stopProgressBar(
|
|
2021
|
+
`Deleted ${journeyCount - journeyErrorCount}/${journeyCount} journeys and ${
|
|
2022
|
+
nodeCount - nodeErrorCount
|
|
2023
|
+
}/${nodeCount} nodes.`
|
|
2024
|
+
);
|
|
2025
|
+
return status;
|
|
2026
|
+
}
|