auth0-deploy-cli 8.43.0 → 8.44.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/CHANGELOG.md +30 -1
- package/lib/context/defaults.d.ts +7 -0
- package/lib/context/defaults.js +74 -30
- package/lib/context/directory/handlers/attackProtection.js +1 -1
- package/lib/context/directory/handlers/branding.js +9 -1
- package/lib/context/directory/handlers/clients.js +9 -3
- package/lib/context/directory/handlers/connections.js +10 -4
- package/lib/context/directory/handlers/emailTemplates.js +9 -0
- package/lib/context/directory/handlers/prompts.js +11 -3
- package/lib/context/yaml/handlers/attackProtection.js +1 -1
- package/lib/context/yaml/handlers/branding.js +8 -1
- package/lib/context/yaml/handlers/clients.js +4 -1
- package/lib/context/yaml/handlers/connections.js +3 -1
- package/lib/context/yaml/handlers/flows.js +7 -0
- package/lib/context/yaml/handlers/forms.js +7 -0
- package/lib/context/yaml/handlers/prompts.js +7 -1
- package/lib/readonly.js +5 -0
- package/lib/tools/auth0/handlers/attackProtection.js +3 -1
- package/lib/tools/auth0/handlers/clients.d.ts +21 -0
- package/lib/tools/auth0/handlers/clients.js +21 -0
- package/lib/tools/auth0/handlers/connectionProfiles.d.ts +27 -0
- package/lib/tools/auth0/handlers/connectionProfiles.js +30 -0
- package/lib/tools/auth0/handlers/connections.js +10 -3
- package/lib/tools/auth0/handlers/databases.js +1 -1
- package/lib/tools/auth0/handlers/default.d.ts +23 -0
- package/lib/tools/auth0/handlers/default.js +21 -10
- package/lib/tools/auth0/handlers/organizations.d.ts +1 -1
- package/lib/tools/auth0/handlers/organizations.js +39 -16
- package/lib/tools/auth0/handlers/phoneProvider.js +10 -2
- package/lib/tools/calculateDryRunChanges.js +12 -8
- package/lib/utils.js +5 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [8.44.0] - 2026-08-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Support `third_party_client_access` and `cross_app_access_resource_app` on clients and connections. [#1471]
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Warn on path traversal in file references and fix double-path resolution in YAML connections handler. [#1477]
|
|
19
|
+
- Strip `form_submission_mode` and `form_submission_behavior` from `bruteForceProtection` on export and import. [#1476]
|
|
20
|
+
- Include empty credentials for custom phone provider on import and export to prevent 400 errors. [#1474]
|
|
21
|
+
- Preserve keyword placeholder strings for all email provider credentials. [#1470]
|
|
22
|
+
- Preserve `idpinitiated.enabled:false` on SAML connection export instead of emitting `undefined_clientId`. [#1465]
|
|
23
|
+
- Apply 429 backoff to organization connection writes to prevent rate-limit failures. [#1464]
|
|
24
|
+
- Resolve dry-run reporting bugs across multiple asset types. [#1462]
|
|
25
|
+
- Skip organization PATCH when no top-level properties have changed. [#1461]
|
|
26
|
+
- Sort `attackProtection` arrays on export to prevent spurious diffs. [#1460]
|
|
27
|
+
|
|
10
28
|
## [8.43.0] - 2026-08-12
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -1927,7 +1945,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1927
1945
|
[#1454]: https://github.com/auth0/auth0-deploy-cli/issues/1454
|
|
1928
1946
|
[#1456]: https://github.com/auth0/auth0-deploy-cli/issues/1456
|
|
1929
1947
|
[#1457]: https://github.com/auth0/auth0-deploy-cli/issues/1457
|
|
1930
|
-
[
|
|
1948
|
+
[#1460]: https://github.com/auth0/auth0-deploy-cli/issues/1460
|
|
1949
|
+
[#1461]: https://github.com/auth0/auth0-deploy-cli/issues/1461
|
|
1950
|
+
[#1462]: https://github.com/auth0/auth0-deploy-cli/issues/1462
|
|
1951
|
+
[#1464]: https://github.com/auth0/auth0-deploy-cli/issues/1464
|
|
1952
|
+
[#1465]: https://github.com/auth0/auth0-deploy-cli/issues/1465
|
|
1953
|
+
[#1470]: https://github.com/auth0/auth0-deploy-cli/issues/1470
|
|
1954
|
+
[#1471]: https://github.com/auth0/auth0-deploy-cli/issues/1471
|
|
1955
|
+
[#1474]: https://github.com/auth0/auth0-deploy-cli/issues/1474
|
|
1956
|
+
[#1476]: https://github.com/auth0/auth0-deploy-cli/issues/1476
|
|
1957
|
+
[#1477]: https://github.com/auth0/auth0-deploy-cli/issues/1477
|
|
1958
|
+
[Unreleased]: https://github.com/auth0/auth0-deploy-cli/compare/v8.44.0...HEAD
|
|
1959
|
+
[8.44.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.43.0...v8.44.0
|
|
1931
1960
|
[8.43.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.42.0...v8.43.0
|
|
1932
1961
|
[8.42.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.41.0...v8.42.0
|
|
1933
1962
|
[8.41.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.40.0...v8.41.0
|
|
@@ -6,4 +6,11 @@ export declare function phoneTemplatesDefaults(phoneTemplate: any): any;
|
|
|
6
6
|
export declare function connectionDefaults(connection: any, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any;
|
|
7
7
|
export declare function logStreamDefaults(logStreams: any, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any;
|
|
8
8
|
export declare function eventStreamDefaults(eventStreams: any[], config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any[];
|
|
9
|
+
/**
|
|
10
|
+
* Sorts primitive arrays (e.g. `shields`, `admin_notification_frequency`) within the
|
|
11
|
+
* attackProtection subtree in place, so repeated exports produce deterministic output.
|
|
12
|
+
* These are unordered, string-typed enum sets — ordering is cosmetic. Object arrays are
|
|
13
|
+
* left as-is.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sortAttackProtectionArrays(attackProtection: AttackProtection): AttackProtection;
|
|
9
16
|
export declare function attackProtectionDefaults(attackProtection: AttackProtection, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): AttackProtection;
|
package/lib/context/defaults.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.phoneTemplatesDefaults = phoneTemplatesDefaults;
|
|
|
6
6
|
exports.connectionDefaults = connectionDefaults;
|
|
7
7
|
exports.logStreamDefaults = logStreamDefaults;
|
|
8
8
|
exports.eventStreamDefaults = eventStreamDefaults;
|
|
9
|
+
exports.sortAttackProtectionArrays = sortAttackProtectionArrays;
|
|
9
10
|
exports.attackProtectionDefaults = attackProtectionDefaults;
|
|
10
11
|
const utils_1 = require("../tools/utils");
|
|
11
12
|
// env vars arrive as strings, so check both forms
|
|
@@ -22,45 +23,57 @@ function emailProviderDefaults(emailProvider, config) {
|
|
|
22
23
|
// Add placeholder for credentials as they cannot be exported
|
|
23
24
|
const { name } = updated;
|
|
24
25
|
if (apiKeyProviders.includes(name)) {
|
|
25
|
-
updated.credentials
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (typeof updated.credentials !== 'string') {
|
|
27
|
+
updated.credentials = {
|
|
28
|
+
api_key: `##${name.toUpperCase()}_API_KEY##`,
|
|
29
|
+
...(updated.credentials || {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
29
32
|
}
|
|
30
33
|
if (name === 'smtp') {
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
// If credentials is a keyword placeholder string (e.g. @@SMTP_CREDENTIALS@@), preserve it as-is.
|
|
35
|
+
// The `in` operator requires an object and would throw a TypeError on a string.
|
|
36
|
+
if (typeof updated.credentials !== 'string') {
|
|
37
|
+
// This is to mask smtp_user to '##SMTP_USER##'
|
|
38
|
+
if (updated.credentials && 'smtp_user' in updated.credentials) {
|
|
39
|
+
delete updated.credentials.smtp_user;
|
|
40
|
+
}
|
|
41
|
+
updated.credentials = {
|
|
42
|
+
smtp_host: '##SMTP_HOSTNAME##',
|
|
43
|
+
smtp_port: '##SMTP_PORT##',
|
|
44
|
+
smtp_user: '##SMTP_USER##',
|
|
45
|
+
smtp_pass: '##SMTP_PASS##',
|
|
46
|
+
...(updated.credentials || {}),
|
|
47
|
+
};
|
|
34
48
|
}
|
|
35
|
-
updated.credentials = {
|
|
36
|
-
smtp_host: '##SMTP_HOSTNAME##',
|
|
37
|
-
smtp_port: '##SMTP_PORT##',
|
|
38
|
-
smtp_user: '##SMTP_USER##',
|
|
39
|
-
smtp_pass: '##SMTP_PASS##',
|
|
40
|
-
...(updated.credentials || {}),
|
|
41
|
-
};
|
|
42
49
|
}
|
|
43
50
|
if (name === 'ses') {
|
|
44
|
-
updated.credentials
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
if (typeof updated.credentials !== 'string') {
|
|
52
|
+
updated.credentials = {
|
|
53
|
+
accessKeyId: '##SES_ACCESS_KEY_ID##',
|
|
54
|
+
secretAccessKey: '##SES_ACCESS_SECRET_KEY##',
|
|
55
|
+
region: '##SES_AWS_REGION##',
|
|
56
|
+
...(updated.credentials || {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
50
59
|
}
|
|
51
60
|
if (name === 'azure_cs') {
|
|
52
|
-
updated.credentials
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
if (typeof updated.credentials !== 'string') {
|
|
62
|
+
updated.credentials = {
|
|
63
|
+
connectionString: '##AZURE_CS_CONNECTION_KEY##',
|
|
64
|
+
...(updated.credentials || {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
56
67
|
}
|
|
57
68
|
if (name === 'ms365') {
|
|
58
|
-
updated.credentials
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
if (typeof updated.credentials !== 'string') {
|
|
70
|
+
updated.credentials = {
|
|
71
|
+
tenantId: '##MS365_TENANT_ID##',
|
|
72
|
+
clientId: '##MS365_CLIENT_ID##',
|
|
73
|
+
clientSecret: '##MS365_CLIENT_SECRET##',
|
|
74
|
+
...(updated.credentials || {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
return updated;
|
|
66
79
|
}
|
|
@@ -87,6 +100,12 @@ function phoneProviderDefaults(phoneProvider) {
|
|
|
87
100
|
auth_token: `##${name.toUpperCase()}_AUTH_TOKEN##`,
|
|
88
101
|
};
|
|
89
102
|
}
|
|
103
|
+
else if (name === 'custom') {
|
|
104
|
+
// The `custom` provider has no exportable secrets, but the API requires a
|
|
105
|
+
// `credentials` object on import even when empty. Emit an empty object so
|
|
106
|
+
// exported configs round-trip cleanly.
|
|
107
|
+
updated.credentials = {};
|
|
108
|
+
}
|
|
90
109
|
return updated;
|
|
91
110
|
}
|
|
92
111
|
function phoneTemplatesDefaults(phoneTemplate) {
|
|
@@ -193,6 +212,31 @@ function eventStreamDefaults(eventStreams, config) {
|
|
|
193
212
|
};
|
|
194
213
|
});
|
|
195
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Sorts primitive arrays (e.g. `shields`, `admin_notification_frequency`) within the
|
|
217
|
+
* attackProtection subtree in place, so repeated exports produce deterministic output.
|
|
218
|
+
* These are unordered, string-typed enum sets — ordering is cosmetic. Object arrays are
|
|
219
|
+
* left as-is.
|
|
220
|
+
*/
|
|
221
|
+
function sortAttackProtectionArrays(attackProtection) {
|
|
222
|
+
const sortArraysDeep = (value) => {
|
|
223
|
+
if (Array.isArray(value)) {
|
|
224
|
+
const isPrimitiveArray = value.every((item) => item === null || typeof item !== 'object');
|
|
225
|
+
if (isPrimitiveArray) {
|
|
226
|
+
value.sort();
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
value.forEach(sortArraysDeep);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (value && typeof value === 'object') {
|
|
234
|
+
Object.values(value).forEach(sortArraysDeep);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
sortArraysDeep(attackProtection);
|
|
238
|
+
return attackProtection;
|
|
239
|
+
}
|
|
196
240
|
function attackProtectionDefaults(attackProtection, config) {
|
|
197
241
|
if (isExportSecrets(config))
|
|
198
242
|
return attackProtection;
|
|
@@ -65,7 +65,7 @@ async function dump(context) {
|
|
|
65
65
|
return;
|
|
66
66
|
const files = attackProtectionFiles(context.filePath);
|
|
67
67
|
fs_extra_1.default.ensureDirSync(files.directory);
|
|
68
|
-
const maskedAttackProtection = (0, defaults_1.attackProtectionDefaults)(attackProtection, context.config);
|
|
68
|
+
const maskedAttackProtection = (0, defaults_1.sortAttackProtectionArrays)((0, defaults_1.attackProtectionDefaults)(attackProtection, context.config));
|
|
69
69
|
if (maskedAttackProtection.botDetection) {
|
|
70
70
|
(0, utils_1.dumpJSON)(files.botDetection, maskedAttackProtection.botDetection);
|
|
71
71
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const path_1 = __importDefault(require("path"));
|
|
7
7
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
8
|
const tools_1 = require("../../../tools");
|
|
9
|
+
const logger_1 = __importDefault(require("../../../logger"));
|
|
9
10
|
const utils_1 = require("../../../utils");
|
|
10
11
|
function parse(context) {
|
|
11
12
|
const brandingDirectory = path_1.default.join(context.filePath, tools_1.constants.BRANDING_DIRECTORY);
|
|
@@ -31,7 +32,14 @@ function parse(context) {
|
|
|
31
32
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
32
33
|
});
|
|
33
34
|
const normalizedPathArray = (0, utils_1.nomalizedYAMLPath)(definition.body);
|
|
34
|
-
|
|
35
|
+
const resolvedBodyFile = path_1.default.resolve(brandingTemplatesFolder, ...normalizedPathArray);
|
|
36
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
37
|
+
if (!resolvedBodyFile.startsWith(configRoot + path_1.default.sep)) {
|
|
38
|
+
logger_1.default.warn(`Branding template body path "${definition.body}" resolves to "${resolvedBodyFile}" which is outside the config directory "${configRoot}". ` +
|
|
39
|
+
`This will be blocked as an error in the next major release. ` +
|
|
40
|
+
`Move the file inside your config directory.`);
|
|
41
|
+
}
|
|
42
|
+
definition.body = (0, tools_1.loadFileAndReplaceKeywords)(resolvedBodyFile, {
|
|
35
43
|
mappings: context.mappings,
|
|
36
44
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
37
45
|
});
|
|
@@ -20,9 +20,15 @@ function parse(context) {
|
|
|
20
20
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
21
21
|
});
|
|
22
22
|
if (client.custom_login_page) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
24
|
+
const resolvedLoginPage = path_1.default.resolve(clientsFolder, client.custom_login_page);
|
|
25
|
+
if ((0, utils_1.isFile)(resolvedLoginPage)) {
|
|
26
|
+
if (!resolvedLoginPage.startsWith(configRoot + path_1.default.sep)) {
|
|
27
|
+
logger_1.default.warn(`Path "${client.custom_login_page}" resolves to "${resolvedLoginPage}" which is outside the config directory "${configRoot}". ` +
|
|
28
|
+
`This will be blocked as an error in the next major release. ` +
|
|
29
|
+
`Move the file inside your config directory.`);
|
|
30
|
+
}
|
|
31
|
+
client.custom_login_page = (0, tools_1.loadFileAndReplaceKeywords)(resolvedLoginPage, {
|
|
26
32
|
mappings: context.mappings,
|
|
27
33
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
28
34
|
});
|
|
@@ -23,11 +23,17 @@ function parse(context) {
|
|
|
23
23
|
});
|
|
24
24
|
if (connection.strategy === 'email') {
|
|
25
25
|
(0, utils_1.ensureProp)(connection, 'options.email.body');
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
27
|
+
const resolvedHtmlFile = path_1.default.resolve(connectionsFolder, connection.options.email.body);
|
|
28
|
+
if (!(0, utils_1.isFile)(resolvedHtmlFile)) {
|
|
29
|
+
throw new Error(`Passwordless email template purportedly located at ${resolvedHtmlFile} does not exist for connection. Ensure the existence of this file to proceed with deployment.`);
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
if (!resolvedHtmlFile.startsWith(configRoot + path_1.default.sep)) {
|
|
32
|
+
logger_1.default.warn(`Path "${connection.options.email.body}" resolves to "${resolvedHtmlFile}" which is outside the config directory "${configRoot}". ` +
|
|
33
|
+
`This will be blocked as an error in the next major release. ` +
|
|
34
|
+
`Move the file inside your config directory.`);
|
|
35
|
+
}
|
|
36
|
+
connection.options.email.body = (0, tools_1.loadFileAndReplaceKeywords)(resolvedHtmlFile, {
|
|
31
37
|
mappings: context.mappings,
|
|
32
38
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
33
39
|
});
|
|
@@ -34,6 +34,15 @@ function parse(context) {
|
|
|
34
34
|
logger_1.default.warn(`Skipping email template file ${meta.body} as missing the corresponding '.json' file`);
|
|
35
35
|
return [];
|
|
36
36
|
}
|
|
37
|
+
if (meta.body !== undefined) {
|
|
38
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
39
|
+
const resolvedTemplatePath = path_1.default.resolve(templateFilePath);
|
|
40
|
+
if (!resolvedTemplatePath.startsWith(configRoot + path_1.default.sep)) {
|
|
41
|
+
logger_1.default.warn(`Email template body path "${meta.body}" resolves to "${resolvedTemplatePath}" which is outside the config directory "${configRoot}". ` +
|
|
42
|
+
`This will be blocked as an error in the next major release. ` +
|
|
43
|
+
`Move the file inside your config directory.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
37
46
|
return {
|
|
38
47
|
...meta,
|
|
39
48
|
body: (0, tools_1.loadFileAndReplaceKeywords)(templateFilePath, {
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const path_1 = __importDefault(require("path"));
|
|
7
7
|
const fs_extra_1 = require("fs-extra");
|
|
8
8
|
const tools_1 = require("../../../tools");
|
|
9
|
+
const logger_1 = __importDefault(require("../../../logger"));
|
|
9
10
|
const utils_1 = require("../../../utils");
|
|
10
11
|
const getPromptsDirectory = (filePath) => path_1.default.join(filePath, tools_1.constants.PROMPTS_DIRECTORY);
|
|
11
12
|
const getPromptsSettingsFile = (promptsDirectory) => path_1.default.join(promptsDirectory, 'prompts.json');
|
|
@@ -46,9 +47,16 @@ function parse(context) {
|
|
|
46
47
|
const screensObject = screensArray[0];
|
|
47
48
|
acc[promptName] = Object.entries(screensObject).reduce((screenAcc, [screenName, items]) => {
|
|
48
49
|
screenAcc[screenName] = items.reduce((insertionAcc, { name, template }) => {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
51
|
+
const resolvedTemplatePath = path_1.default.resolve(promptsDirectory, template);
|
|
52
|
+
if ((0, utils_1.isFile)(resolvedTemplatePath) &&
|
|
53
|
+
!resolvedTemplatePath.startsWith(configRoot + path_1.default.sep)) {
|
|
54
|
+
logger_1.default.warn(`Prompt partial template "${template}" resolves to "${resolvedTemplatePath}" which is outside the config directory "${configRoot}". ` +
|
|
55
|
+
`This will be blocked as an error in the next major release. ` +
|
|
56
|
+
`Move the file inside your config directory.`);
|
|
57
|
+
}
|
|
58
|
+
insertionAcc[name] = (0, utils_1.isFile)(resolvedTemplatePath)
|
|
59
|
+
? (0, tools_1.loadFileAndReplaceKeywords)(resolvedTemplatePath, {
|
|
52
60
|
mappings: context.mappings,
|
|
53
61
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
54
62
|
}).trim()
|
|
@@ -27,7 +27,7 @@ async function dump(context) {
|
|
|
27
27
|
}
|
|
28
28
|
const maskedAttackProtection = (0, defaults_1.attackProtectionDefaults)(attackProtectionConfig, context.config);
|
|
29
29
|
return {
|
|
30
|
-
attackProtection: maskedAttackProtection,
|
|
30
|
+
attackProtection: (0, defaults_1.sortAttackProtectionArrays)(maskedAttackProtection),
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
const attackProtectionHandler = {
|
|
@@ -18,9 +18,16 @@ async function parse(context) {
|
|
|
18
18
|
const parsedTemplates = templates.map((templateDefinition) => {
|
|
19
19
|
const normalizedPathArray = (0, utils_1.nomalizedYAMLPath)(templateDefinition.body);
|
|
20
20
|
const markupFile = path_1.default.join(context.basePath, ...normalizedPathArray);
|
|
21
|
+
const configRoot = path_1.default.resolve(context.basePath);
|
|
22
|
+
const resolvedMarkupFile = path_1.default.resolve(markupFile);
|
|
23
|
+
if (!resolvedMarkupFile.startsWith(configRoot + path_1.default.sep)) {
|
|
24
|
+
logger_1.default.warn(`Branding template body path "${templateDefinition.body}" resolves to "${resolvedMarkupFile}" which is outside the config directory "${configRoot}". ` +
|
|
25
|
+
`This will be blocked as an error in the next major release. ` +
|
|
26
|
+
`Move the file inside your config directory.`);
|
|
27
|
+
}
|
|
21
28
|
return {
|
|
22
29
|
template: templateDefinition.template,
|
|
23
|
-
body: (0, tools_1.loadFileAndReplaceKeywords)(
|
|
30
|
+
body: (0, tools_1.loadFileAndReplaceKeywords)(resolvedMarkupFile, {
|
|
24
31
|
mappings: context.mappings,
|
|
25
32
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
26
33
|
}),
|
|
@@ -21,7 +21,10 @@ async function parse(context) {
|
|
|
21
21
|
if (client.custom_login_page) {
|
|
22
22
|
const htmlFileName = path_1.default.join(clientsFolder, client.custom_login_page);
|
|
23
23
|
if ((0, utils_1.isFile)(htmlFileName)) {
|
|
24
|
-
|
|
24
|
+
// Pass a path relative to basePath so context.loadFile() resolves it correctly.
|
|
25
|
+
// Passing htmlFileName (which already includes basePath) would cause double resolution
|
|
26
|
+
// when basePath is a relative path (e.g. when AUTH0_INPUT_FILE is a relative path).
|
|
27
|
+
client.custom_login_page = context.loadFile(path_1.default.join(tools_1.constants.CLIENTS_DIRECTORY, client.custom_login_page));
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
return client;
|
|
@@ -26,7 +26,9 @@ async function parse(context) {
|
|
|
26
26
|
logger_1.default.error(missingTemplateErrorMessage);
|
|
27
27
|
throw new Error(missingTemplateErrorMessage);
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
// Pass a path relative to basePath so context.loadFile() resolves it correctly.
|
|
30
|
+
// Passing htmlFileName (which already includes basePath) would cause double resolution.
|
|
31
|
+
connection.options.email.body = context.loadFile(path_1.default.join(tools_1.constants.CONNECTIONS_DIRECTORY, connection.options.email.body));
|
|
30
32
|
}
|
|
31
33
|
return connection;
|
|
32
34
|
}),
|
|
@@ -14,6 +14,13 @@ async function parse(context) {
|
|
|
14
14
|
return { flows: null };
|
|
15
15
|
const parsedFlows = flows.map((flow) => {
|
|
16
16
|
const flowFile = path_1.default.join(context.basePath, flow.body);
|
|
17
|
+
const configRoot = path_1.default.resolve(context.basePath);
|
|
18
|
+
const resolvedFlowFile = path_1.default.resolve(flowFile);
|
|
19
|
+
if (!resolvedFlowFile.startsWith(configRoot + path_1.default.sep)) {
|
|
20
|
+
logger_1.default.warn(`Flow body file "${flow.body}" resolves to "${resolvedFlowFile}" which is outside the config directory "${configRoot}". ` +
|
|
21
|
+
`This will be blocked as an error in the next major release. ` +
|
|
22
|
+
`Move the file inside your config directory.`);
|
|
23
|
+
}
|
|
17
24
|
const parsedFlowBody = (0, utils_1.loadJSON)(flowFile, {
|
|
18
25
|
mappings: context.mappings,
|
|
19
26
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
@@ -14,6 +14,13 @@ async function parse(context) {
|
|
|
14
14
|
return { forms: null };
|
|
15
15
|
const parsedForms = forms.map((form) => {
|
|
16
16
|
const formFile = path_1.default.join(context.basePath, form.body);
|
|
17
|
+
const configRoot = path_1.default.resolve(context.basePath);
|
|
18
|
+
const resolvedFormFile = path_1.default.resolve(formFile);
|
|
19
|
+
if (!resolvedFormFile.startsWith(configRoot + path_1.default.sep)) {
|
|
20
|
+
logger_1.default.warn(`Form body file "${form.body}" resolves to "${resolvedFormFile}" which is outside the config directory "${configRoot}". ` +
|
|
21
|
+
`This will be blocked as an error in the next major release. ` +
|
|
22
|
+
`Move the file inside your config directory.`);
|
|
23
|
+
}
|
|
17
24
|
const parsedFormBody = (0, utils_1.loadJSON)(formFile, {
|
|
18
25
|
mappings: context.mappings,
|
|
19
26
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
@@ -19,7 +19,13 @@ const loadScreenRenderers = (context, screenRenderArray) => {
|
|
|
19
19
|
Object.entries(screens).forEach(([, fileName]) => {
|
|
20
20
|
const filePath = fileName;
|
|
21
21
|
try {
|
|
22
|
-
const
|
|
22
|
+
const configRoot = path_1.default.resolve(context.basePath);
|
|
23
|
+
const rendererFile = path_1.default.resolve(context.basePath, filePath);
|
|
24
|
+
if (!rendererFile.startsWith(configRoot + path_1.default.sep)) {
|
|
25
|
+
logger_1.default.warn(`Screen renderer file "${filePath}" resolves to "${rendererFile}" which is outside the config directory "${configRoot}". ` +
|
|
26
|
+
`This will be blocked as an error in the next major release. ` +
|
|
27
|
+
`Move the file inside your config directory.`);
|
|
28
|
+
}
|
|
23
29
|
const rendererData = (0, utils_1.loadJSON)(rendererFile, {
|
|
24
30
|
mappings: context.mappings,
|
|
25
31
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
package/lib/readonly.js
CHANGED
|
@@ -28,6 +28,11 @@ const readOnlyFields = {
|
|
|
28
28
|
'config_route',
|
|
29
29
|
'owners',
|
|
30
30
|
],
|
|
31
|
+
attackProtection: [
|
|
32
|
+
// Returned by GET but rejected by PATCH with "Additional properties not allowed"
|
|
33
|
+
'bruteForceProtection.form_submission_mode',
|
|
34
|
+
'bruteForceProtection.form_submission_behavior',
|
|
35
|
+
],
|
|
31
36
|
};
|
|
32
37
|
function getExcludedFields(config) {
|
|
33
38
|
const strippedFields = { ...readOnlyFields };
|
|
@@ -314,7 +314,9 @@ class AttackProtectionHandler extends default_1.default {
|
|
|
314
314
|
updates.push(this.client.attackProtection.captcha.update(attackProtection.captcha));
|
|
315
315
|
}
|
|
316
316
|
if (attackProtection.bruteForceProtection) {
|
|
317
|
-
|
|
317
|
+
// Strip fields returned by GET but rejected by PATCH
|
|
318
|
+
const { form_submission_mode, form_submission_behavior, ...bruteForcePayload } = attackProtection.bruteForceProtection;
|
|
319
|
+
updates.push(this.client.attackProtection.bruteForceProtection.update(bruteForcePayload));
|
|
318
320
|
}
|
|
319
321
|
if (attackProtection.suspiciousIpThrottling) {
|
|
320
322
|
updates.push(this.client.attackProtection.suspiciousIpThrottling.update(attackProtection.suspiciousIpThrottling));
|
|
@@ -254,6 +254,27 @@ export declare const schema: {
|
|
|
254
254
|
enum: ("allow" | "allow_if_empty")[];
|
|
255
255
|
description: string;
|
|
256
256
|
};
|
|
257
|
+
third_party_client_access: {
|
|
258
|
+
type: string;
|
|
259
|
+
description: string;
|
|
260
|
+
properties: {
|
|
261
|
+
default_value: {
|
|
262
|
+
type: string;
|
|
263
|
+
enum: "block"[];
|
|
264
|
+
description: string;
|
|
265
|
+
};
|
|
266
|
+
allowed_values: {
|
|
267
|
+
type: string;
|
|
268
|
+
items: {
|
|
269
|
+
type: string;
|
|
270
|
+
enum: ("allow" | "block")[];
|
|
271
|
+
};
|
|
272
|
+
uniqueItems: boolean;
|
|
273
|
+
description: string;
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
required: string[];
|
|
277
|
+
};
|
|
257
278
|
invitation_landing_client_id: {
|
|
258
279
|
type: string;
|
|
259
280
|
description: string;
|
|
@@ -59,6 +59,27 @@ const myOrganizationConfigurationSchema = {
|
|
|
59
59
|
enum: Object.values(auth0_1.Management.ClientMyOrganizationDeletionBehaviorEnum),
|
|
60
60
|
description: 'The deletion behavior for My Organization connections created by this client',
|
|
61
61
|
},
|
|
62
|
+
third_party_client_access: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
description: 'Controls whether third-party clients can access the organization via the My Organization API.',
|
|
65
|
+
properties: {
|
|
66
|
+
default_value: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
enum: Object.values(auth0_1.Management.ClientMyOrganizationConfigurationThirdPartyClientAccessDefaultValueEnum),
|
|
69
|
+
description: 'The default third-party client access value.',
|
|
70
|
+
},
|
|
71
|
+
allowed_values: {
|
|
72
|
+
type: 'array',
|
|
73
|
+
items: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
enum: Object.values(auth0_1.Management.ClientMyOrganizationConfigurationThirdPartyClientAccessAllowedValuesEnum),
|
|
76
|
+
},
|
|
77
|
+
uniqueItems: true,
|
|
78
|
+
description: 'The allowed third-party client access values.',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ['default_value', 'allowed_values'],
|
|
82
|
+
},
|
|
62
83
|
invitation_landing_client_id: {
|
|
63
84
|
type: 'string',
|
|
64
85
|
description: 'The client ID of the invitation landing client for the My Organization configuration',
|
|
@@ -36,6 +36,33 @@ export declare const schema: {
|
|
|
36
36
|
connection_config: {
|
|
37
37
|
type: string[];
|
|
38
38
|
};
|
|
39
|
+
cross_app_access_resource_app: {
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
properties: {
|
|
43
|
+
status: {
|
|
44
|
+
type: string;
|
|
45
|
+
properties: {
|
|
46
|
+
default_value: {
|
|
47
|
+
type: string;
|
|
48
|
+
enum: ("enabled" | "disabled")[];
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
51
|
+
allowed_values: {
|
|
52
|
+
type: string;
|
|
53
|
+
items: {
|
|
54
|
+
type: string;
|
|
55
|
+
enum: ("enabled" | "disabled")[];
|
|
56
|
+
};
|
|
57
|
+
uniqueItems: boolean;
|
|
58
|
+
description: string;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
required: string[];
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
required: string[];
|
|
65
|
+
};
|
|
39
66
|
strategy_overrides: {
|
|
40
67
|
type: string[];
|
|
41
68
|
properties: {
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getConnectionProfile = exports.schema = void 0;
|
|
7
|
+
const auth0_1 = require("auth0");
|
|
7
8
|
const default_1 = __importDefault(require("./default"));
|
|
8
9
|
const client_1 = require("../client");
|
|
9
10
|
const logger_1 = __importDefault(require("../../../logger"));
|
|
@@ -42,6 +43,35 @@ exports.schema = {
|
|
|
42
43
|
connection_config: {
|
|
43
44
|
type: ['object', 'null'],
|
|
44
45
|
},
|
|
46
|
+
cross_app_access_resource_app: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
description: 'Controls whether organization admins may enable Cross App Access (XAA) on their Identity Providers.',
|
|
49
|
+
properties: {
|
|
50
|
+
status: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
default_value: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
enum: Object.values(auth0_1.Management.ConnectionProfileCrossAppAccessResourceAppStatusDefaultValueEnum),
|
|
56
|
+
description: 'The default Cross App Access resource app status.',
|
|
57
|
+
},
|
|
58
|
+
allowed_values: {
|
|
59
|
+
type: 'array',
|
|
60
|
+
items: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
enum: Object.values(auth0_1.Management.ConnectionProfileCrossAppAccessResourceAppStatusValueEnum),
|
|
63
|
+
},
|
|
64
|
+
uniqueItems: true,
|
|
65
|
+
description: 'The allowed Cross App Access resource app status values.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
// allowed_values is optional per the SDK type (unlike the client's
|
|
69
|
+
// third_party_client_access, which requires both fields).
|
|
70
|
+
required: ['default_value'],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ['status'],
|
|
74
|
+
},
|
|
45
75
|
strategy_overrides: {
|
|
46
76
|
type: ['object', 'null'],
|
|
47
77
|
properties: {
|
|
@@ -380,12 +380,15 @@ class ConnectionsHandler extends default_1.default {
|
|
|
380
380
|
}
|
|
381
381
|
getFormattedOptions(connection, clients) {
|
|
382
382
|
try {
|
|
383
|
+
const { idpinitiated } = connection.options;
|
|
383
384
|
return {
|
|
384
385
|
options: {
|
|
385
386
|
...connection.options,
|
|
386
387
|
idpinitiated: {
|
|
387
|
-
...
|
|
388
|
-
|
|
388
|
+
...idpinitiated,
|
|
389
|
+
...(idpinitiated.client_id && {
|
|
390
|
+
client_id: (0, utils_1.convertClientNameToId)(idpinitiated.client_id, clients),
|
|
391
|
+
}),
|
|
389
392
|
},
|
|
390
393
|
},
|
|
391
394
|
};
|
|
@@ -744,7 +747,11 @@ class ConnectionsHandler extends default_1.default {
|
|
|
744
747
|
...(enabledClients !== undefined && { enabled_clients: enabledClients }),
|
|
745
748
|
};
|
|
746
749
|
});
|
|
747
|
-
|
|
750
|
+
let proposedChanges = await super.dryRunChanges({ ...assets, connections: formatted });
|
|
751
|
+
const includedConnections = (assets.include && assets.include.connections) || [];
|
|
752
|
+
const excludedConnections = (assets.exclude && assets.exclude.connections) || [];
|
|
753
|
+
proposedChanges = (0, utils_1.filterExcluded)(proposedChanges, excludedConnections);
|
|
754
|
+
proposedChanges = (0, utils_1.filterIncluded)(proposedChanges, includedConnections);
|
|
748
755
|
return (0, exports.addExcludedConnectionPropertiesToChanges)({
|
|
749
756
|
proposedChanges,
|
|
750
757
|
existingConnections,
|
|
@@ -554,7 +554,7 @@ class DatabaseHandler extends default_1.default {
|
|
|
554
554
|
assets: formatted,
|
|
555
555
|
existing: existingDatabasesConnections,
|
|
556
556
|
identifiers: this.identifiers,
|
|
557
|
-
ignoreDryRunFields: this.
|
|
557
|
+
ignoreDryRunFields: this.getEffectiveIgnoreDryRunFields(),
|
|
558
558
|
});
|
|
559
559
|
}
|
|
560
560
|
// Run after clients are updated so we can convert all the enabled_clients names to id's
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { Asset, Assets, Auth0APIClient, CalculatedChanges } from '../../../types';
|
|
2
2
|
import { ConfigFunction } from '../../../configFactory';
|
|
3
3
|
export declare function order(value: any): (t: any, n: any, descriptor: any) => any;
|
|
4
|
+
export interface RetryOptions {
|
|
5
|
+
maxRetries?: number;
|
|
6
|
+
initialDelay?: number;
|
|
7
|
+
maxDelay?: number;
|
|
8
|
+
onRetry?: (error: any, attempt: number, delay: number) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Executes a function with exponential backoff retry logic for rate limit errors (429).
|
|
12
|
+
*
|
|
13
|
+
* @param fn - The function to execute with retry logic
|
|
14
|
+
* @param options - Configuration options for retry behavior
|
|
15
|
+
* @returns Promise that resolves with the function result or rejects after max retries
|
|
16
|
+
*/
|
|
17
|
+
export declare function retryWithExponentialBackoff<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
4
18
|
type ApiMethodOverride = string | Function;
|
|
5
19
|
export default class APIHandler {
|
|
6
20
|
config: ConfigFunction;
|
|
@@ -50,6 +64,15 @@ export default class APIHandler {
|
|
|
50
64
|
*/
|
|
51
65
|
getEffectiveIgnoreDryRunFields(): string[];
|
|
52
66
|
getClientFN(fn: ApiMethodOverride): Function;
|
|
67
|
+
/**
|
|
68
|
+
* Builds the exponential-backoff retry configuration for this handler from the
|
|
69
|
+
* shared `AUTH0_MAX_RETRIES` / `AUTH0_RETRY_INITIAL_DELAY_MS` /
|
|
70
|
+
* `AUTH0_RETRY_MAX_DELAY_MS` config keys. Exposed so that handlers which issue
|
|
71
|
+
* writes outside the default `processChanges` flow (e.g. organizations, which
|
|
72
|
+
* writes nested connections/grants directly) can wrap those calls in the same
|
|
73
|
+
* 429 backoff behaviour as the base handler.
|
|
74
|
+
*/
|
|
75
|
+
getRetryConfig(): RetryOptions;
|
|
53
76
|
didDelete(item: Asset): void;
|
|
54
77
|
didCreate(item: Asset): void;
|
|
55
78
|
didUpdate(item: Asset): void;
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.order = order;
|
|
7
|
+
exports.retryWithExponentialBackoff = retryWithExponentialBackoff;
|
|
7
8
|
const auth0_1 = require("auth0");
|
|
8
9
|
const validationError_1 = __importDefault(require("../../validationError"));
|
|
9
10
|
const utils_1 = require("../../utils");
|
|
@@ -114,6 +115,25 @@ class APIHandler {
|
|
|
114
115
|
}
|
|
115
116
|
return fn;
|
|
116
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Builds the exponential-backoff retry configuration for this handler from the
|
|
120
|
+
* shared `AUTH0_MAX_RETRIES` / `AUTH0_RETRY_INITIAL_DELAY_MS` /
|
|
121
|
+
* `AUTH0_RETRY_MAX_DELAY_MS` config keys. Exposed so that handlers which issue
|
|
122
|
+
* writes outside the default `processChanges` flow (e.g. organizations, which
|
|
123
|
+
* writes nested connections/grants directly) can wrap those calls in the same
|
|
124
|
+
* 429 backoff behaviour as the base handler.
|
|
125
|
+
*/
|
|
126
|
+
getRetryConfig() {
|
|
127
|
+
const retryConfig = {
|
|
128
|
+
maxRetries: this.config('AUTH0_MAX_RETRIES') || DEFAULT_MAX_RETRIES,
|
|
129
|
+
initialDelay: this.config('AUTH0_RETRY_INITIAL_DELAY_MS') || DEFAULT_INITIAL_DELAY_MS,
|
|
130
|
+
maxDelay: this.config('AUTH0_RETRY_MAX_DELAY_MS') || DEFAULT_MAX_DELAY_MS,
|
|
131
|
+
onRetry: (error, attempt, delay) => {
|
|
132
|
+
logger_1.default.warn(`Rate limit hit for [${this.type}]. Retrying attempt ${attempt}/${retryConfig.maxRetries} after ${Math.round(delay / 1000)}s...`);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
return retryConfig;
|
|
136
|
+
}
|
|
117
137
|
didDelete(item) {
|
|
118
138
|
logger_1.default.info(`Deleted [${this.type}]: ${this.objString(item)}`);
|
|
119
139
|
}
|
|
@@ -210,7 +230,6 @@ class APIHandler {
|
|
|
210
230
|
return (0, calculateDryRunChanges_1.calculateDryRunChanges)({
|
|
211
231
|
type: this.type,
|
|
212
232
|
assets: typeAssets,
|
|
213
|
-
// @ts-ignore TODO: investigate what happens when `existing` is null
|
|
214
233
|
existing,
|
|
215
234
|
identifiers: this.identifiers,
|
|
216
235
|
ignoreDryRunFields: this.getEffectiveIgnoreDryRunFields(),
|
|
@@ -243,7 +262,6 @@ class APIHandler {
|
|
|
243
262
|
return (0, calculateDryRunChanges_1.calculateDryRunChanges)({
|
|
244
263
|
type: this.type,
|
|
245
264
|
assets: typeAssets,
|
|
246
|
-
// @ts-ignore TODO: investigate what happens when `existing` is null
|
|
247
265
|
existing,
|
|
248
266
|
identifiers: this.identifiers,
|
|
249
267
|
ignoreDryRunFields: this.getEffectiveIgnoreDryRunFields(),
|
|
@@ -282,14 +300,7 @@ class APIHandler {
|
|
|
282
300
|
const conflicts = changes.conflicts || [];
|
|
283
301
|
logger_1.default.debug(`Start processChanges for ${this.type} [delete:${del.length}] [update:${update.length}], [create:${create.length}], [conflicts:${conflicts.length}]`);
|
|
284
302
|
// Set retry configuration from config
|
|
285
|
-
const retryConfig =
|
|
286
|
-
maxRetries: this.config('AUTH0_MAX_RETRIES') || DEFAULT_MAX_RETRIES,
|
|
287
|
-
initialDelay: this.config('AUTH0_RETRY_INITIAL_DELAY_MS') || DEFAULT_INITIAL_DELAY_MS,
|
|
288
|
-
maxDelay: this.config('AUTH0_RETRY_MAX_DELAY_MS') || DEFAULT_MAX_DELAY_MS,
|
|
289
|
-
onRetry: (error, attempt, delay) => {
|
|
290
|
-
logger_1.default.warn(`Rate limit hit for [${this.type}]. Retrying attempt ${attempt}/${retryConfig.maxRetries} after ${Math.round(delay / 1000)}s...`);
|
|
291
|
-
},
|
|
292
|
-
};
|
|
303
|
+
const retryConfig = this.getRetryConfig();
|
|
293
304
|
// Process Deleted
|
|
294
305
|
if (del.length > 0) {
|
|
295
306
|
const allowDelete = this.config('AUTH0_ALLOW_DELETE') === 'true' || this.config('AUTH0_ALLOW_DELETE') === true;
|
|
@@ -147,7 +147,7 @@ export default class OrganizationsHandler extends DefaultHandler {
|
|
|
147
147
|
createOrganizations(creates: CalculatedChanges['create']): Promise<void>;
|
|
148
148
|
updateOrganization(org: any, organizations: any): Promise<{
|
|
149
149
|
id: any;
|
|
150
|
-
}>;
|
|
150
|
+
} | null>;
|
|
151
151
|
getClientGrantIDByClientName(clientsName: string): string;
|
|
152
152
|
getClientIdByClientName(clientName: string): string;
|
|
153
153
|
getFormattedClientGrants(): Promise<FormattedClientGrants[]>;
|
|
@@ -194,8 +194,9 @@ class OrganizationsHandler extends default_1.default {
|
|
|
194
194
|
throw new Error(`Organization "${organization.name}" was created but the response did not include an ID. Skipping connection/grant association.`);
|
|
195
195
|
}
|
|
196
196
|
const createdId = created.id;
|
|
197
|
+
const retryConfig = this.getRetryConfig();
|
|
197
198
|
if (typeof org.connections !== 'undefined' && org.connections.length > 0) {
|
|
198
|
-
await Promise.all(org.connections.map((conn) => this.client.organizations.connections.create(createdId, conn)));
|
|
199
|
+
await Promise.all(org.connections.map((conn) => (0, default_1.retryWithExponentialBackoff)(() => this.client.organizations.connections.create(createdId, conn), retryConfig)));
|
|
199
200
|
}
|
|
200
201
|
if (typeof org.client_grants !== 'undefined' && org.client_grants.length > 0) {
|
|
201
202
|
await Promise.all(org.client_grants.map((organizationClientGrants) => this.createOrganizationClientGrants(createdId, this.getClientGrantIDByClientName(organizationClientGrants.client_id))));
|
|
@@ -240,7 +241,8 @@ class OrganizationsHandler extends default_1.default {
|
|
|
240
241
|
.promise();
|
|
241
242
|
}
|
|
242
243
|
async updateOrganization(org, organizations) {
|
|
243
|
-
const
|
|
244
|
+
const existingOrg = organizations.find((orgToUpdate) => orgToUpdate.name === org.name);
|
|
245
|
+
const { connections: existingConnections, client_grants: existingClientGrants, discovery_domains: existingDiscoveryDomains, clients: existingOrgClients = [], } = existingOrg;
|
|
244
246
|
const params = { id: org.id };
|
|
245
247
|
const { connections, client_grants: organizationClientGrants, discovery_domains: organizationDiscoveryDomains, clients: organizationClients, } = org;
|
|
246
248
|
delete org.connections;
|
|
@@ -249,7 +251,14 @@ class OrganizationsHandler extends default_1.default {
|
|
|
249
251
|
delete org.client_grants;
|
|
250
252
|
delete org.discovery_domains;
|
|
251
253
|
delete org.clients;
|
|
252
|
-
|
|
254
|
+
// Only PATCH if top-level properties actually differ from the existing state.
|
|
255
|
+
// Compare only the keys present in the desired config against those same keys
|
|
256
|
+
// on the remote org, so that fields the user didn't specify are not considered.
|
|
257
|
+
let changed = false;
|
|
258
|
+
if (!(0, lodash_1.isEqual)(org, (0, lodash_1.pick)(existingOrg, Object.keys(org)))) {
|
|
259
|
+
await this.client.organizations.update(params.id, org);
|
|
260
|
+
changed = true;
|
|
261
|
+
}
|
|
253
262
|
// organization connections
|
|
254
263
|
const connectionsToRemove = existingConnections.filter((c) => !connections.find((x) => x.connection_id === c.connection_id));
|
|
255
264
|
const connectionsToAdd = connections.filter((c) => !existingConnections.find((x) => x.connection_id === c.connection_id));
|
|
@@ -260,27 +269,27 @@ class OrganizationsHandler extends default_1.default {
|
|
|
260
269
|
x.organization_access_level !== c.organization_access_level ||
|
|
261
270
|
x.organization_connection_name !== c.organization_connection_name ||
|
|
262
271
|
x.is_enabled !== (c.is_enabled ?? true))));
|
|
272
|
+
if (connectionsToUpdate.length > 0 ||
|
|
273
|
+
connectionsToAdd.length > 0 ||
|
|
274
|
+
connectionsToRemove.length > 0) {
|
|
275
|
+
changed = true;
|
|
276
|
+
}
|
|
277
|
+
const retryConfig = this.getRetryConfig();
|
|
263
278
|
// Handle updates first
|
|
264
|
-
await Promise.all(connectionsToUpdate.map((conn) => this.client.organizations.connections
|
|
265
|
-
.update(params.id, conn.connection_id, {
|
|
279
|
+
await Promise.all(connectionsToUpdate.map((conn) => (0, default_1.retryWithExponentialBackoff)(() => this.client.organizations.connections.update(params.id, conn.connection_id, {
|
|
266
280
|
organization_connection_name: conn.organization_connection_name,
|
|
267
281
|
assign_membership_on_login: conn.assign_membership_on_login,
|
|
268
282
|
show_as_button: conn.show_as_button,
|
|
269
283
|
is_signup_enabled: conn.is_signup_enabled,
|
|
270
284
|
is_enabled: conn.is_enabled,
|
|
271
285
|
organization_access_level: conn.organization_access_level,
|
|
272
|
-
})
|
|
273
|
-
.catch(() => {
|
|
286
|
+
}), retryConfig).catch(() => {
|
|
274
287
|
throw new Error(`Problem updating Enabled Connection ${conn.connection_id} for organizations ${params.id}`);
|
|
275
288
|
})));
|
|
276
|
-
await Promise.all(connectionsToAdd.map((conn) => this.client.organizations.connections
|
|
277
|
-
.create(params.id, (0, lodash_1.omit)(conn, 'connection'))
|
|
278
|
-
.catch(() => {
|
|
289
|
+
await Promise.all(connectionsToAdd.map((conn) => (0, default_1.retryWithExponentialBackoff)(() => this.client.organizations.connections.create(params.id, (0, lodash_1.omit)(conn, 'connection')), retryConfig).catch(() => {
|
|
279
290
|
throw new Error(`Problem adding Enabled Connection ${conn.connection_id} for organizations ${params.id}`);
|
|
280
291
|
})));
|
|
281
|
-
await Promise.all(connectionsToRemove.map((conn) => this.client.organizations.connections
|
|
282
|
-
.delete(params.id, conn.connection_id)
|
|
283
|
-
.catch(() => {
|
|
292
|
+
await Promise.all(connectionsToRemove.map((conn) => (0, default_1.retryWithExponentialBackoff)(() => this.client.organizations.connections.delete(params.id, conn.connection_id), retryConfig).catch(() => {
|
|
284
293
|
throw new Error(`Problem removing Enabled Connection ${conn.connection_id} for organizations ${params.id}`);
|
|
285
294
|
})));
|
|
286
295
|
// organization client_grants
|
|
@@ -294,6 +303,9 @@ class OrganizationsHandler extends default_1.default {
|
|
|
294
303
|
?.map((clientGrant) => ({
|
|
295
304
|
grant_id: this.getClientGrantIDByClientName(clientGrant.client_id),
|
|
296
305
|
})) || [];
|
|
306
|
+
if (orgClientGrantsToAdd.length > 0 || orgClientGrantsToRemove.length > 0) {
|
|
307
|
+
changed = true;
|
|
308
|
+
}
|
|
297
309
|
// Handle updates first
|
|
298
310
|
await Promise.all(orgClientGrantsToAdd.map((orgClientGrant) => this.createOrganizationClientGrants(params.id, orgClientGrant.grant_id).catch(() => {
|
|
299
311
|
throw new Error(`Problem adding organization clientGrant ${orgClientGrant.grant_id} for organizations ${params.id}`);
|
|
@@ -315,6 +327,9 @@ class OrganizationsHandler extends default_1.default {
|
|
|
315
327
|
};
|
|
316
328
|
})
|
|
317
329
|
.filter(Boolean) || [];
|
|
330
|
+
if (orgDiscoveryDomainsToUpdate.length > 0 || orgDiscoveryDomainsToAdd.length > 0) {
|
|
331
|
+
changed = true;
|
|
332
|
+
}
|
|
318
333
|
for (const { id, domain, ...updateParams } of orgDiscoveryDomainsToUpdate) {
|
|
319
334
|
try {
|
|
320
335
|
await this.updateOrganizationDiscoveryDomain(params.id, id, domain, updateParams);
|
|
@@ -338,6 +353,7 @@ class OrganizationsHandler extends default_1.default {
|
|
|
338
353
|
if (orgDiscoveryDomainsToRemove.length > 0) {
|
|
339
354
|
if (this.config('AUTH0_ALLOW_DELETE') === 'true' ||
|
|
340
355
|
this.config('AUTH0_ALLOW_DELETE') === true) {
|
|
356
|
+
changed = true;
|
|
341
357
|
for (const domain of orgDiscoveryDomainsToRemove) {
|
|
342
358
|
try {
|
|
343
359
|
await this.deleteOrganizationDiscoveryDomain(params.id, domain.domain, domain.id);
|
|
@@ -375,6 +391,7 @@ class OrganizationsHandler extends default_1.default {
|
|
|
375
391
|
}))
|
|
376
392
|
.filter((oc) => !!oc.client_id);
|
|
377
393
|
if (orgClientsToAdd.length > 0) {
|
|
394
|
+
changed = true;
|
|
378
395
|
await this.createOrganizationClients(params.id, orgClientsToAdd).catch((err) => {
|
|
379
396
|
throw new Error(`Problem adding org clients for organization ${params.id}\n${err}`);
|
|
380
397
|
});
|
|
@@ -382,6 +399,7 @@ class OrganizationsHandler extends default_1.default {
|
|
|
382
399
|
if (orgClientsToRemove.length > 0) {
|
|
383
400
|
if (this.config('AUTH0_ALLOW_DELETE') === 'true' ||
|
|
384
401
|
this.config('AUTH0_ALLOW_DELETE') === true) {
|
|
402
|
+
changed = true;
|
|
385
403
|
await this.deleteOrganizationClients(params.id, orgClientsToRemove).catch((err) => {
|
|
386
404
|
throw new Error(`Problem removing org clients for organization ${params.id}\n${err}`);
|
|
387
405
|
});
|
|
@@ -390,6 +408,9 @@ class OrganizationsHandler extends default_1.default {
|
|
|
390
408
|
logger_1.default.warn(`Detected the following organization client associations should be removed. Doing so may be destructive.\nYou can enable deletes by setting 'AUTH0_ALLOW_DELETE' to true in the config\n${orgClientsToRemove.join('\n')}`);
|
|
391
409
|
}
|
|
392
410
|
}
|
|
411
|
+
if (orgClientsToUpdate.length > 0) {
|
|
412
|
+
changed = true;
|
|
413
|
+
}
|
|
393
414
|
await Promise.all(orgClientsToUpdate.map((oc) => this.client.organizations.clients
|
|
394
415
|
.update(params.id, oc.client_id, {
|
|
395
416
|
use_for_member_access: oc.use_for_member_access,
|
|
@@ -397,7 +418,7 @@ class OrganizationsHandler extends default_1.default {
|
|
|
397
418
|
.catch((err) => {
|
|
398
419
|
throw new Error(`Problem updating org client ${oc.client_id} for organization ${params.id}\n${err}`);
|
|
399
420
|
})));
|
|
400
|
-
return params;
|
|
421
|
+
return changed ? params : null;
|
|
401
422
|
}
|
|
402
423
|
getClientGrantIDByClientName(clientsName) {
|
|
403
424
|
const found = this.formattedClientGrants.find((c) => c.client_id === clientsName);
|
|
@@ -435,8 +456,10 @@ class OrganizationsHandler extends default_1.default {
|
|
|
435
456
|
data: updates || [],
|
|
436
457
|
generator: (item) => this.updateOrganization(item, orgs)
|
|
437
458
|
.then((data) => {
|
|
438
|
-
|
|
439
|
-
|
|
459
|
+
if (data) {
|
|
460
|
+
this.didUpdate(data);
|
|
461
|
+
this.updated += 1;
|
|
462
|
+
}
|
|
440
463
|
})
|
|
441
464
|
.catch((err) => {
|
|
442
465
|
throw new Error(`Problem updating ${this.type} ${this.objString(item)}\n${err}`);
|
|
@@ -202,12 +202,20 @@ class PhoneProviderHandler extends default_1.default {
|
|
|
202
202
|
}
|
|
203
203
|
const currentProviders = await this.getPhoneProviders();
|
|
204
204
|
const providerReqPayload = (() => {
|
|
205
|
+
// Work on a shallow copy so we don't mutate the caller's asset (phoneProviders[0]).
|
|
206
|
+
// The SDK response type doesn't declare `credentials`, so widen it here.
|
|
207
|
+
const payload = { ...phoneProviders[0] };
|
|
205
208
|
// Removing id from update and create payloads, otherwise API will error
|
|
206
209
|
// id may be required to handle if `--export_ids=true`
|
|
207
|
-
|
|
208
|
-
if (payload && 'id' in payload) {
|
|
210
|
+
if ('id' in payload) {
|
|
209
211
|
delete payload.id;
|
|
210
212
|
}
|
|
213
|
+
// The `custom` provider requires a `credentials` object in the payload even
|
|
214
|
+
// when it is empty. Exported configs omit credentials, so inject an empty
|
|
215
|
+
// object here to avoid a "Missing required property: credentials" 400 error.
|
|
216
|
+
if (payload.name === 'custom' && payload.credentials == null) {
|
|
217
|
+
payload.credentials = {};
|
|
218
|
+
}
|
|
211
219
|
return payload;
|
|
212
220
|
})();
|
|
213
221
|
if (currentProviders === null || currentProviders.length === 0) {
|
|
@@ -108,6 +108,13 @@ function normalizeArrayValues(values) {
|
|
|
108
108
|
.map((value) => normalizeValue(value))
|
|
109
109
|
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Serializes a value to a human-readable string for use in diff messages.
|
|
113
|
+
* Objects and arrays are JSON-stringified to avoid "[object Object]" output.
|
|
114
|
+
*/
|
|
115
|
+
function formatDiffValue(value) {
|
|
116
|
+
return typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
|
|
117
|
+
}
|
|
111
118
|
/**
|
|
112
119
|
* Writes the accumulated diff log to a JSON file on disk.
|
|
113
120
|
* Useful for CI pipelines that want a machine-readable dry-run report.
|
|
@@ -179,7 +186,7 @@ function getObjectDifferences(localObj, remoteObj, keyObjPath = '', resourceType
|
|
|
179
186
|
differences.push(...nestedDifferences);
|
|
180
187
|
}
|
|
181
188
|
else if (item !== normalizedRemoteValue[index]) {
|
|
182
|
-
const message = `Array item difference at [${currentPath}[${index}]]: local:${item} vs remote:${normalizedRemoteValue[index]}`;
|
|
189
|
+
const message = `Array item difference at [${currentPath}[${index}]]: local:${formatDiffValue(item)} vs remote:${formatDiffValue(normalizedRemoteValue[index])}`;
|
|
183
190
|
differences.push(message);
|
|
184
191
|
}
|
|
185
192
|
});
|
|
@@ -204,13 +211,10 @@ function getObjectDifferences(localObj, remoteObj, keyObjPath = '', resourceType
|
|
|
204
211
|
differences.push(...nestedDifferences);
|
|
205
212
|
return;
|
|
206
213
|
}
|
|
207
|
-
// Compare primitive values
|
|
214
|
+
// Compare primitive values
|
|
208
215
|
if (localValue !== remoteValue) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const message = `Value difference for [${currentPath}]: local:${localValue} vs remote:${remoteValue}`;
|
|
212
|
-
differences.push(message);
|
|
213
|
-
}
|
|
216
|
+
const message = `Value difference for [${currentPath}]: local:${localValue} vs remote:${remoteValue}`;
|
|
217
|
+
differences.push(message);
|
|
214
218
|
}
|
|
215
219
|
});
|
|
216
220
|
return differences;
|
|
@@ -242,7 +246,7 @@ function calculateDryRunChanges({ type, assets, existing, identifiers = ['id', '
|
|
|
242
246
|
const create = [];
|
|
243
247
|
const conflicts = [];
|
|
244
248
|
const localAssets = (Array.isArray(assets) ? [...assets] : [assets]).map((asset) => type === 'tenant' ? normalizeTenantForDryRun(asset) : asset);
|
|
245
|
-
const remoteAssets = (Array.isArray(existing) ? [...existing] : [existing]).map((asset) => (type === 'tenant' ? normalizeTenantForDryRun(asset) : asset));
|
|
249
|
+
const remoteAssets = (Array.isArray(existing) ? [...existing] : existing ? [existing] : []).map((asset) => (type === 'tenant' ? normalizeTenantForDryRun(asset) : asset));
|
|
246
250
|
// Helper: returns true if a local and remote asset share at least one identifier value
|
|
247
251
|
const assetsMatch = (localAsset, remoteAsset) => identifiers.some((id) => {
|
|
248
252
|
if (Array.isArray(id)) {
|
package/lib/utils.js
CHANGED
|
@@ -301,12 +301,15 @@ exports.decodeBase64ToCertString = decodeBase64ToCertString;
|
|
|
301
301
|
// Format connection options by converting client IDs to client names for SAML connections
|
|
302
302
|
const getFormattedOptions = (connection, clients) => {
|
|
303
303
|
try {
|
|
304
|
+
const { idpinitiated } = connection.options;
|
|
304
305
|
return {
|
|
305
306
|
options: {
|
|
306
307
|
...connection.options,
|
|
307
308
|
idpinitiated: {
|
|
308
|
-
...
|
|
309
|
-
|
|
309
|
+
...idpinitiated,
|
|
310
|
+
...(idpinitiated.client_id && {
|
|
311
|
+
client_id: convertClientIdToName(idpinitiated.client_id, clients),
|
|
312
|
+
}),
|
|
310
313
|
},
|
|
311
314
|
},
|
|
312
315
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auth0-deploy-cli",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.44.0",
|
|
4
4
|
"description": "A command line tool for deploying updates to your Auth0 tenant",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@clack/prompts": "1.4.0",
|
|
37
37
|
"ajv": "^6.12.6",
|
|
38
|
-
"auth0": "^6.
|
|
38
|
+
"auth0": "^6.3.0",
|
|
39
39
|
"chalk": "5.6.2",
|
|
40
40
|
"dot-prop": "^5.3.0",
|
|
41
41
|
"fs-extra": "^10.1.0",
|