auth0-deploy-cli 8.42.0 → 8.43.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 +22 -1
- package/lib/context/directory/handlers/actionModules.js +11 -12
- package/lib/context/directory/handlers/actions.js +11 -13
- package/lib/context/directory/handlers/databases.js +4 -4
- package/lib/context/directory/handlers/hooks.js +12 -1
- package/lib/context/directory/handlers/rules.js +12 -1
- package/lib/context/directory/index.js +7 -8
- package/lib/context/yaml/index.js +7 -8
- package/lib/tools/auth0/handlers/connections.d.ts +11 -6
- package/lib/tools/auth0/handlers/connections.js +23 -4
- package/lib/tools/auth0/handlers/databases.js +11 -2
- package/lib/tools/auth0/handlers/organizations.d.ts +28 -0
- package/lib/tools/auth0/handlers/organizations.js +127 -2
- package/lib/tools/auth0/handlers/phoneTemplates.d.ts +4 -1
- package/lib/tools/auth0/handlers/phoneTemplates.js +67 -10
- package/lib/tools/auth0/handlers/roles.d.ts +4 -0
- package/lib/tools/auth0/handlers/roles.js +10 -0
- package/lib/types.d.ts +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [8.43.0] - 2026-08-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Add org-to-app entitlement (EA) support for organizations. [#1454]
|
|
15
|
+
- Support full group metadata for Google Workspace directory sync (GA). [#1456]
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Create phone templates on import instead of skipping when they have no ID. [#1457]
|
|
20
|
+
- Address path traversal vulnerability and false warnings in config file handlers. [#1449]
|
|
21
|
+
- Exclude org-scoped roles from tenant export by filtering `type='tenant'`. [#1453]
|
|
22
|
+
- Strip read-only field from roles to fix export/import round-trip. [#1445]
|
|
23
|
+
|
|
10
24
|
## [8.42.0] - 2026-07-30
|
|
11
25
|
|
|
12
26
|
### Added
|
|
@@ -1907,7 +1921,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1907
1921
|
[#1436]: https://github.com/auth0/auth0-deploy-cli/issues/1436
|
|
1908
1922
|
[#1437]: https://github.com/auth0/auth0-deploy-cli/issues/1437
|
|
1909
1923
|
[#1438]: https://github.com/auth0/auth0-deploy-cli/issues/1438
|
|
1910
|
-
[
|
|
1924
|
+
[#1445]: https://github.com/auth0/auth0-deploy-cli/issues/1445
|
|
1925
|
+
[#1449]: https://github.com/auth0/auth0-deploy-cli/issues/1449
|
|
1926
|
+
[#1453]: https://github.com/auth0/auth0-deploy-cli/issues/1453
|
|
1927
|
+
[#1454]: https://github.com/auth0/auth0-deploy-cli/issues/1454
|
|
1928
|
+
[#1456]: https://github.com/auth0/auth0-deploy-cli/issues/1456
|
|
1929
|
+
[#1457]: https://github.com/auth0/auth0-deploy-cli/issues/1457
|
|
1930
|
+
[Unreleased]: https://github.com/auth0/auth0-deploy-cli/compare/v8.43.0...HEAD
|
|
1931
|
+
[8.43.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.42.0...v8.43.0
|
|
1911
1932
|
[8.42.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.41.0...v8.42.0
|
|
1912
1933
|
[8.41.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.40.0...v8.41.0
|
|
1913
1934
|
[8.40.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.39.0...v8.40.0
|
|
@@ -20,20 +20,19 @@ function parse(context) {
|
|
|
20
20
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
21
21
|
}),
|
|
22
22
|
};
|
|
23
|
-
const moduleFolder = path_1.default.join(tools_1.constants.ACTION_MODULES_DIRECTORY, `${module.name}`);
|
|
24
23
|
if (module.code) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
logger_1.default.warn(`
|
|
30
|
-
`
|
|
31
|
-
`
|
|
32
|
-
module.code = context.loadFile(unixPath, moduleFolder);
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
module.code = context.loadFile(path_1.default.join(context.filePath, module.code), moduleFolder);
|
|
24
|
+
const normalizedCode = module.code.replace(/\\/g, '/');
|
|
25
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
26
|
+
const resolvedPath = path_1.default.resolve(context.filePath, normalizedCode);
|
|
27
|
+
if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
|
|
28
|
+
logger_1.default.warn(`Path "${module.code}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
|
|
29
|
+
`This will be blocked as an error in the next major release. ` +
|
|
30
|
+
`Move the file inside your config directory.`);
|
|
36
31
|
}
|
|
32
|
+
module.code = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
|
|
33
|
+
mappings: context.mappings,
|
|
34
|
+
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
35
|
+
});
|
|
37
36
|
}
|
|
38
37
|
return module;
|
|
39
38
|
});
|
|
@@ -22,21 +22,19 @@ function parse(context) {
|
|
|
22
22
|
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
23
23
|
}),
|
|
24
24
|
};
|
|
25
|
-
const actionFolder = path_1.default.join(tools_1.constants.ACTIONS_DIRECTORY, `${action.name}`);
|
|
26
25
|
if (action.code) {
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
logger_1.default.warn(`
|
|
32
|
-
`
|
|
33
|
-
`
|
|
34
|
-
action.code = context.loadFile(unixPath, actionFolder);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
// Otherwise, load the file from the context's file path
|
|
38
|
-
action.code = context.loadFile(path_1.default.join(context.filePath, action.code), actionFolder);
|
|
26
|
+
const normalizedCode = action.code.replace(/\\/g, '/');
|
|
27
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
28
|
+
const resolvedPath = path_1.default.resolve(context.filePath, normalizedCode);
|
|
29
|
+
if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
|
|
30
|
+
logger_1.default.warn(`Path "${action.code}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
|
|
31
|
+
`This will be blocked as an error in the next major release. ` +
|
|
32
|
+
`Move the file inside your config directory.`);
|
|
39
33
|
}
|
|
34
|
+
action.code = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
|
|
35
|
+
mappings: context.mappings,
|
|
36
|
+
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
37
|
+
});
|
|
40
38
|
}
|
|
41
39
|
return action;
|
|
42
40
|
});
|
|
@@ -41,11 +41,11 @@ function getDatabase(folder, configRoot, mappingOpts) {
|
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
43
|
const resolvedBase = path_1.default.resolve(configRoot);
|
|
44
|
-
const toLoad = path_1.default.resolve(folder, script);
|
|
44
|
+
const toLoad = path_1.default.resolve(folder, script.replace(/\\/g, '/'));
|
|
45
45
|
if (!toLoad.startsWith(resolvedBase + path_1.default.sep)) {
|
|
46
|
-
logger_1.default.warn(`
|
|
47
|
-
`
|
|
48
|
-
`
|
|
46
|
+
logger_1.default.warn(`Path "${script}" resolves to "${toLoad}" which is outside the config directory "${resolvedBase}". ` +
|
|
47
|
+
`This will be blocked as an error in the next major release. ` +
|
|
48
|
+
`Move the file inside your config directory.`);
|
|
49
49
|
}
|
|
50
50
|
database.options.customScripts[name] = (0, tools_1.loadFileAndReplaceKeywords)(toLoad, mappingOpts);
|
|
51
51
|
}
|
|
@@ -21,7 +21,18 @@ function parse(context) {
|
|
|
21
21
|
}),
|
|
22
22
|
};
|
|
23
23
|
if (hook.script) {
|
|
24
|
-
|
|
24
|
+
const normalizedScript = hook.script.replace(/\\/g, '/');
|
|
25
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
26
|
+
const resolvedPath = path_1.default.resolve(context.filePath, tools_1.constants.HOOKS_DIRECTORY, normalizedScript);
|
|
27
|
+
if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
|
|
28
|
+
logger_1.default.warn(`Path "${hook.script}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
|
|
29
|
+
`This will be blocked as an error in the next major release. ` +
|
|
30
|
+
`Move the file inside your config directory.`);
|
|
31
|
+
}
|
|
32
|
+
hook.script = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
|
|
33
|
+
mappings: context.mappings,
|
|
34
|
+
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
35
|
+
});
|
|
25
36
|
}
|
|
26
37
|
hook.name = hook.name.toLowerCase().replace(/\s/g, '-');
|
|
27
38
|
return hook;
|
|
@@ -21,7 +21,18 @@ function parse(context) {
|
|
|
21
21
|
}),
|
|
22
22
|
};
|
|
23
23
|
if (rule.script) {
|
|
24
|
-
|
|
24
|
+
const normalizedScript = rule.script.replace(/\\/g, '/');
|
|
25
|
+
const configRoot = path_1.default.resolve(context.filePath);
|
|
26
|
+
const resolvedPath = path_1.default.resolve(context.filePath, tools_1.constants.RULES_DIRECTORY, normalizedScript);
|
|
27
|
+
if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
|
|
28
|
+
logger_1.default.warn(`Path "${rule.script}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
|
|
29
|
+
`This will be blocked as an error in the next major release. ` +
|
|
30
|
+
`Move the file inside your config directory.`);
|
|
31
|
+
}
|
|
32
|
+
rule.script = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
|
|
33
|
+
mappings: context.mappings,
|
|
34
|
+
disableKeywordReplacement: context.disableKeywordReplacement,
|
|
35
|
+
});
|
|
25
36
|
}
|
|
26
37
|
return rule;
|
|
27
38
|
});
|
|
@@ -68,14 +68,13 @@ class DirectoryContext {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
loadFile(f, folder) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
toLoad
|
|
76
|
-
|
|
77
|
-
`
|
|
78
|
-
`Current absolute path used: ["${f}"]`);
|
|
71
|
+
const configRoot = path.resolve(this.filePath);
|
|
72
|
+
const basePath = path.resolve(this.filePath, folder);
|
|
73
|
+
const toLoad = path.resolve(basePath, f.replace(/\\/g, '/'));
|
|
74
|
+
if (!toLoad.startsWith(configRoot + path.sep)) {
|
|
75
|
+
logger_1.default.warn(`Path "${f}" resolves to "${toLoad}" which is outside the config directory "${configRoot}". ` +
|
|
76
|
+
`This will be blocked as an error in the next major release. ` +
|
|
77
|
+
`Move the file inside your config directory.`);
|
|
79
78
|
}
|
|
80
79
|
return (0, tools_1.loadFileAndReplaceKeywords)(toLoad, {
|
|
81
80
|
mappings: this.mappings,
|
|
@@ -43,15 +43,14 @@ class YAMLContext {
|
|
|
43
43
|
})();
|
|
44
44
|
}
|
|
45
45
|
loadFile(f) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
toLoad
|
|
50
|
-
|
|
51
|
-
`
|
|
52
|
-
`Current absolute path used: ["${f}"]`);
|
|
46
|
+
const configRoot = path_1.default.resolve(this.basePath);
|
|
47
|
+
const toLoad = path_1.default.resolve(this.basePath, f.replace(/\\/g, '/'));
|
|
48
|
+
if (!toLoad.startsWith(configRoot + path_1.default.sep)) {
|
|
49
|
+
logger_1.default.warn(`Path "${f}" resolves to "${toLoad}" which is outside the config directory "${configRoot}". ` +
|
|
50
|
+
`This will be blocked as an error in the next major release. ` +
|
|
51
|
+
`Move the file inside your config directory.`);
|
|
53
52
|
}
|
|
54
|
-
return (0, tools_1.loadFileAndReplaceKeywords)(
|
|
53
|
+
return (0, tools_1.loadFileAndReplaceKeywords)(toLoad, {
|
|
55
54
|
mappings: this.mappings,
|
|
56
55
|
disableKeywordReplacement: this.disableKeywordReplacement,
|
|
57
56
|
});
|
|
@@ -159,6 +159,15 @@ export declare const schema: {
|
|
|
159
159
|
id: {
|
|
160
160
|
type: string;
|
|
161
161
|
};
|
|
162
|
+
name: {
|
|
163
|
+
type: string;
|
|
164
|
+
};
|
|
165
|
+
email: {
|
|
166
|
+
type: string;
|
|
167
|
+
};
|
|
168
|
+
direct_members_count: {
|
|
169
|
+
type: string;
|
|
170
|
+
};
|
|
162
171
|
};
|
|
163
172
|
required: string[];
|
|
164
173
|
};
|
|
@@ -173,9 +182,7 @@ type DirectoryProvisioningConfig = Management.DirectoryProvisioning;
|
|
|
173
182
|
export type Connection = Management.ConnectionForList & {
|
|
174
183
|
enabled_clients?: string[];
|
|
175
184
|
directory_provisioning_configuration?: Pick<DirectoryProvisioningConfig, 'mapping' | 'synchronize_automatically' | 'synchronize_groups'> & {
|
|
176
|
-
synchronized_groups?:
|
|
177
|
-
id: string;
|
|
178
|
-
}>;
|
|
185
|
+
synchronized_groups?: Management.SynchronizedGroupPayload[];
|
|
179
186
|
};
|
|
180
187
|
};
|
|
181
188
|
export declare const addExcludedConnectionPropertiesToChanges: ({ proposedChanges, existingConnections, config, }: {
|
|
@@ -251,9 +258,7 @@ export default class ConnectionsHandler extends DefaultAPIHandler {
|
|
|
251
258
|
/**
|
|
252
259
|
* Retrieves all synchronized groups for a connection using checkpoint pagination.
|
|
253
260
|
*/
|
|
254
|
-
getConnectionSynchronizedGroups(connectionId: string): Promise<
|
|
255
|
-
id: string;
|
|
256
|
-
}> | null>;
|
|
261
|
+
getConnectionSynchronizedGroups(connectionId: string): Promise<Management.SynchronizedGroupPayload[] | null>;
|
|
257
262
|
/**
|
|
258
263
|
* Replaces the synchronized groups for a connection (PUT replace-all semantics).
|
|
259
264
|
*/
|
|
@@ -172,6 +172,9 @@ exports.schema = {
|
|
|
172
172
|
type: 'object',
|
|
173
173
|
properties: {
|
|
174
174
|
id: { type: 'string' },
|
|
175
|
+
name: { type: 'string' },
|
|
176
|
+
email: { type: 'string' },
|
|
177
|
+
direct_members_count: { type: 'number' },
|
|
175
178
|
},
|
|
176
179
|
required: ['id'],
|
|
177
180
|
},
|
|
@@ -483,12 +486,28 @@ class ConnectionsHandler extends default_1.default {
|
|
|
483
486
|
try {
|
|
484
487
|
const groups = [];
|
|
485
488
|
let page = await this.client.connections.directoryProvisioning.listSynchronizedGroups(connectionId, {});
|
|
486
|
-
for (const g of page.data ?? [])
|
|
487
|
-
groups.push({
|
|
489
|
+
for (const g of page.data ?? []) {
|
|
490
|
+
groups.push({
|
|
491
|
+
id: g.id,
|
|
492
|
+
...(g.name !== undefined && { name: g.name }),
|
|
493
|
+
...(g.email !== undefined && { email: g.email }),
|
|
494
|
+
...(g.direct_members_count !== undefined && {
|
|
495
|
+
direct_members_count: g.direct_members_count,
|
|
496
|
+
}),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
488
499
|
while (page.hasNextPage?.()) {
|
|
489
500
|
page = await page.getNextPage();
|
|
490
|
-
for (const g of page.data ?? [])
|
|
491
|
-
groups.push({
|
|
501
|
+
for (const g of page.data ?? []) {
|
|
502
|
+
groups.push({
|
|
503
|
+
id: g.id,
|
|
504
|
+
...(g.name !== undefined && { name: g.name }),
|
|
505
|
+
...(g.email !== undefined && { email: g.email }),
|
|
506
|
+
...(g.direct_members_count !== undefined && {
|
|
507
|
+
direct_members_count: g.direct_members_count,
|
|
508
|
+
}),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
492
511
|
}
|
|
493
512
|
return groups;
|
|
494
513
|
}
|
|
@@ -462,12 +462,21 @@ class DatabaseHandler extends default_1.default {
|
|
|
462
462
|
}
|
|
463
463
|
return connection;
|
|
464
464
|
});
|
|
465
|
+
// A connection can return both legacy password fields and `password_options` on read, but
|
|
466
|
+
// they're mutually exclusive on write. Strip the legacy fields to keep the export deployable.
|
|
467
|
+
const dbConnectionsNormalized = dbConnectionsWithActionNames.map((connection) => {
|
|
468
|
+
if (connection.options && 'password_options' in connection.options) {
|
|
469
|
+
const { passwordPolicy, password_complexity_options, password_history, password_no_personal_info, password_dictionary, ...options } = connection.options;
|
|
470
|
+
return { ...connection, options };
|
|
471
|
+
}
|
|
472
|
+
return connection;
|
|
473
|
+
});
|
|
465
474
|
// If options option is empty for all connection, log the missing options scope.
|
|
466
|
-
const isOptionExists =
|
|
475
|
+
const isOptionExists = dbConnectionsNormalized.every((c) => c.options && Object.keys(c.options).length > 0);
|
|
467
476
|
if (!isOptionExists) {
|
|
468
477
|
logger_1.default.warn(`Insufficient scope the read:connections_options scope is required to get ${this.type} options.`);
|
|
469
478
|
}
|
|
470
|
-
this.existing =
|
|
479
|
+
this.existing = dbConnectionsNormalized;
|
|
471
480
|
return this.existing;
|
|
472
481
|
}
|
|
473
482
|
async calcChanges(assets) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Management } from 'auth0';
|
|
2
2
|
import DefaultHandler from './default';
|
|
3
3
|
import { Asset, Assets, CalculatedChanges } from '../../../types';
|
|
4
|
+
import { Client } from './clients';
|
|
4
5
|
export declare const schema: {
|
|
5
6
|
type: string;
|
|
6
7
|
items: {
|
|
@@ -108,6 +109,25 @@ export declare const schema: {
|
|
|
108
109
|
type: string;
|
|
109
110
|
enum: ("allow" | "block")[];
|
|
110
111
|
};
|
|
112
|
+
is_app_entitlement_active: {
|
|
113
|
+
type: string;
|
|
114
|
+
};
|
|
115
|
+
clients: {
|
|
116
|
+
type: string;
|
|
117
|
+
items: {
|
|
118
|
+
type: string;
|
|
119
|
+
properties: {
|
|
120
|
+
client_id: {
|
|
121
|
+
type: string;
|
|
122
|
+
};
|
|
123
|
+
use_for_member_access: {
|
|
124
|
+
type: string;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
required: string[];
|
|
128
|
+
};
|
|
129
|
+
default: never[];
|
|
130
|
+
};
|
|
111
131
|
};
|
|
112
132
|
required: string[];
|
|
113
133
|
};
|
|
@@ -119,6 +139,7 @@ type FormattedClientGrants = {
|
|
|
119
139
|
export default class OrganizationsHandler extends DefaultHandler {
|
|
120
140
|
existing: Asset[];
|
|
121
141
|
formattedClientGrants: FormattedClientGrants[];
|
|
142
|
+
allClients: Client[];
|
|
122
143
|
constructor(config: DefaultHandler);
|
|
123
144
|
deleteOrganization(org: any): Promise<void>;
|
|
124
145
|
deleteOrganizations(data: Asset[]): Promise<void>;
|
|
@@ -128,6 +149,7 @@ export default class OrganizationsHandler extends DefaultHandler {
|
|
|
128
149
|
id: any;
|
|
129
150
|
}>;
|
|
130
151
|
getClientGrantIDByClientName(clientsName: string): string;
|
|
152
|
+
getClientIdByClientName(clientName: string): string;
|
|
131
153
|
getFormattedClientGrants(): Promise<FormattedClientGrants[]>;
|
|
132
154
|
updateOrganizations(updates: CalculatedChanges['update'], orgs: Asset[]): Promise<void>;
|
|
133
155
|
getType(): Promise<Asset[]>;
|
|
@@ -141,5 +163,11 @@ export default class OrganizationsHandler extends DefaultHandler {
|
|
|
141
163
|
createOrganizationDiscoveryDomain(organizationId: string, discoveryDomain: Management.CreateOrganizationDiscoveryDomainRequestContent): Promise<Management.CreateOrganizationDiscoveryDomainResponseContent>;
|
|
142
164
|
updateOrganizationDiscoveryDomain(organizationId: string, discoveryDomainId: string, discoveryDomain: string, discoveryDomainUpdate: Management.UpdateOrganizationDiscoveryDomainRequestContent): Promise<Management.UpdateOrganizationDiscoveryDomainResponseContent>;
|
|
143
165
|
deleteOrganizationDiscoveryDomain(organizationId: string, discoveryDomain: string, discoveryDomainId: string): Promise<void>;
|
|
166
|
+
getAllOrganizationClients(organizationId: string): Promise<Management.OrganizationClient[] | null>;
|
|
167
|
+
createOrganizationClients(organizationId: string, clients: Array<{
|
|
168
|
+
client_id: string;
|
|
169
|
+
use_for_member_access: boolean;
|
|
170
|
+
}>): Promise<void>;
|
|
171
|
+
deleteOrganizationClients(organizationId: string, clientIds: string[]): Promise<void>;
|
|
144
172
|
}
|
|
145
173
|
export {};
|
|
@@ -131,6 +131,19 @@ exports.schema = {
|
|
|
131
131
|
type: 'string',
|
|
132
132
|
enum: Object.values(auth0_1.Management.OrganizationThirdPartyClientAccessEnum),
|
|
133
133
|
},
|
|
134
|
+
is_app_entitlement_active: { type: 'boolean' },
|
|
135
|
+
clients: {
|
|
136
|
+
type: 'array',
|
|
137
|
+
items: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
client_id: { type: 'string' },
|
|
141
|
+
use_for_member_access: { type: 'boolean' },
|
|
142
|
+
},
|
|
143
|
+
required: ['client_id', 'use_for_member_access'],
|
|
144
|
+
},
|
|
145
|
+
default: [],
|
|
146
|
+
},
|
|
134
147
|
},
|
|
135
148
|
required: ['name'],
|
|
136
149
|
},
|
|
@@ -172,6 +185,7 @@ class OrganizationsHandler extends default_1.default {
|
|
|
172
185
|
const organization = { ...org };
|
|
173
186
|
delete organization.connections;
|
|
174
187
|
delete organization.client_grants;
|
|
188
|
+
delete organization.clients;
|
|
175
189
|
if ('discovery_domains' in organization) {
|
|
176
190
|
delete organization.discovery_domains;
|
|
177
191
|
}
|
|
@@ -195,6 +209,19 @@ class OrganizationsHandler extends default_1.default {
|
|
|
195
209
|
throw new Error(`Problem creating discovery domain ${discoveryDomain?.domain} for organization ${createdId}\n${err}`);
|
|
196
210
|
})));
|
|
197
211
|
}
|
|
212
|
+
if (typeof org.clients !== 'undefined' && org.clients.length > 0) {
|
|
213
|
+
const clientsToCreate = org.clients
|
|
214
|
+
.map((oc) => ({
|
|
215
|
+
client_id: this.getClientIdByClientName(oc.client_id),
|
|
216
|
+
use_for_member_access: oc.use_for_member_access,
|
|
217
|
+
}))
|
|
218
|
+
.filter((oc) => !!oc.client_id);
|
|
219
|
+
if (clientsToCreate.length > 0) {
|
|
220
|
+
await this.createOrganizationClients(createdId, clientsToCreate).catch((err) => {
|
|
221
|
+
throw new Error(`Problem creating org clients for organization ${createdId}\n${err}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
198
225
|
return created;
|
|
199
226
|
}
|
|
200
227
|
async createOrganizations(creates) {
|
|
@@ -213,14 +240,15 @@ class OrganizationsHandler extends default_1.default {
|
|
|
213
240
|
.promise();
|
|
214
241
|
}
|
|
215
242
|
async updateOrganization(org, organizations) {
|
|
216
|
-
const { connections: existingConnections, client_grants: existingClientGrants, discovery_domains: existingDiscoveryDomains, } = await organizations.find((orgToUpdate) => orgToUpdate.name === org.name);
|
|
243
|
+
const { connections: existingConnections, client_grants: existingClientGrants, discovery_domains: existingDiscoveryDomains, clients: existingOrgClients = [], } = await organizations.find((orgToUpdate) => orgToUpdate.name === org.name);
|
|
217
244
|
const params = { id: org.id };
|
|
218
|
-
const { connections, client_grants: organizationClientGrants, discovery_domains: organizationDiscoveryDomains, } = org;
|
|
245
|
+
const { connections, client_grants: organizationClientGrants, discovery_domains: organizationDiscoveryDomains, clients: organizationClients, } = org;
|
|
219
246
|
delete org.connections;
|
|
220
247
|
delete org.name;
|
|
221
248
|
delete org.id;
|
|
222
249
|
delete org.client_grants;
|
|
223
250
|
delete org.discovery_domains;
|
|
251
|
+
delete org.clients;
|
|
224
252
|
await this.client.organizations.update(params.id, org);
|
|
225
253
|
// organization connections
|
|
226
254
|
const connectionsToRemove = existingConnections.filter((c) => !connections.find((x) => x.connection_id === c.connection_id));
|
|
@@ -324,12 +352,61 @@ class OrganizationsHandler extends default_1.default {
|
|
|
324
352
|
\n${orgDiscoveryDomainsToRemove.map((i) => this.objString(i)).join('\n')}`);
|
|
325
353
|
}
|
|
326
354
|
}
|
|
355
|
+
// organization clients
|
|
356
|
+
const orgClientsToAdd = (organizationClients || [])
|
|
357
|
+
.filter((c) => !existingOrgClients.find((x) => x.client_id === c.client_id))
|
|
358
|
+
.map((oc) => ({
|
|
359
|
+
client_id: this.getClientIdByClientName(oc.client_id),
|
|
360
|
+
use_for_member_access: oc.use_for_member_access,
|
|
361
|
+
}))
|
|
362
|
+
.filter((oc) => !!oc.client_id);
|
|
363
|
+
const orgClientsToRemove = (existingOrgClients || [])
|
|
364
|
+
.filter((c) => !(organizationClients || []).find((x) => x.client_id === c.client_id))
|
|
365
|
+
.map((oc) => this.getClientIdByClientName(oc.client_id))
|
|
366
|
+
.filter(Boolean);
|
|
367
|
+
const orgClientsToUpdate = (organizationClients || [])
|
|
368
|
+
.filter((c) => {
|
|
369
|
+
const existing = existingOrgClients.find((x) => x.client_id === c.client_id);
|
|
370
|
+
return existing && existing.use_for_member_access !== c.use_for_member_access;
|
|
371
|
+
})
|
|
372
|
+
.map((oc) => ({
|
|
373
|
+
client_id: this.getClientIdByClientName(oc.client_id),
|
|
374
|
+
use_for_member_access: oc.use_for_member_access,
|
|
375
|
+
}))
|
|
376
|
+
.filter((oc) => !!oc.client_id);
|
|
377
|
+
if (orgClientsToAdd.length > 0) {
|
|
378
|
+
await this.createOrganizationClients(params.id, orgClientsToAdd).catch((err) => {
|
|
379
|
+
throw new Error(`Problem adding org clients for organization ${params.id}\n${err}`);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (orgClientsToRemove.length > 0) {
|
|
383
|
+
if (this.config('AUTH0_ALLOW_DELETE') === 'true' ||
|
|
384
|
+
this.config('AUTH0_ALLOW_DELETE') === true) {
|
|
385
|
+
await this.deleteOrganizationClients(params.id, orgClientsToRemove).catch((err) => {
|
|
386
|
+
throw new Error(`Problem removing org clients for organization ${params.id}\n${err}`);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
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
|
+
}
|
|
392
|
+
}
|
|
393
|
+
await Promise.all(orgClientsToUpdate.map((oc) => this.client.organizations.clients
|
|
394
|
+
.update(params.id, oc.client_id, {
|
|
395
|
+
use_for_member_access: oc.use_for_member_access,
|
|
396
|
+
})
|
|
397
|
+
.catch((err) => {
|
|
398
|
+
throw new Error(`Problem updating org client ${oc.client_id} for organization ${params.id}\n${err}`);
|
|
399
|
+
})));
|
|
327
400
|
return params;
|
|
328
401
|
}
|
|
329
402
|
getClientGrantIDByClientName(clientsName) {
|
|
330
403
|
const found = this.formattedClientGrants.find((c) => c.client_id === clientsName);
|
|
331
404
|
return found?.grant_id || '';
|
|
332
405
|
}
|
|
406
|
+
getClientIdByClientName(clientName) {
|
|
407
|
+
const found = this.allClients?.find((c) => c.name === clientName);
|
|
408
|
+
return found?.client_id || '';
|
|
409
|
+
}
|
|
333
410
|
async getFormattedClientGrants() {
|
|
334
411
|
const [clients, clientGrants] = await Promise.all([
|
|
335
412
|
(0, client_1.paginate)(this.client.clients.list, {
|
|
@@ -339,6 +416,8 @@ class OrganizationsHandler extends default_1.default {
|
|
|
339
416
|
paginate: true,
|
|
340
417
|
}),
|
|
341
418
|
]);
|
|
419
|
+
// Store clients for org-client name→ID resolution
|
|
420
|
+
this.allClients = clients;
|
|
342
421
|
// Convert clients by name to the id and store it in the formattedClientGrants
|
|
343
422
|
const formattedClientGrantsMapping = clientGrants?.map((clientGrant) => {
|
|
344
423
|
const { id, client_id: clientName } = clientGrant;
|
|
@@ -394,6 +473,14 @@ class OrganizationsHandler extends default_1.default {
|
|
|
394
473
|
if (organizationDiscoveryDomains) {
|
|
395
474
|
org.discovery_domains = organizationDiscoveryDomains;
|
|
396
475
|
}
|
|
476
|
+
// Get org-client associations
|
|
477
|
+
const orgClients = await this.getAllOrganizationClients(org.id);
|
|
478
|
+
if (orgClients) {
|
|
479
|
+
org.clients = orgClients.map((oc) => ({
|
|
480
|
+
client_id: (0, utils_1.convertClientIdToName)(oc.client_id, clients),
|
|
481
|
+
use_for_member_access: oc.use_for_member_access,
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
397
484
|
}
|
|
398
485
|
this.existing = organizations;
|
|
399
486
|
return this.existing;
|
|
@@ -539,6 +626,44 @@ class OrganizationsHandler extends default_1.default {
|
|
|
539
626
|
logger_1.default.debug(`Deleting discovery domain ${discoveryDomain} for organization ${organizationId}`);
|
|
540
627
|
await this.client.organizations.discoveryDomains.delete(organizationId, discoveryDomainId);
|
|
541
628
|
}
|
|
629
|
+
async getAllOrganizationClients(organizationId) {
|
|
630
|
+
const allOrgClients = [];
|
|
631
|
+
try {
|
|
632
|
+
let orgClients = await this.client.organizations.clients.list(organizationId);
|
|
633
|
+
allOrgClients.push(...orgClients.data);
|
|
634
|
+
while (orgClients.hasNextPage()) {
|
|
635
|
+
orgClients = await orgClients.getNextPage();
|
|
636
|
+
allOrgClients.push(...orgClients.data);
|
|
637
|
+
}
|
|
638
|
+
return allOrgClients;
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
if (err.statusCode === 404 || err.statusCode === 501) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
if (err.statusCode === 403 || err.errorCode === 'feature_not_enabled') {
|
|
645
|
+
logger_1.default.debug('Org-to-app entitlement is not enabled for this tenant. Skipping org-client associations.');
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async createOrganizationClients(organizationId, clients) {
|
|
652
|
+
const BATCH_SIZE = 10;
|
|
653
|
+
for (let i = 0; i < clients.length; i += BATCH_SIZE) {
|
|
654
|
+
const batch = clients.slice(i, i + BATCH_SIZE);
|
|
655
|
+
logger_1.default.debug(`Creating ${batch.length} org client(s) for organization ${organizationId}`);
|
|
656
|
+
await this.client.organizations.clients.create(organizationId, { clients: batch });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async deleteOrganizationClients(organizationId, clientIds) {
|
|
660
|
+
const BATCH_SIZE = 10;
|
|
661
|
+
for (let i = 0; i < clientIds.length; i += BATCH_SIZE) {
|
|
662
|
+
const batch = clientIds.slice(i, i + BATCH_SIZE);
|
|
663
|
+
logger_1.default.debug(`Deleting ${batch.length} org client(s) for organization ${organizationId}`);
|
|
664
|
+
await this.client.organizations.clients.delete(organizationId, { clients: batch });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
542
667
|
}
|
|
543
668
|
exports.default = OrganizationsHandler;
|
|
544
669
|
__decorate([
|
|
@@ -54,7 +54,10 @@ export default class PhoneTemplatesHandler extends DefaultHandler {
|
|
|
54
54
|
objString(template: any): string;
|
|
55
55
|
getType(): Promise<PhoneTemplate[]>;
|
|
56
56
|
processChanges(assets: Assets): Promise<void>;
|
|
57
|
-
createPhoneTemplate(template: any): Promise<
|
|
57
|
+
createPhoneTemplate(template: any): Promise<{
|
|
58
|
+
asset: Asset;
|
|
59
|
+
action: 'create' | 'update';
|
|
60
|
+
}>;
|
|
58
61
|
createPhoneTemplates(creates: CalculatedChanges['create']): Promise<void>;
|
|
59
62
|
updatePhoneTemplate(template: any): Promise<Asset>;
|
|
60
63
|
updatePhoneTemplates(updates: CalculatedChanges['update']): Promise<void>;
|
|
@@ -44,6 +44,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.schema = void 0;
|
|
46
46
|
const default_1 = __importStar(require("./default"));
|
|
47
|
+
const utils_1 = require("../../utils");
|
|
47
48
|
const logger_1 = __importDefault(require("../../../logger"));
|
|
48
49
|
exports.schema = {
|
|
49
50
|
type: 'array',
|
|
@@ -121,25 +122,77 @@ class PhoneTemplatesHandler extends default_1.default {
|
|
|
121
122
|
if (del.length > 0) {
|
|
122
123
|
await this.deletePhoneTemplates(del);
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
// On newly created tenants the phone templates returned by the API have no
|
|
126
|
+
// ID until they are explicitly created. calcChanges matches assets by `type`
|
|
127
|
+
// and routes them all to `update`, but PATCH requires an ID. Split out the
|
|
128
|
+
// templates whose existing counterpart has no ID yet and create them instead.
|
|
129
|
+
const missingId = [];
|
|
130
|
+
const hasId = [];
|
|
131
|
+
update.forEach((template) => {
|
|
132
|
+
const existing = this.existing?.find((t) => t.type === template.type);
|
|
133
|
+
if (existing?.id) {
|
|
134
|
+
hasId.push(template);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
missingId.push(template);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const toCreate = [...create, ...missingId];
|
|
141
|
+
if (toCreate.length > 0) {
|
|
142
|
+
await this.createPhoneTemplates(toCreate);
|
|
126
143
|
}
|
|
127
|
-
if (
|
|
128
|
-
await this.updatePhoneTemplates(
|
|
144
|
+
if (hasId.length > 0) {
|
|
145
|
+
await this.updatePhoneTemplates(hasId);
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
async createPhoneTemplate(template) {
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
// The create endpoint only accepts type/disabled/content; strip read-only
|
|
150
|
+
// fields (channel, customizable, tenant) that the API would reject.
|
|
151
|
+
const createPayload = (0, utils_1.stripFields)(template, this.stripCreateFields);
|
|
152
|
+
// The API rejects an empty `content.from`, which is what fresh tenants
|
|
153
|
+
// export. Drop it when blank (cloning `content`, which stripFields only
|
|
154
|
+
// shallow-copied, so the caller's original asset isn't mutated).
|
|
155
|
+
if (createPayload.content && !createPayload.content.from) {
|
|
156
|
+
createPayload.content = { ...createPayload.content };
|
|
157
|
+
delete createPayload.content.from;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const created = await this.client.branding.phone.templates.create(createPayload);
|
|
161
|
+
return { asset: created, action: 'create' };
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
// A 409 means the template already exists on the tenant (it was created
|
|
165
|
+
// between our list call and this create, or the list endpoint didn't
|
|
166
|
+
// surface its ID). Re-fetch to pick up the ID and fall back to an update.
|
|
167
|
+
if (err.statusCode === 409) {
|
|
168
|
+
logger_1.default.debug(`Phone template type '${template.type}' already exists, falling back to update`);
|
|
169
|
+
// Reassigns the shared `this.existing`; safe under concurrency since
|
|
170
|
+
// every re-fetch returns the same consistent list. If it still lacks an
|
|
171
|
+
// ID, updatePhoneTemplate warns and skips.
|
|
172
|
+
const response = await this.client.branding.phone.templates.list();
|
|
173
|
+
this.existing = response.templates ?? [];
|
|
174
|
+
const updated = await this.updatePhoneTemplate(template);
|
|
175
|
+
return { asset: updated, action: 'update' };
|
|
176
|
+
}
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
134
179
|
}
|
|
135
180
|
async createPhoneTemplates(creates) {
|
|
136
181
|
await this.client.pool
|
|
137
182
|
.addEachTask({
|
|
138
183
|
data: creates || [],
|
|
139
184
|
generator: (item) => this.createPhoneTemplate(item)
|
|
140
|
-
.then((
|
|
141
|
-
|
|
142
|
-
|
|
185
|
+
.then(({ asset, action }) => {
|
|
186
|
+
// A 409 fallback actually performed an update; count it as such so
|
|
187
|
+
// the import summary doesn't report a phantom "create".
|
|
188
|
+
if (action === 'update') {
|
|
189
|
+
this.didUpdate(asset);
|
|
190
|
+
this.updated += 1;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
this.didCreate(asset);
|
|
194
|
+
this.created += 1;
|
|
195
|
+
}
|
|
143
196
|
})
|
|
144
197
|
.catch((err) => {
|
|
145
198
|
throw new Error(`Problem creating ${this.type} ${this.objString(item)}\n${err}`);
|
|
@@ -160,7 +213,6 @@ class PhoneTemplatesHandler extends default_1.default {
|
|
|
160
213
|
logger_1.default.debug(`Stripping ${this.type} create-only fields ${JSON.stringify(stripUpdateFields)}`);
|
|
161
214
|
const updatePayload = {
|
|
162
215
|
content: {
|
|
163
|
-
from: template.content?.from,
|
|
164
216
|
body: {
|
|
165
217
|
text: template.content?.body?.text,
|
|
166
218
|
voice: template.content?.body?.voice,
|
|
@@ -168,6 +220,11 @@ class PhoneTemplatesHandler extends default_1.default {
|
|
|
168
220
|
},
|
|
169
221
|
disabled: template.disabled,
|
|
170
222
|
};
|
|
223
|
+
// `content.from` is optional but the API rejects an empty string, which is
|
|
224
|
+
// what fresh tenants export. Only include it when it has a value.
|
|
225
|
+
if (template.content?.from) {
|
|
226
|
+
updatePayload.content.from = template.content.from;
|
|
227
|
+
}
|
|
171
228
|
const updated = await this.client.branding.phone.templates.update(existing.id, updatePayload);
|
|
172
229
|
return updated;
|
|
173
230
|
}
|
|
@@ -56,6 +56,7 @@ exports.schema = {
|
|
|
56
56
|
name: { type: 'string' },
|
|
57
57
|
id: { type: 'string' },
|
|
58
58
|
description: { type: 'string' },
|
|
59
|
+
type: { type: 'string', enum: ['tenant', 'organization'] },
|
|
59
60
|
permissions: {
|
|
60
61
|
type: 'array',
|
|
61
62
|
items: {
|
|
@@ -81,6 +82,9 @@ class RolesHandler extends default_1.default {
|
|
|
81
82
|
async createRole(data) {
|
|
82
83
|
const role = { ...data };
|
|
83
84
|
delete role.permissions;
|
|
85
|
+
// deploy-cli only manages tenant-level roles, so `type` is stripped on write; it
|
|
86
|
+
// defaults to `tenant` on create. This keeps exports (which carry `type`) round-trippable.
|
|
87
|
+
delete role.type;
|
|
84
88
|
const created = await this.client.roles.create(role);
|
|
85
89
|
if (created.id && typeof data.permissions !== 'undefined' && data.permissions.length > 0) {
|
|
86
90
|
await this.client.roles.permissions.add(created.id, { permissions: data.permissions });
|
|
@@ -133,6 +137,9 @@ class RolesHandler extends default_1.default {
|
|
|
133
137
|
const newPermissions = data.permissions;
|
|
134
138
|
delete data.permissions;
|
|
135
139
|
delete data.id;
|
|
140
|
+
// deploy-cli only manages tenant-level roles, so `type` is stripped on write; this
|
|
141
|
+
// lets files that already carry it (from a prior export) be imported cleanly.
|
|
142
|
+
delete data.type;
|
|
136
143
|
await this.client.roles.update(params.id, data);
|
|
137
144
|
if (typeof existingRole.permissions !== 'undefined' && existingRole.permissions.length > 0) {
|
|
138
145
|
await this.client.roles.permissions.delete(params.id, {
|
|
@@ -164,9 +171,12 @@ class RolesHandler extends default_1.default {
|
|
|
164
171
|
return this.existing;
|
|
165
172
|
}
|
|
166
173
|
try {
|
|
174
|
+
// Only manage tenant-level roles; exclude org-scoped roles surfaced when
|
|
175
|
+
// the api2_org_level_roles_ea flag is enabled.
|
|
167
176
|
const roles = await (0, client_1.paginate)(this.client.roles.list, {
|
|
168
177
|
paginate: true,
|
|
169
178
|
include_totals: true,
|
|
179
|
+
type: 'tenant',
|
|
170
180
|
});
|
|
171
181
|
for (let index = 0; index < roles.length; index++) {
|
|
172
182
|
const allPermission = [];
|
package/lib/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auth0-deploy-cli",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.43.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.2.0",
|
|
39
39
|
"chalk": "5.6.2",
|
|
40
40
|
"dot-prop": "^5.3.0",
|
|
41
41
|
"fs-extra": "^10.1.0",
|