From d1894617f6712aadb78394da4b7eed627efff5dc Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Thu, 18 Dec 2025 08:45:42 +0530 Subject: [PATCH 1/7] feat: add directory provisioning support for connections - src/tools/auth0/handlers/connections.ts: add directory_provisioning_configuration to schema - src/tools/auth0/handlers/connections.ts: implement methods for retrieving, creating, updating, and deleting directory provisioning configurations - src/tools/auth0/handlers/connections.ts: process directory provisioning in connection changes --- src/tools/auth0/handlers/connections.ts | 233 +++++++++++++++++++++++- 1 file changed, 230 insertions(+), 3 deletions(-) diff --git a/src/tools/auth0/handlers/connections.ts b/src/tools/auth0/handlers/connections.ts index 5f9eca789..6ba3f28c3 100644 --- a/src/tools/auth0/handlers/connections.ts +++ b/src/tools/auth0/handlers/connections.ts @@ -52,12 +52,36 @@ export const schema = { required: ['active'], additionalProperties: false, }, + directory_provisioning_configuration: { + type: 'object', + properties: { + mapping: { + type: 'array', + items: { + type: 'object', + properties: { + auth0: { type: 'string', description: 'The field location in the Auth0 schema' }, + idp: { type: 'string', description: 'The field location in the IDP schema' }, + }, + }, + }, + synchronize_automatically: { + type: 'boolean', + description: 'The field whether periodic automatic synchronization is enabled', + }, + }, + }, }, required: ['name', 'strategy'], }, }; -export type Connection = Management.ConnectionForList; +type DirectoryProvisioningConfig = Management.GetDirectoryProvisioningResponseContent; + +export type Connection = Management.ConnectionForList & { + enabled_clients?: string[]; + directory_provisioning_configuration?: DirectoryProvisioningConfig; +}; // addExcludedConnectionPropertiesToChanges superimposes excluded properties on the `options` object. The Auth0 API // will overwrite the options property when updating connections, so it is necessary to add excluded properties back in to prevent those excluded properties from being deleted. @@ -305,6 +329,194 @@ export default class ConnectionsHandler extends DefaultAPIHandler { } } + /** + * Retrieves directory provisioning configuration for a specific Auth0 connection. + * @param connectionId - The unique identifier of the connection + * @returns A promise that resolves to the configuration object, or null if not configured/supported + */ + async getConnectionDirectoryProvisioning( + connectionId: string + ): Promise { + if (!connectionId) return null; + + const creates = [connectionId]; + let config: DirectoryProvisioningConfig | null = null; + + try { + await this.client.pool + .addEachTask({ + data: creates || [], + generator: async (id: string) => + this.client.connections.directoryProvisioning + .get(id) + .then((resp) => { + config = resp; + }) + .catch((err) => { + throw new Error(err); + }), + }) + .promise(); + + return config; + } catch (error) { + if (error.statusCode === 403) { + log.warn( + 'Directory Provisioning feature is not available on this tenant. Please contact Auth0 support to enable this feature.' + ); + } else { + log.error( + `Unable to fetch directory provisioning for connection '${connectionId}':`, + error + ); + } + return null; + } + } + + /** + * Creates directory provisioning configuration for a connection. + */ + private async createConnectionDirectoryProvisioning( + connectionId: string, + payload: Management.CreateDirectoryProvisioningRequestContent + ): Promise { + if (!connectionId) { + throw new Error('Connection ID is required to create directory provisioning configuration.'); + } + await this.client.connections.directoryProvisioning.create(connectionId, payload); + log.debug(`Created directory provisioning for ${this.type}: ${connectionId}`); + } + + /** + * Updates directory provisioning configuration for a connection. + */ + private async updateConnectionDirectoryProvisioning( + connectionId: string, + payload: Management.UpdateDirectoryProvisioningRequestContent + ): Promise { + if (!connectionId) { + throw new Error('Connection ID is required to update directory provisioning configuration.'); + } + await this.client.connections.directoryProvisioning.update(connectionId, payload); + log.debug(`Updated directory provisioning for ${this.type}: ${connectionId}`); + } + + /** + * Deletes directory provisioning configuration for a connection. + */ + private async deleteConnectionDirectoryProvisioning(connectionId: string): Promise { + if (!connectionId) { + throw new Error('Connection ID is required to delete directory provisioning configuration.'); + } + await this.client.connections.directoryProvisioning.delete(connectionId); + log.debug(`Deleted directory provisioning for ${this.type}: ${connectionId}`); + } + + /** + * This function processes directory provisioning for create, update, and conflict operations. + * Directory provisioning is only supported for google-apps strategy connections. + * + * @param changes - Object containing arrays of connections to create, update, and resolve conflicts for + */ + async processConnectionDirectoryProvisioning(changes: CalculatedChanges): Promise { + const { create, update, conflicts } = changes; + + // Build a map of existing connections by ID for quick lookup + const existingConnectionsMap = keyBy(this.existing || [], 'id'); + + // Filter to only google-apps connections that have directory_provisioning_configuration + const googleAppsWithDirProvFilter = (conn: Asset) => + conn.strategy === 'google-apps' && conn.directory_provisioning_configuration; + + const connectionsToProcess = [ + ...update.filter(googleAppsWithDirProvFilter), + ...create.filter(googleAppsWithDirProvFilter), + ...conflicts.filter(googleAppsWithDirProvFilter), + ]; + + if (connectionsToProcess.length === 0) { + return; + } + + const directoryConnectionsToUpdate: Connection[] = []; + const directoryConnectionsToCreate: Connection[] = []; + const directoryConnectionsToDelete: Connection[] = []; + + for (const conn of connectionsToProcess) { + if (!conn.id) continue; + + const existingConn = existingConnectionsMap[conn.id]; + const existingConfig = existingConn?.directory_provisioning_configuration; + const proposedConfig = conn.directory_provisioning_configuration; + + if (existingConfig && proposedConfig) { + directoryConnectionsToUpdate.push(conn); + } else if (!existingConfig && proposedConfig) { + directoryConnectionsToCreate.push(conn); + } else if (existingConfig && !proposedConfig) { + directoryConnectionsToDelete.push(conn); + } + // If neither exists, do nothing + } + + // Process updates first + await this.client.pool + .addEachTask({ + data: directoryConnectionsToUpdate || [], + generator: (conn) => + this.updateConnectionDirectoryProvisioning( + conn.id!, + conn.directory_provisioning_configuration! + ).catch((err) => { + throw new Error( + `Failed to update directory provisioning for connection '${conn.name}':\n${err}` + ); + }), + }) + .promise(); + + // Process creates + await this.client.pool + .addEachTask({ + data: directoryConnectionsToCreate || [], + generator: (conn) => + this.createConnectionDirectoryProvisioning( + conn.id!, + conn.directory_provisioning_configuration! + ).catch((err) => { + throw new Error( + `Failed to create directory provisioning for connection '${conn.name}':\n${err}` + ); + }), + }) + .promise(); + + // Process deletes + if ( + this.config('AUTH0_ALLOW_DELETE') === 'true' || + this.config('AUTH0_ALLOW_DELETE') === true + ) { + await this.client.pool + .addEachTask({ + data: directoryConnectionsToDelete || [], + generator: (conn) => + this.deleteConnectionDirectoryProvisioning(conn.id!).catch((err) => { + throw new Error( + `Failed to delete directory provisioning for connection '${conn.name}':\n${err}` + ); + }), + }) + .promise(); + } else if (directoryConnectionsToDelete.length) { + log.warn( + `Detected directory provisioning configs to delete. Deletes are disabled (set 'AUTH0_ALLOW_DELETE' to true to allow).\n${directoryConnectionsToDelete + .map((i) => this.objString(i)) + .join('\n')}` + ); + } + } + async getType(): Promise { if (this.existing) return this.existing; @@ -332,10 +544,22 @@ export default class ConnectionsHandler extends DefaultAPIHandler { filteredConnections.map(async (con) => { if (!con?.id) return con; const enabledClients = await getConnectionEnabledClients(this.client, con.id); + + // Cast to Asset to allow adding properties + let connection: Connection = { ...con }; + if (enabledClients && enabledClients?.length) { - return { ...con, enabled_clients: enabledClients }; + connection.enabled_clients = enabledClients; + } + + if (connection.strategy === 'google-apps') { + const dirProvConfig = await this.getConnectionDirectoryProvisioning(con.id); + if (dirProvConfig) { + connection.directory_provisioning_configuration = dirProvConfig; + } } - return con; + + return connection; }) ); @@ -417,5 +641,8 @@ export default class ConnectionsHandler extends DefaultAPIHandler { this.type, filterExcluded(changes, excludedConnections) ); + + // process directory provisioning + await this.processConnectionDirectoryProvisioning(filterExcluded(changes, excludedConnections)); } } From ed43dd5c8e953b17c4758f6bd53ac405e20fbbfc Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:42:18 +0530 Subject: [PATCH 2/7] feat: enhance error handling in connections handler - src/tools/auth0/handlers/connections.ts: throw ManagementError instead of generic Error for better error classification - src/tools/auth0/handlers/connections.ts: improve logging for directory provisioning fetch errors --- src/tools/auth0/handlers/connections.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/tools/auth0/handlers/connections.ts b/src/tools/auth0/handlers/connections.ts index 6ba3f28c3..8df8cdf4a 100644 --- a/src/tools/auth0/handlers/connections.ts +++ b/src/tools/auth0/handlers/connections.ts @@ -1,6 +1,6 @@ import dotProp from 'dot-prop'; import { chunk, keyBy } from 'lodash'; -import { Management } from 'auth0'; +import { Management, ManagementError } from 'auth0'; import DefaultAPIHandler, { order } from './default'; import { filterExcluded, convertClientNameToId, getEnabledClients, sleep } from '../../utils'; import { CalculatedChanges, Asset, Assets, Auth0APIClient } from '../../../types'; @@ -353,22 +353,19 @@ export default class ConnectionsHandler extends DefaultAPIHandler { config = resp; }) .catch((err) => { - throw new Error(err); + throw new ManagementError(err); }), }) .promise(); return config; } catch (error) { - if (error.statusCode === 403) { - log.warn( - 'Directory Provisioning feature is not available on this tenant. Please contact Auth0 support to enable this feature.' - ); + const errLog = `Unable to fetch directory provisioning for connection '${connectionId}'. `; + if (error instanceof ManagementError && error.statusCode === 403) { + const bodyMessage = (error.body as any)?.message; + log.warn(errLog + bodyMessage); } else { - log.error( - `Unable to fetch directory provisioning for connection '${connectionId}':`, - error - ); + log.error(errLog, error?.message); } return null; } From 83cae32ed607abbe35d75d5efd397187750446fb Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:07:55 +0530 Subject: [PATCH 3/7] feat: update error messages and clean up bodyParams for directory provisioning - src/tools/auth0/handlers/connections.ts: change error messages to use connection ID instead of name - src/tools/auth0/handlers/scimHandler.ts: remove directory_provisioning_configuration from bodyParams --- src/tools/auth0/handlers/connections.ts | 6 +++--- src/tools/auth0/handlers/scimHandler.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/tools/auth0/handlers/connections.ts b/src/tools/auth0/handlers/connections.ts index 8df8cdf4a..1800388c6 100644 --- a/src/tools/auth0/handlers/connections.ts +++ b/src/tools/auth0/handlers/connections.ts @@ -467,7 +467,7 @@ export default class ConnectionsHandler extends DefaultAPIHandler { conn.directory_provisioning_configuration! ).catch((err) => { throw new Error( - `Failed to update directory provisioning for connection '${conn.name}':\n${err}` + `Failed to update directory provisioning for connection '${conn.id}':\n${err}` ); }), }) @@ -483,7 +483,7 @@ export default class ConnectionsHandler extends DefaultAPIHandler { conn.directory_provisioning_configuration! ).catch((err) => { throw new Error( - `Failed to create directory provisioning for connection '${conn.name}':\n${err}` + `Failed to create directory provisioning for connection '${conn.id}':\n${err}` ); }), }) @@ -500,7 +500,7 @@ export default class ConnectionsHandler extends DefaultAPIHandler { generator: (conn) => this.deleteConnectionDirectoryProvisioning(conn.id!).catch((err) => { throw new Error( - `Failed to delete directory provisioning for connection '${conn.name}':\n${err}` + `Failed to delete directory provisioning for connection '${conn.id}':\n${err}` ); }), }) diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index 560488672..2c2148f8c 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -286,6 +286,7 @@ export default class ScimHandler { // Remove `scim_configuration` from `bodyParams`, because `connections.update` doesn't accept it. const { scim_configuration: scimBodyParams } = bodyParams; delete bodyParams.scim_configuration; + delete bodyParams.directory_provisioning_configuration; // First, update `connections`. const updated = await this.connectionsManager.update(requestParams.id, bodyParams); From d7c243e0efbd7d7144e877a638f2e5e168625270 Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Fri, 19 Dec 2025 13:40:39 +0530 Subject: [PATCH 4/7] feat: add Google Workspace directory provisioning support - docs/resource-specific-documentation.md: document directory provisioning for Google Workspace connections - examples/directory/connections/google-apps.json: add example configuration for Google Workspace directory provisioning - examples/yaml/tenant.yaml: include Google Workspace connection in tenant YAML configuration - src/tools/auth0/handlers/connections.ts: implement directory provisioning create, update, and delete operations - test/tools/auth0/handlers/connections.tests.js: add tests for directory provisioning configuration handling - test/tools/auth0/handlers/scimHandler.tests.js: ensure directory provisioning configuration is removed before updating connection --- docs/resource-specific-documentation.md | 53 +++++ .../directory/connections/google-apps.json | 20 ++ examples/yaml/tenant.yaml | 17 ++ src/tools/auth0/handlers/connections.ts | 32 ++- .../tools/auth0/handlers/connections.tests.js | 195 ++++++++++++++++++ .../tools/auth0/handlers/scimHandler.tests.js | 25 +++ 6 files changed, 336 insertions(+), 6 deletions(-) create mode 100644 examples/directory/connections/google-apps.json diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index b99cda260..9388e17e9 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -121,6 +121,59 @@ Contents of `promptName_screenName.json` } ``` +## Connections (Google Workspace directory provisioning) + +The Deploy CLI supports managing the `directory_provisioning_configuration` for Google Workspace (`google-apps`) connections. Only `google-apps` connections are processed for directory provisioning; other strategies will ignore this block. Deleting directory provisioning requires `AUTH0_ALLOW_DELETE=true`. + +The `mapping` array pairs Auth0 user fields with IdP fields, and `synchronize_automatically` controls whether Auth0 runs scheduled sync jobs for the connection. + +**YAML Example** + +```yaml +connections: + - name: google-workspace + strategy: google-apps + options: + domain: example.com + tenant_domain: example.com + client_id: 'some_client_id' + client_secret: 'some_client_secret' + directory_provisioning_configuration: + mapping: + - auth0: email + idp: mail + - auth0: name + idp: displayName + synchronize_automatically: true +``` + +**Directory Example** + +``` +./connections/google-apps-directory-provisioning.json +``` + +```json +{ + "name": "google-apps-directory-provisioning", + "strategy": "google-apps", + "enabled_clients": ["My SPA"], + "options": { + "domain": "example.com", + "tenant_domain": "example.com", + "client_id": "some_client_id", + "client_secret": "some_client_secret" + }, + "directory_provisioning_configuration": { + "mapping": [ + { "auth0": "email", "idp": "mail" }, + { "auth0": "name", "idp": "displayName" } + ], + "synchronize_automatically": true + } +} +``` + ## Databases When managing database connections, the values of `options.customScripts` point to specific javascript files relative to diff --git a/examples/directory/connections/google-apps.json b/examples/directory/connections/google-apps.json new file mode 100644 index 000000000..06ea555bc --- /dev/null +++ b/examples/directory/connections/google-apps.json @@ -0,0 +1,20 @@ +{ + "name": "google-apps-directory-provisioning", + "strategy": "google-apps", + "enabled_clients": [ + "My SPA" + ], + "options": { + "domain": "example.com", + "tenant_domain": "example.com", + "client_id": "some_client_id", + "client_secret": "some_client_secret" + }, + "directory_provisioning_configuration": { + "mapping": [ + { "auth0": "email", "idp": "mail" }, + { "auth0": "name", "idp": "displayName" } + ], + "synchronize_automatically": true + } +} diff --git a/examples/yaml/tenant.yaml b/examples/yaml/tenant.yaml index 71ace6219..10e66ff98 100644 --- a/examples/yaml/tenant.yaml +++ b/examples/yaml/tenant.yaml @@ -111,6 +111,23 @@ connections: ext_groups: true # Add other connection settings (https://auth0.com/docs/api/management/v2#!/Connections/post_connections) + - name: "google-workspace" + strategy: "google-apps" + enabled_clients: + - "My SPA" + options: + domain: "example.com" + tenant_domain: "example.com" + client_id: 'some_client_id' + client_secret: 'some_client_secret' + directory_provisioning_configuration: + mapping: + - auth0: "email" + idp: "mail" + - auth0: "name" + idp: "displayName" + synchronize_automatically: true + resourceServers: - diff --git a/src/tools/auth0/handlers/connections.ts b/src/tools/auth0/handlers/connections.ts index 1800388c6..c21a435e4 100644 --- a/src/tools/auth0/handlers/connections.ts +++ b/src/tools/auth0/handlers/connections.ts @@ -358,6 +358,20 @@ export default class ConnectionsHandler extends DefaultAPIHandler { }) .promise(); + const stripKeysFromOutput = [ + 'connection_id', + 'connection_name', + 'strategy', + 'created_at', + 'updated_at', + ]; + + stripKeysFromOutput.forEach((key) => { + if (config && key in config) { + delete (config as Partial)[key]; + } + }); + return config; } catch (error) { const errLog = `Unable to fetch directory provisioning for connection '${connectionId}'. `; @@ -381,8 +395,11 @@ export default class ConnectionsHandler extends DefaultAPIHandler { if (!connectionId) { throw new Error('Connection ID is required to create directory provisioning configuration.'); } - await this.client.connections.directoryProvisioning.create(connectionId, payload); - log.debug(`Created directory provisioning for ${this.type}: ${connectionId}`); + const createPayload: Management.CreateDirectoryProvisioningRequestContent = { + mapping: payload.mapping, + synchronize_automatically: payload.synchronize_automatically, + }; + await this.client.connections.directoryProvisioning.create(connectionId, createPayload); } /** @@ -395,8 +412,13 @@ export default class ConnectionsHandler extends DefaultAPIHandler { if (!connectionId) { throw new Error('Connection ID is required to update directory provisioning configuration.'); } - await this.client.connections.directoryProvisioning.update(connectionId, payload); - log.debug(`Updated directory provisioning for ${this.type}: ${connectionId}`); + + const updatePayload: Management.UpdateDirectoryProvisioningRequestContent = { + mapping: payload.mapping, + synchronize_automatically: payload.synchronize_automatically, + }; + + await this.client.connections.directoryProvisioning.update(connectionId, updatePayload); } /** @@ -407,7 +429,6 @@ export default class ConnectionsHandler extends DefaultAPIHandler { throw new Error('Connection ID is required to delete directory provisioning configuration.'); } await this.client.connections.directoryProvisioning.delete(connectionId); - log.debug(`Deleted directory provisioning for ${this.type}: ${connectionId}`); } /** @@ -454,7 +475,6 @@ export default class ConnectionsHandler extends DefaultAPIHandler { } else if (existingConfig && !proposedConfig) { directoryConnectionsToDelete.push(conn); } - // If neither exists, do nothing } // Process updates first diff --git a/test/tools/auth0/handlers/connections.tests.js b/test/tools/auth0/handlers/connections.tests.js index b010be25c..03f8cfd2d 100644 --- a/test/tools/auth0/handlers/connections.tests.js +++ b/test/tools/auth0/handlers/connections.tests.js @@ -155,6 +155,34 @@ describe('#connections handler', () => { expect(getEnabledClientsCalledOnce).to.equal(true); }); + it('should include directory provisioning configuration for google-apps connections', async () => { + const auth0 = { + connections: { + list: (params) => + mockPagedData(params, 'connections', [ + { id: 'con1', strategy: 'google-apps', name: 'gsuite', options: {} }, + ]), + }, + clients: { + list: (params) => mockPagedData(params, 'clients', []), + }, + pool, + }; + + const handler = new connections.default({ client: pageClient(auth0), config }); + sinon.stub(connections, 'getConnectionEnabledClients').resolves(undefined); + const dirProvConfig = { mapping: [{ auth0: 'email', idp: 'mail' }] }; + sinon.stub(handler, 'getConnectionDirectoryProvisioning').resolves(dirProvConfig); + handler.scimHandler.applyScimConfiguration = sinon.stub().resolves(); + + const data = await handler.getType(); + + expect(handler.getConnectionDirectoryProvisioning.calledOnceWith('con1')).to.be.true; + expect(data[0]) + .to.have.property('directory_provisioning_configuration') + .that.deep.equals(dirProvConfig); + }); + it('should update connection', async () => { const auth0 = { connections: { @@ -316,6 +344,173 @@ describe('#connections handler', () => { await stageFn.apply(handler, [{ connections: data }]); }); + it('should process directory provisioning create and update operations', async () => { + const poolExecutor = { + addEachTask: ({ data, generator }) => ({ + promise: async () => { + // run all tasks sequentially to preserve order + for (const item of data || []) { + await generator(item); + } + }, + }), + }; + + const auth0 = { + connections: { + directoryProvisioning: {}, + }, + clients: { + list: (params) => mockPagedData(params, 'clients', []), + }, + pool: poolExecutor, + }; + + const handler = new connections.default({ client: pageClient(auth0), config }); + handler.existing = [ + { + id: 'con1', + name: 'gsuite-existing', + strategy: 'google-apps', + directory_provisioning_configuration: { mapping: [{ auth0: 'email', idp: 'mail' }] }, + }, + ]; + + const updateStub = sinon + .stub(handler, 'updateConnectionDirectoryProvisioning') + .resolves(undefined); + const createStub = sinon + .stub(handler, 'createConnectionDirectoryProvisioning') + .resolves(undefined); + const deleteStub = sinon + .stub(handler, 'deleteConnectionDirectoryProvisioning') + .resolves(undefined); + + await handler.processConnectionDirectoryProvisioning({ + create: [ + { + id: 'con2', + name: 'gsuite-new', + strategy: 'google-apps', + directory_provisioning_configuration: { + mapping: [{ auth0: 'name', idp: 'displayName' }], + }, + }, + ], + update: [ + { + id: 'con1', + name: 'gsuite-existing', + strategy: 'google-apps', + directory_provisioning_configuration: { mapping: [{ auth0: 'email', idp: 'mail' }] }, + }, + ], + conflicts: [], + del: [], + }); + + expect(updateStub.calledOnceWith('con1', sinon.match.object)).to.be.true; + expect(createStub.calledOnceWith('con2', sinon.match.object)).to.be.true; + expect(deleteStub.called).to.be.false; + }); + + describe('directory provisioning helpers', () => { + const sampleMapping = [ + { idp: 'id', auth0: 'external_id' }, + { idp: 'primaryEmail', auth0: 'email' }, + { idp: 'name.givenName', auth0: 'given_name' }, + { idp: 'name.familyName', auth0: 'family_name' }, + ]; + + it('should create directory provisioning configuration (POST)', async () => { + const createStub = sinon.stub().resolves(); + const auth0 = { + connections: { + directoryProvisioning: { + create: createStub, + }, + }, + pool, + }; + + const handler = new connections.default({ client: pageClient(auth0), config }); + await handler.createConnectionDirectoryProvisioning('con-post', { + mapping: sampleMapping, + }); + + expect(createStub.calledOnceWith('con-post', { mapping: sampleMapping })).to.be.true; + }); + + it('should retrieve directory provisioning configuration (GET)', async () => { + const dirProvConfig = { mapping: sampleMapping, synchronize_automatically: true }; + const getStub = sinon.stub().resolves(dirProvConfig); + const poolExecutor = { + addEachTask: ({ data, generator }) => ({ + promise: async () => { + for (const item of data || []) { + await generator(item); + } + }, + }), + }; + + const auth0 = { + connections: { + directoryProvisioning: { + get: getStub, + }, + }, + pool: poolExecutor, + }; + + const handler = new connections.default({ client: pageClient(auth0), config }); + const result = await handler.getConnectionDirectoryProvisioning('con-get'); + + expect(getStub.calledOnceWith('con-get')).to.be.true; + expect(result).to.deep.equal(dirProvConfig); + }); + + it('should update directory provisioning configuration (PATCH)', async () => { + const updateStub = sinon.stub().resolves(); + const auth0 = { + connections: { + directoryProvisioning: { + update: updateStub, + }, + }, + }; + + const handler = new connections.default({ client: pageClient(auth0), config }); + await handler.updateConnectionDirectoryProvisioning('con-patch', { + mapping: sampleMapping, + synchronize_automatically: false, + }); + + expect( + updateStub.calledOnceWith('con-patch', { + mapping: sampleMapping, + synchronize_automatically: false, + }) + ).to.be.true; + }); + + it('should delete directory provisioning configuration (DELETE)', async () => { + const deleteStub = sinon.stub().resolves(); + const auth0 = { + connections: { + directoryProvisioning: { + delete: deleteStub, + }, + }, + }; + + const handler = new connections.default({ client: pageClient(auth0), config }); + await handler.deleteConnectionDirectoryProvisioning('con-del'); + + expect(deleteStub.calledOnceWith('con-del')).to.be.true; + }); + }); + it('should keep client ID in idpinitiated.client_id', async () => { const auth0 = { connections: { diff --git a/test/tools/auth0/handlers/scimHandler.tests.js b/test/tools/auth0/handlers/scimHandler.tests.js index 5e1cbb22f..1338365c9 100644 --- a/test/tools/auth0/handlers/scimHandler.tests.js +++ b/test/tools/auth0/handlers/scimHandler.tests.js @@ -250,6 +250,31 @@ describe('ScimHandler', () => { expect(handler.updateScimConfiguration.calledOnce).to.be.true; }); + it('should remove directory provisioning configuration before updating connection', async () => { + const requestParams = { id: 'con_dirprov' }; + const bodyParams = { + name: 'google-apps-connection', + scim_configuration: { mapping: [], user_id_attribute: 'id' }, + directory_provisioning_configuration: { mapping: [{ auth0: 'email', idp: 'mail' }] }, + }; + + handler.idMap.set('con_dirprov', { + strategy: 'samlp', + scimConfiguration: bodyParams.scim_configuration, + }); + + mockConnectionsManager.update.resolves({ id: 'con_dirprov' }); + handler.updateScimConfiguration = sinon.stub().resolves({ connection_id: 'con_dirprov' }); + + await handler.updateOverride(requestParams, { ...bodyParams }); + + const [, updateBody] = mockConnectionsManager.update.firstCall.args; + expect(updateBody).to.not.have.property('directory_provisioning_configuration'); + expect(updateBody).to.not.have.property('scim_configuration'); + expect(updateBody).to.have.property('name', 'google-apps-connection'); + expect(handler.updateScimConfiguration.calledOnce).to.be.true; + }); + it('should create SCIM configuration when updating connection during updateOverride', async () => { const requestParams = { id: 'con_kzpLY0Afi4I8lvwM' }; const bodyParams = { scim_configuration: { mapping: [], user_id_attribute: 'id' } }; From dbeeacb81b50e3e6f2fa557afa8f2eda1b96ba47 Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Fri, 19 Dec 2025 13:59:53 +0530 Subject: [PATCH 5/7] feat: enhance directory provisioning handling and tests - src/tools/auth0/handlers/connections.ts: improve error handling for directory provisioning and add debug logs for create, update, and delete operations - test/tools/auth0/handlers/connections.tests.js: update test to use sinon.match for more flexible assertions --- src/tools/auth0/handlers/connections.ts | 10 ++++++---- test/tools/auth0/handlers/connections.tests.js | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/tools/auth0/handlers/connections.ts b/src/tools/auth0/handlers/connections.ts index c21a435e4..04b0fc44a 100644 --- a/src/tools/auth0/handlers/connections.ts +++ b/src/tools/auth0/handlers/connections.ts @@ -375,7 +375,7 @@ export default class ConnectionsHandler extends DefaultAPIHandler { return config; } catch (error) { const errLog = `Unable to fetch directory provisioning for connection '${connectionId}'. `; - if (error instanceof ManagementError && error.statusCode === 403) { + if (error instanceof ManagementError) { const bodyMessage = (error.body as any)?.message; log.warn(errLog + bodyMessage); } else { @@ -400,6 +400,7 @@ export default class ConnectionsHandler extends DefaultAPIHandler { synchronize_automatically: payload.synchronize_automatically, }; await this.client.connections.directoryProvisioning.create(connectionId, createPayload); + log.debug(`Created directory provisioning for connection '${connectionId}'`); } /** @@ -419,6 +420,7 @@ export default class ConnectionsHandler extends DefaultAPIHandler { }; await this.client.connections.directoryProvisioning.update(connectionId, updatePayload); + log.debug(`Updated directory provisioning for connection '${connectionId}'`); } /** @@ -429,6 +431,7 @@ export default class ConnectionsHandler extends DefaultAPIHandler { throw new Error('Connection ID is required to delete directory provisioning configuration.'); } await this.client.connections.directoryProvisioning.delete(connectionId); + log.debug(`Deleted directory provisioning for connection '${connectionId}'`); } /** @@ -443,9 +446,8 @@ export default class ConnectionsHandler extends DefaultAPIHandler { // Build a map of existing connections by ID for quick lookup const existingConnectionsMap = keyBy(this.existing || [], 'id'); - // Filter to only google-apps connections that have directory_provisioning_configuration - const googleAppsWithDirProvFilter = (conn: Asset) => - conn.strategy === 'google-apps' && conn.directory_provisioning_configuration; + // Filter to only google-apps connections + const googleAppsWithDirProvFilter = (conn: Asset) => conn.strategy === 'google-apps'; const connectionsToProcess = [ ...update.filter(googleAppsWithDirProvFilter), diff --git a/test/tools/auth0/handlers/connections.tests.js b/test/tools/auth0/handlers/connections.tests.js index 03f8cfd2d..7b5d50b7e 100644 --- a/test/tools/auth0/handlers/connections.tests.js +++ b/test/tools/auth0/handlers/connections.tests.js @@ -438,7 +438,8 @@ describe('#connections handler', () => { mapping: sampleMapping, }); - expect(createStub.calledOnceWith('con-post', { mapping: sampleMapping })).to.be.true; + expect(createStub.calledOnceWith('con-post', sinon.match.has('mapping', sampleMapping))).to + .be.true; }); it('should retrieve directory provisioning configuration (GET)', async () => { From 3bbb5204497ad894902f7fd50a7f52ef6b6dd2ba Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:52:05 +0530 Subject: [PATCH 6/7] feat: update SCIM handler to remove unnecessary configuration - src/tools/auth0/handlers/scimHandler.ts: delete from bodyParams --- src/tools/auth0/handlers/scimHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index 2c2148f8c..2de82d8b4 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -324,6 +324,7 @@ export default class ScimHandler { // Remove `scim_configuration` from `bodyParams`, because `connections.create` doesn't accept it. const { scim_configuration: scimBodyParams } = bodyParams; delete bodyParams.scim_configuration; + delete bodyParams.directory_provisioning_configuration; // First, create the new `connection`. const data = await this.connectionsManager.create( From cd43527247702308231fcd148657cb3b7579e6c8 Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:03:38 +0530 Subject: [PATCH 7/7] feat: update directory provisioning configuration - docs/resource-specific-documentation.md: set synchronize_automatically to false - examples/directory/connections/google-apps.json: set synchronize_automatically to false - examples/yaml/tenant.yaml: set synchronize_automatically to false - examples/yaml/tenant.yaml: add api_enable_users field - examples/directory/connections/google-apps.json: add api_enable_users field - docs/resource-specific-documentation.md: add api_enable_users field --- docs/resource-specific-documentation.md | 8 +++++--- examples/directory/connections/google-apps.json | 5 +++-- examples/yaml/tenant.yaml | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index 6cf9d6a3c..0a399a365 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -138,13 +138,14 @@ connections: tenant_domain: example.com client_id: 'some_client_id' client_secret: 'some_client_secret' + api_enable_users: true directory_provisioning_configuration: mapping: - auth0: email idp: mail - auth0: name idp: displayName - synchronize_automatically: true + synchronize_automatically: false ``` **Directory Example** @@ -162,14 +163,15 @@ connections: "domain": "example.com", "tenant_domain": "example.com", "client_id": "some_client_id", - "client_secret": "some_client_secret" + "client_secret": "some_client_secret", + "api_enable_users": true }, "directory_provisioning_configuration": { "mapping": [ { "auth0": "email", "idp": "mail" }, { "auth0": "name", "idp": "displayName" } ], - "synchronize_automatically": true + "synchronize_automatically": false } } ``` diff --git a/examples/directory/connections/google-apps.json b/examples/directory/connections/google-apps.json index 06ea555bc..9bc0cbd47 100644 --- a/examples/directory/connections/google-apps.json +++ b/examples/directory/connections/google-apps.json @@ -8,13 +8,14 @@ "domain": "example.com", "tenant_domain": "example.com", "client_id": "some_client_id", - "client_secret": "some_client_secret" + "client_secret": "some_client_secret", + "api_enable_users": true }, "directory_provisioning_configuration": { "mapping": [ { "auth0": "email", "idp": "mail" }, { "auth0": "name", "idp": "displayName" } ], - "synchronize_automatically": true + "synchronize_automatically": false } } diff --git a/examples/yaml/tenant.yaml b/examples/yaml/tenant.yaml index 49c64015f..d74af510f 100644 --- a/examples/yaml/tenant.yaml +++ b/examples/yaml/tenant.yaml @@ -120,13 +120,14 @@ connections: tenant_domain: "example.com" client_id: 'some_client_id' client_secret: 'some_client_secret' + api_enable_users: true directory_provisioning_configuration: mapping: - auth0: "email" idp: "mail" - auth0: "name" idp: "displayName" - synchronize_automatically: true + synchronize_automatically: false resourceServers: