@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.
Files changed (112) hide show
  1. package/.eslintrc +32 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +30 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. package/.github/README.md +121 -0
  5. package/.github/workflows/pipeline.yml +287 -0
  6. package/.prettierrc +6 -0
  7. package/CHANGELOG.md +512 -0
  8. package/CODE_OF_CONDUCT.md +128 -0
  9. package/LICENSE +21 -0
  10. package/README.md +8 -0
  11. package/docs/CONTRIBUTE.md +96 -0
  12. package/docs/PIPELINE.md +169 -0
  13. package/docs/images/npm_versioning_guidelines.png +0 -0
  14. package/docs/images/release_pipeline.png +0 -0
  15. package/jsconfig.json +6 -0
  16. package/package.json +95 -0
  17. package/resources/sampleEntitiesFile.json +8 -0
  18. package/resources/sampleEnvFile.env +2 -0
  19. package/src/api/AuthenticateApi.js +33 -0
  20. package/src/api/BaseApi.js +242 -0
  21. package/src/api/CirclesOfTrustApi.js +87 -0
  22. package/src/api/EmailTemplateApi.js +37 -0
  23. package/src/api/IdmConfigApi.js +88 -0
  24. package/src/api/LogApi.js +45 -0
  25. package/src/api/ManagedObjectApi.js +62 -0
  26. package/src/api/OAuth2ClientApi.js +69 -0
  27. package/src/api/OAuth2OIDCApi.js +73 -0
  28. package/src/api/OAuth2ProviderApi.js +32 -0
  29. package/src/api/RealmApi.js +99 -0
  30. package/src/api/Saml2Api.js +176 -0
  31. package/src/api/ScriptApi.js +84 -0
  32. package/src/api/SecretsApi.js +151 -0
  33. package/src/api/ServerInfoApi.js +41 -0
  34. package/src/api/SocialIdentityProvidersApi.js +114 -0
  35. package/src/api/StartupApi.js +45 -0
  36. package/src/api/ThemeApi.js +181 -0
  37. package/src/api/TreeApi.js +207 -0
  38. package/src/api/VariablesApi.js +104 -0
  39. package/src/api/utils/ApiUtils.js +77 -0
  40. package/src/api/utils/ApiUtils.test.js +96 -0
  41. package/src/api/utils/Base64.js +62 -0
  42. package/src/index.js +32 -0
  43. package/src/index.test.js +13 -0
  44. package/src/ops/AdminOps.js +901 -0
  45. package/src/ops/AuthenticateOps.js +342 -0
  46. package/src/ops/CirclesOfTrustOps.js +350 -0
  47. package/src/ops/ConnectionProfileOps.js +254 -0
  48. package/src/ops/EmailTemplateOps.js +326 -0
  49. package/src/ops/IdmOps.js +227 -0
  50. package/src/ops/IdpOps.js +342 -0
  51. package/src/ops/JourneyOps.js +2026 -0
  52. package/src/ops/LogOps.js +357 -0
  53. package/src/ops/ManagedObjectOps.js +34 -0
  54. package/src/ops/OAuth2ClientOps.js +151 -0
  55. package/src/ops/OrganizationOps.js +85 -0
  56. package/src/ops/RealmOps.js +139 -0
  57. package/src/ops/SamlOps.js +541 -0
  58. package/src/ops/ScriptOps.js +211 -0
  59. package/src/ops/SecretsOps.js +288 -0
  60. package/src/ops/StartupOps.js +114 -0
  61. package/src/ops/ThemeOps.js +379 -0
  62. package/src/ops/VariablesOps.js +185 -0
  63. package/src/ops/templates/OAuth2ClientTemplate.json +270 -0
  64. package/src/ops/templates/OrgModelUserAttributesTemplate.json +149 -0
  65. package/src/ops/templates/cloud/GenericExtensionAttributesTemplate.json +392 -0
  66. package/src/ops/templates/cloud/managed.json +4119 -0
  67. package/src/ops/utils/Console.js +434 -0
  68. package/src/ops/utils/DataProtection.js +92 -0
  69. package/src/ops/utils/DataProtection.test.js +28 -0
  70. package/src/ops/utils/ExportImportUtils.js +146 -0
  71. package/src/ops/utils/ExportImportUtils.test.js +119 -0
  72. package/src/ops/utils/OpsUtils.js +76 -0
  73. package/src/ops/utils/Wordwrap.js +11 -0
  74. package/src/storage/SessionStorage.js +45 -0
  75. package/src/storage/StaticStorage.js +15 -0
  76. package/test/e2e/journey/baseline/ForgottenUsername.journey.json +216 -0
  77. package/test/e2e/journey/baseline/Login.journey.json +205 -0
  78. package/test/e2e/journey/baseline/PasswordGrant.journey.json +139 -0
  79. package/test/e2e/journey/baseline/ProgressiveProfile.journey.json +198 -0
  80. package/test/e2e/journey/baseline/Registration.journey.json +249 -0
  81. package/test/e2e/journey/baseline/ResetPassword.journey.json +268 -0
  82. package/test/e2e/journey/baseline/UpdatePassword.journey.json +323 -0
  83. package/test/e2e/journey/baseline/allAlphaJourneys.journeys.json +1520 -0
  84. package/test/e2e/journey/delete/ForgottenUsername.journey.json +216 -0
  85. package/test/e2e/journey/delete/Login.journey.json +205 -0
  86. package/test/e2e/journey/delete/PasswordGrant.journey.json +139 -0
  87. package/test/e2e/journey/delete/ProgressiveProfile.journey.json +198 -0
  88. package/test/e2e/journey/delete/Registration.journey.json +249 -0
  89. package/test/e2e/journey/delete/ResetPassword.journey.json +268 -0
  90. package/test/e2e/journey/delete/UpdatePassword.journey.json +323 -0
  91. package/test/e2e/journey/delete/deleteMe.journey.json +230 -0
  92. package/test/e2e/journey/list/Disabled.journey.json +43 -0
  93. package/test/e2e/journey/list/ForgottenUsername.journey.json +216 -0
  94. package/test/e2e/journey/list/Login.journey.json +205 -0
  95. package/test/e2e/journey/list/PasswordGrant.journey.json +139 -0
  96. package/test/e2e/journey/list/ProgressiveProfile.journey.json +198 -0
  97. package/test/e2e/journey/list/Registration.journey.json +249 -0
  98. package/test/e2e/journey/list/ResetPassword.journey.json +268 -0
  99. package/test/e2e/journey/list/UpdatePassword.journey.json +323 -0
  100. package/test/e2e/setup.js +107 -0
  101. package/test/e2e/theme/baseline/Contrast.theme.json +95 -0
  102. package/test/e2e/theme/baseline/Highlander.theme.json +95 -0
  103. package/test/e2e/theme/baseline/Robroy.theme.json +95 -0
  104. package/test/e2e/theme/baseline/Starter-Theme.theme.json +94 -0
  105. package/test/e2e/theme/baseline/Zardoz.theme.json +95 -0
  106. package/test/e2e/theme/import/Contrast.theme.json +95 -0
  107. package/test/e2e/theme/import/Highlander.theme.json +95 -0
  108. package/test/e2e/theme/import/Robroy.theme.json +95 -0
  109. package/test/e2e/theme/import/Starter-Theme.theme.json +94 -0
  110. package/test/e2e/theme/import/Zardoz.default.theme.json +95 -0
  111. package/test/fs_tmp/.gitkeep +2 -0
  112. package/test/global/setup.js +65 -0
@@ -0,0 +1,342 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import url from 'url';
3
+ import { createHash, randomBytes } from 'crypto';
4
+ import readlineSync from 'readline-sync';
5
+ import { encodeBase64Url } from '../api/utils/Base64.js';
6
+ import storage from '../storage/SessionStorage.js';
7
+ import * as global from '../storage/StaticStorage.js';
8
+ import { printMessage } from './utils/Console.js';
9
+ import { getServerInfo, getServerVersionInfo } from '../api/ServerInfoApi.js';
10
+ import { step } from '../api/AuthenticateApi.js';
11
+ import { accessToken, authorize } from '../api/OAuth2OIDCApi.js';
12
+ import {
13
+ getConnectionProfile,
14
+ saveConnectionProfile,
15
+ } from './ConnectionProfileOps.js';
16
+
17
+ const adminClientPassword = 'doesnotmatter';
18
+ const redirectUrlTemplate = '/platform/appAuthHelperRedirect.html';
19
+
20
+ const idmAdminScope = 'fr:idm:* openid';
21
+
22
+ let adminClientId = 'idmAdminClient';
23
+
24
+ /**
25
+ * Helper function to get cookie name
26
+ * @returns {String} cookie name
27
+ */
28
+ async function getCookieName() {
29
+ try {
30
+ return (await getServerInfo()).data.cookieName;
31
+ } catch (error) {
32
+ printMessage(`Error getting cookie name: ${error}`, 'error');
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Helper function to determine if this is a setup mfa prompt in the ID Cloud tenant admin login journey
39
+ * @param {Object} payload response from the previous authentication journey step
40
+ * @returns {Object} an object indicating if 2fa is required and the original payload
41
+ */
42
+ function checkAndHandle2FA(payload) {
43
+ // let skippable = false;
44
+ if ('callbacks' in payload) {
45
+ for (const element of payload.callbacks) {
46
+ if (element.type === 'HiddenValueCallback') {
47
+ if (element.input[0].value.includes('skip')) {
48
+ // skippable = true;
49
+ element.input[0].value = 'Skip';
50
+ return {
51
+ need2fa: true,
52
+ payload,
53
+ };
54
+ }
55
+ }
56
+ if (element.type === 'NameCallback') {
57
+ if (element.output[0].value.includes('code')) {
58
+ // skippable = false;
59
+ printMessage('2FA is enabled and required for this user...');
60
+ const code = readlineSync.question(`${element.output[0].value}: `);
61
+ element.input[0].value = code;
62
+ return {
63
+ need2fa: true,
64
+ payload,
65
+ };
66
+ }
67
+ }
68
+ }
69
+ // console.info("NO2FA");
70
+ return {
71
+ need2fa: false,
72
+ payload,
73
+ };
74
+ }
75
+ // console.info("NO2FA");
76
+ return {
77
+ need2fa: false,
78
+ payload,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Helper function to set the default realm by deployment type
84
+ * @param {String} deploymentType deployment type
85
+ */
86
+ function determineDefaultRealm(deploymentType) {
87
+ if (storage.session.getRealm() === global.DEFAULT_REALM_KEY) {
88
+ storage.session.setRealm(global.DEPLOYMENT_TYPE_REALM_MAP[deploymentType]);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Helper function to determine the deployment type
94
+ * @returns {String} deployment type
95
+ */
96
+ async function determineDeploymentType() {
97
+ const fidcClientId = 'idmAdminClient';
98
+ const forgeopsClientId = 'idm-admin-ui';
99
+ let response = {};
100
+
101
+ const verifier = encodeBase64Url(randomBytes(32));
102
+ const challenge = encodeBase64Url(
103
+ createHash('sha256').update(verifier).digest()
104
+ );
105
+ const challengeMethod = 'S256';
106
+ const redirectURL = url.resolve(
107
+ storage.session.getTenant(),
108
+ redirectUrlTemplate
109
+ );
110
+
111
+ const config = {
112
+ maxRedirects: 0,
113
+ };
114
+ let bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScope}&response_type=code&client_id=${fidcClientId}&csrf=${storage.session.getCookieValue()}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`;
115
+
116
+ let deploymentType = global.CLASSIC_DEPLOYMENT_TYPE_KEY;
117
+ try {
118
+ response = await authorize(bodyFormData, config);
119
+ } catch (e) {
120
+ if (e.response && e.response.status === 302) {
121
+ printMessage('ForgeRock Identity Cloud ', 'info', false);
122
+ deploymentType = global.CLOUD_DEPLOYMENT_TYPE_KEY;
123
+ } else {
124
+ try {
125
+ bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScope}&response_type=code&client_id=${forgeopsClientId}&csrf=${storage.session.getCookieValue()}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`;
126
+ // eslint-disable-next-line no-unused-vars
127
+ response = await authorize(bodyFormData, config);
128
+ } catch (ex) {
129
+ if (ex.response.status === 302) {
130
+ adminClientId = forgeopsClientId;
131
+ printMessage('ForgeOps deployment ', 'info', false);
132
+ deploymentType = global.FORGEOPS_DEPLOYMENT_TYPE_KEY;
133
+ } else {
134
+ printMessage('Classic deployment ', 'info', false);
135
+ }
136
+ }
137
+ }
138
+ printMessage('detected.');
139
+ }
140
+ determineDefaultRealm(deploymentType);
141
+ return deploymentType;
142
+ }
143
+
144
+ /**
145
+ * Helper function to extract the semantic version string from a version info object
146
+ * @param {Object} versionInfo version info object
147
+ * @returns {String} semantic version
148
+ */
149
+ async function getSemanticVersion(versionInfo) {
150
+ if ('version' in versionInfo) {
151
+ const versionString = versionInfo.version;
152
+ const rx = /([\d]\.[\d]\.[\d](\.[\d])*)/g;
153
+ const version = versionString.match(rx);
154
+ return version[0];
155
+ }
156
+ throw new Error('Cannot extract semantic version from version info object.');
157
+ }
158
+
159
+ /**
160
+ * Helper function to authenticate and obtain and store session cookie
161
+ * @returns {String} empty string or null
162
+ */
163
+ async function authenticate() {
164
+ storage.session.setCookieName(await getCookieName());
165
+ try {
166
+ const config = {
167
+ headers: {
168
+ 'X-OpenAM-Username': storage.session.getUsername(),
169
+ 'X-OpenAM-Password': storage.session.getPassword(),
170
+ },
171
+ };
172
+ const response1 = (await step({}, config)).data;
173
+ const skip2FA = checkAndHandle2FA(response1);
174
+ let response2 = {};
175
+ if (skip2FA.need2fa) {
176
+ response2 = (await step(skip2FA.payload)).data;
177
+ } else {
178
+ response2 = skip2FA.payload;
179
+ }
180
+ if ('tokenId' in response2) {
181
+ storage.session.setCookieValue(response2.tokenId);
182
+ if (!storage.session.getDeploymentType()) {
183
+ storage.session.setDeploymentType(await determineDeploymentType());
184
+ } else {
185
+ determineDefaultRealm(storage.session.getDeploymentType());
186
+ }
187
+ const versionInfo = (await getServerVersionInfo()).data;
188
+ printMessage(`Connected to ${versionInfo.fullVersion}`);
189
+ const version = getSemanticVersion(versionInfo);
190
+ storage.session.setAmVersion(version);
191
+ return '';
192
+ }
193
+ printMessage(`error authenticating`, 'error');
194
+ printMessage('+++ likely cause, bad credentials!!! +++', 'error');
195
+ return null;
196
+ } catch (e) {
197
+ if (e.response && e.response.status === 401) {
198
+ printMessage(`error authenticating - ${e.message}`, 'error');
199
+ printMessage('+++ likely cause, bad credentials +++', 'error');
200
+ }
201
+ if (e.message && e.message === 'self signed certificate') {
202
+ printMessage(`error authenticating - ${e.message}`, 'error');
203
+ printMessage('+++ use -k, --insecure option to allow +++', 'error');
204
+ } else {
205
+ printMessage(`error authenticating - ${e.message}`, 'error');
206
+ }
207
+ return null;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Helper function to obtain an oauth2 authorization code
213
+ * @param {String} redirectURL oauth2 redirect uri
214
+ * @param {String} codeChallenge PKCE code challenge
215
+ * @param {String} codeChallengeMethod PKCE code challenge method
216
+ * @returns {String} oauth2 authorization code or null
217
+ */
218
+ async function getAuthCode(redirectURL, codeChallenge, codeChallengeMethod) {
219
+ try {
220
+ const bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScope}&response_type=code&client_id=${adminClientId}&csrf=${storage.session.getCookieValue()}&decision=allow&code_challenge=${codeChallenge}&code_challenge_method=${codeChallengeMethod}`;
221
+ const config = {
222
+ headers: {
223
+ 'Content-Type': 'application/x-www-form-urlencoded',
224
+ },
225
+ };
226
+ const response = await authorize(bodyFormData, config);
227
+ if (response.status < 200 || response.status > 399) {
228
+ printMessage('error getting auth code', 'error');
229
+ printMessage(
230
+ 'likely cause: mismatched parameters with OAuth client config',
231
+ 'error'
232
+ );
233
+ return null;
234
+ }
235
+ const redirectLocationURL = response.request.res.responseUrl;
236
+ const queryObject = url.parse(redirectLocationURL, true).query;
237
+ if ('code' in queryObject) {
238
+ return queryObject.code;
239
+ }
240
+ printMessage('auth code not found', 'error');
241
+ return null;
242
+ } catch (error) {
243
+ printMessage(`error getting auth code - ${error.message}`, 'error');
244
+ printMessage(error.response.data, 'error');
245
+ return null;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Helper function to obtain oauth2 access token
251
+ * @returns {String} empty string or null
252
+ */
253
+ async function getAccessToken() {
254
+ try {
255
+ const verifier = encodeBase64Url(randomBytes(32));
256
+ const challenge = encodeBase64Url(
257
+ createHash('sha256').update(verifier).digest()
258
+ );
259
+ const challengeMethod = 'S256';
260
+ const redirectURL = url.resolve(
261
+ storage.session.getTenant(),
262
+ redirectUrlTemplate
263
+ );
264
+ const authCode = await getAuthCode(redirectURL, challenge, challengeMethod);
265
+ if (authCode == null) {
266
+ printMessage('error getting auth code', 'error');
267
+ return null;
268
+ }
269
+ let response = null;
270
+ if (
271
+ storage.session.getDeploymentType() === global.CLOUD_DEPLOYMENT_TYPE_KEY
272
+ ) {
273
+ const config = {
274
+ auth: {
275
+ username: adminClientId,
276
+ password: adminClientPassword,
277
+ },
278
+ };
279
+ const bodyFormData = `redirect_uri=${redirectURL}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`;
280
+ response = await accessToken(bodyFormData, config);
281
+ } else {
282
+ const bodyFormData = `client_id=${adminClientId}&redirect_uri=${redirectURL}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`;
283
+ response = await accessToken(bodyFormData);
284
+ }
285
+ if (response.status < 200 || response.status > 399) {
286
+ printMessage(`access token call returned ${response.status}`, 'error');
287
+ return null;
288
+ }
289
+ if ('access_token' in response.data) {
290
+ storage.session.setBearerToken(response.data.access_token);
291
+ return '';
292
+ }
293
+ printMessage("can't get access token", 'error');
294
+ return null;
295
+ } catch (e) {
296
+ printMessage('error getting access token - ', 'error');
297
+ return null;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Get tokens
303
+ * @param {boolean} save true to save a connection profile upon successful authentication, false otherwise
304
+ * @returns {boolean} true if tokens were successfully obtained, false otherwise
305
+ */
306
+ export async function getTokens(save = false) {
307
+ let credsFromParameters = true;
308
+ // if username/password on cli are empty, try to read from connections.json
309
+ if (
310
+ storage.session.getUsername() == null &&
311
+ storage.session.getPassword() == null
312
+ ) {
313
+ credsFromParameters = false;
314
+ const conn = await getConnectionProfile();
315
+ if (conn) {
316
+ storage.session.setTenant(conn.tenant);
317
+ storage.session.setUsername(conn.username);
318
+ storage.session.setPassword(conn.password);
319
+ } else {
320
+ return false;
321
+ }
322
+ }
323
+ await authenticate();
324
+ if (
325
+ storage.session.getCookieValue() &&
326
+ !storage.session.getBearerToken() &&
327
+ (storage.session.getDeploymentType() === global.CLOUD_DEPLOYMENT_TYPE_KEY ||
328
+ storage.session.getDeploymentType() ===
329
+ global.FORGEOPS_DEPLOYMENT_TYPE_KEY)
330
+ ) {
331
+ await getAccessToken();
332
+ }
333
+ if (save && storage.session.getCookieValue() && credsFromParameters) {
334
+ // valid cookie, which means valid username/password combo. Save it in connections.json
335
+ saveConnectionProfile();
336
+ return true;
337
+ }
338
+ if (!storage.session.getCookieValue()) {
339
+ return false;
340
+ }
341
+ return true;
342
+ }
@@ -0,0 +1,350 @@
1
+ import fs from 'fs';
2
+ import _ from 'lodash';
3
+ import {
4
+ createTable,
5
+ printMessage,
6
+ createProgressBar,
7
+ updateProgressBar,
8
+ stopProgressBar,
9
+ } from './utils/Console.js';
10
+ import {
11
+ getCirclesOfTrust,
12
+ getCircleOfTrust,
13
+ createCircleOfTrust,
14
+ } from '../api/CirclesOfTrustApi.js';
15
+ import {
16
+ getRealmString,
17
+ getTypedFilename,
18
+ saveJsonToFile,
19
+ validateImport,
20
+ } from './utils/ExportImportUtils.js';
21
+
22
+ // use a function vs a template variable to avoid problems in loops
23
+ function getFileDataTemplate() {
24
+ return {
25
+ meta: {},
26
+ script: {},
27
+ saml: {
28
+ hosted: {},
29
+ remote: {},
30
+ metadata: {},
31
+ cot: {},
32
+ },
33
+ };
34
+ }
35
+
36
+ /**
37
+ * List entity providers
38
+ * @param {String} long Long list format with details
39
+ */
40
+ export async function listCirclesOfTrust(long = false) {
41
+ let cotList = [];
42
+ try {
43
+ const response = await getCirclesOfTrust();
44
+ if (response.status < 200 || response.status > 399) {
45
+ printMessage(response, 'data');
46
+ printMessage(`getCirclesOfTrust: ${response.status}`, 'error');
47
+ }
48
+ cotList = response.data.result;
49
+ } catch (error) {
50
+ printMessage(`getCirclesOfTrust ERROR: ${error}`, 'error');
51
+ printMessage(error, 'data');
52
+ }
53
+ cotList.sort((a, b) => a._id.localeCompare(b._id));
54
+ if (!long) {
55
+ cotList.forEach((cot) => {
56
+ printMessage(`${cot._id}`, 'data');
57
+ });
58
+ } else {
59
+ const table = createTable([
60
+ 'Name'.brightCyan,
61
+ 'Description'.brightCyan,
62
+ 'Status'.brightCyan,
63
+ 'Trusted Providers'.brightCyan,
64
+ ]);
65
+ cotList.forEach((cot) => {
66
+ table.push([
67
+ cot._id,
68
+ cot.description,
69
+ cot.status,
70
+ cot.trustedProviders
71
+ .map((provider) => provider.split('|')[0])
72
+ .join('\n'),
73
+ ]);
74
+ });
75
+ printMessage(table.toString());
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Include dependencies in the export file
81
+ * @param {Object} cotData Object representing a SAML circle of trust
82
+ * @param {Object} fileData File data object to add dependencies to
83
+ */
84
+ async function exportDependencies(cotData, fileData) {
85
+ // TODO: Export dependencies
86
+ return [cotData, fileData];
87
+ }
88
+
89
+ /**
90
+ * Export a single circle of trust to file
91
+ * @param {String} cotId circle of trust id/name
92
+ * @param {String} file Optional filename
93
+ */
94
+ export async function exportCircleOfTrust(cotId, file = null) {
95
+ let fileName = file;
96
+ if (!fileName) {
97
+ fileName = getTypedFilename(cotId, 'cot.saml');
98
+ }
99
+ createProgressBar(1, `Exporting circle of trust ${cotId}`);
100
+ getCircleOfTrust(cotId)
101
+ .then(async (response) => {
102
+ const cotData = _.cloneDeep(response.data);
103
+ delete cotData._rev;
104
+ updateProgressBar(`Exporting ${cotId}`);
105
+ const fileData = getFileDataTemplate();
106
+ fileData.saml.cot[cotId] = cotData;
107
+ await exportDependencies(cotData, fileData);
108
+ saveJsonToFile(fileData, fileName);
109
+ stopProgressBar(
110
+ `Exported ${cotId.brightCyan} to ${fileName.brightCyan}.`
111
+ );
112
+ })
113
+ .catch((err) => {
114
+ stopProgressBar(`${err}`);
115
+ printMessage(err, 'error');
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Export all circles of trust to one file
121
+ * @param {String} file Optional filename
122
+ */
123
+ export async function exportCirclesOfTrustToFile(file = null) {
124
+ let fileName = file;
125
+ if (!fileName) {
126
+ fileName = getTypedFilename(
127
+ `all${getRealmString()}CirclesOfTrust`,
128
+ 'cot.saml'
129
+ );
130
+ }
131
+ const fileData = getFileDataTemplate();
132
+ let allCotData = [];
133
+ try {
134
+ const response = await getCirclesOfTrust();
135
+ if (response.status < 200 || response.status > 399) {
136
+ printMessage(response, 'data');
137
+ printMessage(`getCirclesOfTrust: ${response.status}`, 'error');
138
+ }
139
+ allCotData = _.cloneDeep(response.data.result);
140
+ createProgressBar(allCotData.length, 'Exporting circles of trust');
141
+ for (const cotData of allCotData) {
142
+ delete cotData._rev;
143
+ updateProgressBar(`Exporting circle of trust ${cotData._id}`);
144
+ // eslint-disable-next-line no-await-in-loop
145
+ await exportDependencies(cotData, fileData);
146
+ fileData.saml.cot[cotData._id] = cotData;
147
+ }
148
+ saveJsonToFile(fileData, fileName);
149
+ stopProgressBar(
150
+ `${allCotData.length} circle(s) of trust exported to ${fileName}.`
151
+ );
152
+ } catch (error) {
153
+ printMessage(`getCirclesOfTrust ERROR: ${error}`, 'error');
154
+ printMessage(error, 'data');
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Export all circles of trust to individual files
160
+ */
161
+ export async function exportCirclesOfTrustToFiles() {
162
+ let allCotData = [];
163
+ try {
164
+ const response = await getCirclesOfTrust();
165
+ if (response.status < 200 || response.status > 399) {
166
+ printMessage(response, 'data');
167
+ printMessage(`getCirclesOfTrust: ${response.status}`, 'error');
168
+ }
169
+ allCotData = _.cloneDeep(response.data.result);
170
+ createProgressBar(allCotData.length, 'Exporting circles of trust');
171
+ for (const cotData of allCotData) {
172
+ delete cotData._rev;
173
+ updateProgressBar(`Exporting circle of trust ${cotData._id}`);
174
+ const fileName = getTypedFilename(cotData._id, 'cot.saml');
175
+ const fileData = getFileDataTemplate();
176
+ // eslint-disable-next-line no-await-in-loop
177
+ await exportDependencies(cotData, fileData);
178
+ fileData.saml.cot[cotData._id] = cotData;
179
+ saveJsonToFile(fileData, fileName);
180
+ }
181
+ stopProgressBar(`${allCotData.length} providers exported.`);
182
+ } catch (error) {
183
+ printMessage(`getCirclesOfTrust ERROR: ${error}`, 'error');
184
+ printMessage(error, 'data');
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Include dependencies from the import file
190
+ * @param {Object} cotData Object representing a SAML circle of trust
191
+ * @param {Object} fileData File data object to read dependencies from
192
+ */
193
+ async function importDependencies(cotData, fileData) {
194
+ // TODO: Import dependencies
195
+ return [cotData, fileData];
196
+ }
197
+
198
+ /**
199
+ * Import a SAML circle of trust by id/name from file
200
+ * @param {String} cotId Circle of trust id/name
201
+ * @param {String} file Import file name
202
+ */
203
+ export async function importCircleOfTrust(cotId, file) {
204
+ fs.readFile(file, 'utf8', async (err, data) => {
205
+ if (err) throw err;
206
+ const fileData = JSON.parse(data);
207
+ if (validateImport(fileData.meta)) {
208
+ createProgressBar(1, 'Importing circle of trust...');
209
+ const cotData = _.get(fileData, ['saml', 'cot', cotId]);
210
+ if (cotData) {
211
+ updateProgressBar(`Importing ${cotId}`);
212
+ await importDependencies(cotData, fileData);
213
+ createCircleOfTrust(cotData)
214
+ .then(() => {
215
+ stopProgressBar(`Successfully imported ${cotId}.`);
216
+ })
217
+ .catch((createProviderErr) => {
218
+ stopProgressBar(`Error importing ${cotId}.`);
219
+ printMessage(`Error importing ${cotId}`, 'error');
220
+ printMessage(createProviderErr.response.data, 'error');
221
+ });
222
+ } else {
223
+ stopProgressBar(
224
+ `Circle of trust ${cotId.brightCyan} not found in ${file.brightCyan}!`
225
+ );
226
+ }
227
+ } else {
228
+ printMessage('Import validation failed...', 'error');
229
+ }
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Import first SAML circle of trust from file
235
+ * @param {String} file Import file name
236
+ */
237
+ export async function importFirstCircleOfTrust(file) {
238
+ fs.readFile(file, 'utf8', async (err, data) => {
239
+ if (err) throw err;
240
+ const fileData = JSON.parse(data);
241
+ if (validateImport(fileData.meta)) {
242
+ createProgressBar(1, 'Importing circle of trust...');
243
+ for (const cotId in fileData.saml.cot) {
244
+ if ({}.hasOwnProperty.call(fileData.saml.cot, cotId)) {
245
+ const cotData = _.cloneDeep(fileData.saml.cot[cotId]);
246
+ updateProgressBar(`Importing ${cotId}`);
247
+ // eslint-disable-next-line no-await-in-loop
248
+ await importDependencies(cotData, fileData);
249
+ createCircleOfTrust(cotData)
250
+ .then(() => {
251
+ stopProgressBar(`Successfully imported ${cotId}.`);
252
+ })
253
+ .catch((createCircleOfTrustErr) => {
254
+ stopProgressBar(`Error importing ${cotId}.`);
255
+ printMessage(`Error importing ${cotId}`, 'error');
256
+ printMessage(createCircleOfTrustErr.response.data, 'error');
257
+ });
258
+ break;
259
+ }
260
+ }
261
+ } else {
262
+ printMessage('Import validation failed...', 'error');
263
+ }
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Import all SAML circles of trust from file
269
+ * @param {String} file Import file name
270
+ */
271
+ export async function importCirclesOfTrustFromFile(file) {
272
+ fs.readFile(file, 'utf8', async (err, data) => {
273
+ if (err) throw err;
274
+ const fileData = JSON.parse(data);
275
+ if (validateImport(fileData.meta)) {
276
+ createProgressBar(
277
+ Object.keys(fileData.saml.cot).length,
278
+ 'Importing circles of trust...'
279
+ );
280
+ for (const cotId in fileData.saml.cot) {
281
+ if ({}.hasOwnProperty.call(fileData.saml.cot, cotId)) {
282
+ const cotData = _.cloneDeep(fileData.saml.cot[cotId]);
283
+ // eslint-disable-next-line no-await-in-loop
284
+ await importDependencies(cotData, fileData);
285
+ try {
286
+ // eslint-disable-next-line no-await-in-loop
287
+ await createCircleOfTrust(cotData);
288
+ updateProgressBar(`Imported ${cotId}`);
289
+ } catch (createCircleOfTrustErr) {
290
+ printMessage(`\nError importing ${cotId}`, 'error');
291
+ printMessage(createCircleOfTrustErr.response.data, 'error');
292
+ }
293
+ }
294
+ }
295
+ stopProgressBar(`Circles of trust imported.`);
296
+ } else {
297
+ printMessage('Import validation failed...', 'error');
298
+ }
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Import all SAML circles of trust from all *.cot.saml.json files in the current directory
304
+ */
305
+ export async function importCirclesOfTrustFromFiles() {
306
+ const names = fs.readdirSync('.');
307
+ const jsonFiles = names.filter((name) =>
308
+ name.toLowerCase().endsWith('.cot.saml.json')
309
+ );
310
+ createProgressBar(jsonFiles.length, 'Importing circles or trust...');
311
+ let total = 0;
312
+ let totalErrors = 0;
313
+ for (const file of jsonFiles) {
314
+ const data = fs.readFileSync(file, 'utf8');
315
+ const fileData = JSON.parse(data);
316
+ if (validateImport(fileData.meta)) {
317
+ total += _.keys(fileData.saml.cot).length;
318
+ let errors = 0;
319
+ for (const cotId in fileData.saml.cot) {
320
+ if ({}.hasOwnProperty.call(fileData.saml.cot, cotId)) {
321
+ const cotData = _.cloneDeep(fileData.saml.cot[cotId]);
322
+ // eslint-disable-next-line no-await-in-loop
323
+ await importDependencies(cotData, fileData);
324
+ try {
325
+ // eslint-disable-next-line no-await-in-loop
326
+ await createCircleOfTrust(cotData);
327
+ // updateProgressBar(`Imported ${cotId}`);
328
+ } catch (createCircleOfTrustErr) {
329
+ errors += 1;
330
+ printMessage(`\nError importing ${cotId}`, 'error');
331
+ printMessage(createCircleOfTrustErr.response.data, 'error');
332
+ }
333
+ }
334
+ }
335
+ totalErrors += errors;
336
+ updateProgressBar(
337
+ `Imported ${
338
+ _.keys(fileData.saml.cot).length - errors
339
+ } circle(s) of trust from ${file}`
340
+ );
341
+ } else {
342
+ printMessage(`Validation of ${file} failed!`, 'error');
343
+ }
344
+ }
345
+ stopProgressBar(
346
+ `Imported ${total - totalErrors} of ${total} circle(s) of trust from ${
347
+ jsonFiles.length
348
+ } file(s).`
349
+ );
350
+ }