diff --git a/src/tools/auth0/handlers/connections.ts b/src/tools/auth0/handlers/connections.ts index bef7832e1..10b2587f7 100644 --- a/src/tools/auth0/handlers/connections.ts +++ b/src/tools/auth0/handlers/connections.ts @@ -4,6 +4,7 @@ import DefaultAPIHandler, { order } from './default'; import { filterExcluded, convertClientNameToId, getEnabledClients } from '../../utils'; import { CalculatedChanges, Asset, Assets } from '../../../types'; import { ConfigFunction } from '../../../configFactory'; +import ScimHandler from './scimHandler'; export const schema = { type: 'array', @@ -16,6 +17,15 @@ export const schema = { enabled_clients: { type: 'array', items: { type: 'string' } }, realms: { type: 'array', items: { type: 'string' } }, metadata: { type: 'object' }, + scim_configuration: { + type: 'object', + properties: { + connection_name: { type: 'string' }, + mapping: { type: 'array', items: { type: 'object', properties: { scim: { type: 'string' }, auth0: { type: 'string' } } } }, + user_id_attribute: { type: 'string' } + }, + required: ['mapping', 'user_id_attribute'], + } }, required: ['name', 'strategy'], }, @@ -79,13 +89,26 @@ export const addExcludedConnectionPropertiesToChanges = ({ export default class ConnectionsHandler extends DefaultAPIHandler { existing: Asset[] | null; + scimHandler: ScimHandler; constructor(config: DefaultAPIHandler) { super({ ...config, type: 'connections', stripUpdateFields: ['strategy', 'name'], + functions: { + // When `connections` is updated, it can result in `update`,`create` or `delete` action on SCIM. + // Because, `scim_configuration` is inside `connections`. + update: async (requestParams, bodyParams) => await this.scimHandler.updateOverride(requestParams, bodyParams), + + // When a new `connection` is created. We can perform only `create` option on SCIM. + // When a connection is `deleted`. `scim_configuration` is also deleted along with it; no action on SCIM is required. + create: async (bodyParams) => await this.scimHandler.createOverride(bodyParams) + }, }); + + // @ts-ignore + this.scimHandler = new ScimHandler(this.config, this.client.tokenProvider, this.client.connections); } objString(connection): string { @@ -114,9 +137,14 @@ export default class ConnectionsHandler extends DefaultAPIHandler { paginate: true, include_totals: true, }); + // Filter out database connections this.existing = connections.filter((c) => c.strategy !== 'auth0'); if (this.existing === null) return []; + + // Apply `scim_configuration` to all the relevant `SCIM` connections. This method mutates `this.existing`. + await this.scimHandler.applyScimConfiguration(this.existing); + return this.existing; } @@ -138,6 +166,10 @@ export default class ConnectionsHandler extends DefaultAPIHandler { paginate: true, include_totals: true, }); + + // Prepare an id map. We'll use this map later to get the `strategy` and SCIM enable status of the connections. + await this.scimHandler.createIdMap(existingConnections); + const formatted = connections.map((connection) => ({ ...connection, ...this.getFormattedOptions(connection, clients), diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts new file mode 100644 index 000000000..064b1168a --- /dev/null +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -0,0 +1,233 @@ +import { Asset } from '../../../types'; +import axios, { AxiosResponse } from 'axios'; +import log from '../../../logger'; +import { sleep } from '../../utils'; + +interface IdMapValue { + strategy: string; + hasConfig: boolean; +} + +interface scimRequestParams { + id: string; +} + +interface scimBodyParams { + user_id_attribute: string; + mapping: { scim: string; auth0: string; }[]; +} + +/** + * The current version of this sdk use `node-auth0` v3. But `SCIM` features are not natively supported by v3. + * This is a workaround to make this SDK support SCIM without `node-auth0` upgrade. + */ +export default class ScimHandler { + private idMap: Map; + private readonly scimStrategies = ['samlp', 'oidc', 'okta', 'waad']; + private tokenProvider: any; + private config: any; + private connectionsManager: any; + + constructor(config, tokenProvider, connectionsManager) { + this.config = config; + this.tokenProvider = tokenProvider; + this.connectionsManager = connectionsManager; + this.idMap = new Map(); + } + + /** + * Check if the connection strategy is SCIM supported. + * Only few of the enterprise connections are SCIM supported. + */ + isScimStrategy(strategy: string) { + return this.scimStrategies.includes(strategy.toLowerCase()); + } + + /** + * Creates connection_id -> { strategy, hasConfig } map. + * Store only the SCIM ids available on the existing / remote config. + * Payload received on `create` and `update` methods has the property `strategy` stripped. + * So, we need this map to perform `create`, `update` or `delete` actions on SCIM. + * @param connections + */ + async createIdMap(connections: Asset[]) { + this.idMap.clear(); + + for (let connection of connections) { + if (!this.isScimStrategy(connection.strategy)) continue; + + try { + this.idMap.set(connection.id, { strategy: connection.strategy, hasConfig: false }); + await this.getScimConfiguration({ id: connection.id }); + this.idMap.set(connection.id, { ...this.idMap.get(connection.id)!, hasConfig: true }); + + // To avoid rate limiter error, we making API requests with a small delay. + // TODO: However, this logic needs to be re-worked. + await sleep(500); + } catch (err) { + // Skip the connection if it returns 404. This can happen if `SCIM` is not enabled on a `SCIM` connection. + if (err !== 'SCIM_NOT_FOUND') throw err; + } + } + } + + /** + * Iterate through all the connections and add property `scim_configuration` to only `SCIM` connections. + * The following conditions should be met to have `scim_configuration` set to a `connection`. + * 1. Connection `strategy` should be one of `scimStrategies` + * 2. Connection should have `SCIM` enabled. + * + * This method mutates the incoming `connections`. + */ + async applyScimConfiguration(connections: Asset[]) { + for (let connection of connections) { + if (!this.isScimStrategy(connection.strategy)) continue; + + try { + const { user_id_attribute, mapping } = await this.getScimConfiguration({ id: connection.id }); + connection.scim_configuration = { user_id_attribute, mapping } + + // To avoid rate limiter error, we making API requests with a small delay. + // TODO: However, this logic needs to be re-worked. + await sleep(500); + } catch (err) { + // Skip the connection if it returns 404. This can happen if `SCIM` is not enabled on a `SCIM` connection. + if (err !== 'SCIM_NOT_FOUND') throw err; + + const warningMessage = `SCIM configuration not found on connection \"${connection.id}\".`; + log.warn(warningMessage); + } + } + } + + /** + * HTTP request wrapper on axios. + */ + private async scimHttpRequest(method: string, options: [string, ...Record[]]): Promise { + return await this.withErrorHandling(async () => { + // @ts-ignore + const accessToken = await this.tokenProvider?.getAccessToken(); + const headers = { + 'Accept': 'application/json', + 'Authorization': `Bearer ${ accessToken }` + } + options = [...options, { headers }]; + + return await axios[method](...options); + }); + } + + /** + * Error handler wrapper. + */ + async withErrorHandling(callback) { + try { + return await callback(); + } catch (error) { + const errorData = error?.response?.data; + if (errorData?.statusCode === 404) throw "SCIM_NOT_FOUND"; + + const statusCode = errorData?.statusCode || error?.response?.status; + const errorCode = errorData?.errorCode || errorData?.error || error?.response?.statusText; + const errorMessage = errorData?.message || error?.response?.statusText; + const message = `SCIM request failed with statusCode ${ statusCode } (${ errorCode }). ${ errorMessage }.`; + + log.error(message); + throw error; + } + } + + /** + * Returns formatted endpoint url. + */ + private getScimEndpoint(connection_id: string) { + // Call `scim-configuration` endpoint directly to support `SCIM` features. + return `https://${ this.config('AUTH0_DOMAIN') }/api/v2/connections/${ connection_id }/scim-configuration`; + } + + /** + * Creates a new `SCIM` configuration. + */ + async createScimConfiguration({ id: connection_id }: scimRequestParams, { user_id_attribute, mapping }: scimBodyParams): Promise { + log.debug(`Creating SCIM configuration for connection ${ connection_id }`); + const url = this.getScimEndpoint(connection_id); + return (await this.scimHttpRequest('post', [ url, { user_id_attribute, mapping } ])).data; + } + + /** + * Retrieves `SCIM` configuration of an enterprise connection. + */ + async getScimConfiguration({ id: connection_id }: scimRequestParams): Promise { + log.debug(`Getting SCIM configuration from connection ${ connection_id }`); + const url = this.getScimEndpoint(connection_id); + return (await this.scimHttpRequest('get', [ url ])).data; + } + + /** + * Updates an existing `SCIM` configuration. + */ + async updateScimConfiguration({ id: connection_id }: scimRequestParams, { user_id_attribute, mapping }: scimBodyParams): Promise { + log.debug(`Updating SCIM configuration on connection ${ connection_id }`); + const url = this.getScimEndpoint(connection_id); + return (await this.scimHttpRequest('patch', [ url, { user_id_attribute, mapping } ])).data; + } + + /** + * Deletes an existing `SCIM` configuration. + */ + async deleteScimConfiguration({ id: connection_id }: scimRequestParams): Promise { + log.debug(`Deleting SCIM configuration of connection ${ connection_id }`); + const url = this.getScimEndpoint(connection_id); + return (await this.scimHttpRequest('delete', [ url ])).data; + } + + async updateOverride(requestParams: scimRequestParams, bodyParams: Asset) { + // Extract `scim_configuration` from `bodyParams`. + // Remove `scim_configuration` from `bodyParams`, because `connections.update` doesn't accept it. + const { scim_configuration: scimBodyParams } = bodyParams; + delete bodyParams.scim_configuration; + + // First, update `connections`. + const updated = await this.connectionsManager.update(requestParams, bodyParams); + const idMapEntry = this.idMap.get(requestParams.id); + + // Now, update `scim_configuration` inside the updated connection. + // If `scim_configuration` exists in both local and remote -> updateScimConfiguration(...) + // If `scim_configuration` exists in remote but local -> deleteScimConfiguration(...) + // If `scim_configuration` exists in local but remote -> createScimConfiguration(...) + if (idMapEntry?.hasConfig) { + if (scimBodyParams) { + await this.updateScimConfiguration(requestParams, scimBodyParams); + } else { + if (this.config('AUTH0_ALLOW_DELETE')) { + log.warn(`Deleting scim_configuration on connection ${ requestParams.id }.`); + await this.deleteScimConfiguration(requestParams); + } else { + log.warn('Skipping DELETE scim_configuration. Enable deletes by setting AUTH0_ALLOW_DELETE to true in your config.'); + } + } + } else if (scimBodyParams) { + await this.createScimConfiguration(requestParams, scimBodyParams); + } + + // Return response from connections.update(...). + return updated; + } + + async createOverride(bodyParams: Asset) { + // Extract `scim_configuration` from `bodyParams`. + // Remove `scim_configuration` from `bodyParams`, because `connections.create` doesn't accept it. + const { scim_configuration: scimBodyParams } = bodyParams; + delete bodyParams.scim_configuration; + + // First, create the new `connection`. + const created = await this.connectionsManager.create(bodyParams); + if (scimBodyParams) { + // Now, create the `scim_configuration` for newly created `connection`. + await this.createScimConfiguration({ id: created.id }, scimBodyParams); + } + + // Return response from connections.create(...). + return created; + } +} \ No newline at end of file diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index 0d46ce645..4f6021579 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -2,9 +2,11 @@ import path from 'path'; import fs from 'fs-extra'; import jsYaml from 'js-yaml'; import { expect } from 'chai'; +import sinon from 'sinon'; import Context from '../../../src/context/yaml'; import { cleanThenMkdir, testDataDir, mockMgmtClient } from '../../utils'; +import ScimHandler from '../../../src/tools/auth0/handlers/scimHandler'; describe('#YAML context validation', () => { it('should do nothing on empty yaml', async () => { @@ -537,6 +539,9 @@ describe('#YAML context validation', () => { }); it('should preserve keywords when dumping', async () => { + const applyScimConfiguration = (connections) => connections; + sinon.stub(ScimHandler.prototype, 'applyScimConfiguration').returns(applyScimConfiguration); + const dir = path.resolve(testDataDir, 'yaml', 'dump'); cleanThenMkdir(dir); const tenantFile = path.join(dir, 'tenant.yml'); @@ -585,10 +590,11 @@ describe('#YAML context validation', () => { }, }, ], - }), - }, + }) + } } ); + await context.dump(); const yaml = jsYaml.load(fs.readFileSync(tenantFile)); @@ -607,5 +613,6 @@ describe('#YAML context validation', () => { }, ], }); + sinon.restore(); }); }); diff --git a/test/tools/auth0/handlers/connections.tests.js b/test/tools/auth0/handlers/connections.tests.js index 92c1882f2..2c3537a0c 100644 --- a/test/tools/auth0/handlers/connections.tests.js +++ b/test/tools/auth0/handlers/connections.tests.js @@ -1,5 +1,6 @@ /* eslint-disable consistent-return */ const { expect } = require('chai'); +const sinon = require('sinon'); const connections = require('../../../../src/tools/auth0/handlers/connections'); const pool = { @@ -56,6 +57,32 @@ describe('#connections handler', () => { }); describe('#connections process', () => { + let scimHandlerMock; + + beforeEach(() => { + scimHandlerMock = { + createIdMap: sinon.stub().resolves(new Map()), + getScimConfiguration: sinon.stub().resolves({ + connection_id: 'con_KYp633cmKtnEQ31C', + connection_name: 'okta', + strategy: 'okta', + tenant_name: 'test-tenant', + user_id_attribute: 'externalId-1', + mapping: [ + { + scim: 'scim_id', + auth0: 'auth0_id' + } + ] + }), + applyScimConfiguration: sinon.stub().resolves(undefined) + }; + }); + + afterEach(() => { + sinon.restore(); + }); + it('should create connection', async () => { const auth0 = { connections: { @@ -199,6 +226,7 @@ describe('#connections handler', () => { }; const handler = new connections.default({ client: auth0, config }); + handler.scimHandler = scimHandlerMock; const stageFn = Object.getPrototypeOf(handler).processChanges; const data = [ { @@ -283,6 +311,7 @@ describe('#connections handler', () => { }; const handler = new connections.default({ client: auth0, config }); + handler.scimHandler = scimHandlerMock; const stageFn = Object.getPrototypeOf(handler).processChanges; const data = [ { diff --git a/test/tools/auth0/handlers/scimHandler.tests.js b/test/tools/auth0/handlers/scimHandler.tests.js new file mode 100644 index 000000000..db98afcc4 --- /dev/null +++ b/test/tools/auth0/handlers/scimHandler.tests.js @@ -0,0 +1,408 @@ +const { expect } = require('chai'); +// eslint-disable-next-line import/no-extraneous-dependencies +const axios = require('axios'); +const sinon = require('sinon'); +const ScimHandler = require('../../../../src/tools/auth0/handlers/scimHandler').default; + +let scimHandler; +beforeEach(() => { + const connectionResponse = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection' + }; + const connectionsManagerMock = { + update: sinon.stub().resolves(connectionResponse), + create: sinon.stub().resolves(connectionResponse) + }; + + scimHandler = new ScimHandler( + function() { return 'https://test-host.auth0.com'; }, + { + getAccessToken: async function () { + return 'mock_access_token'; + } + }, + connectionsManagerMock + ); +}); + +describe('ScimHandler', () => { + describe('#isScimStrategy', () => { + it('should return true for SCIM strategy', () => { + const response = scimHandler.isScimStrategy('samlp'); + // eslint-disable-next-line no-unused-expressions + expect(response).to.be.true; + }); + + it('should return false for non-SCIM strategy', () => { + const response = scimHandler.isScimStrategy('oauth'); + // eslint-disable-next-line no-unused-expressions + expect(response).to.be.false; + }); + }); + + describe('#createIdMap', () => { + it('should create id map with SCIM configuration', async () => { + const connections = [ + { id: 'con_KYp633cmKtnEQ31C', strategy: 'samlp' }, // SCIM connection. + { id: 'con_Njd1bxE3QTqTRwAk', strategy: 'auth0' }, // Non-SCIM connection. + { id: 'con_d3tmuoAkaUQgxN1f', strategy: 'gmail' } // Connection which doesn't exist. + ]; + const getScimConfigurationStub = sinon.stub(scimHandler, 'getScimConfiguration'); + getScimConfigurationStub.withArgs({ id: 'con_KYp633cmKtnEQ31C' }).resolves({ user_id_attribute: 'externalId-115', mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] }); + getScimConfigurationStub.withArgs({ id: 'con_Njd1bxE3QTqTRwAk' }).rejects({ response: { data: { statusCode: 404 } } }); + getScimConfigurationStub.withArgs({ id: 'con_d3tmuoAkaUQgxN1f' }).rejects({ response: { data: { statusCode: 404 } } }); + + await scimHandler.createIdMap(connections); + // eslint-disable-next-line no-unused-expressions + expect(scimHandler.idMap.get('con_KYp633cmKtnEQ31C')).to.deep.equal({ strategy: 'samlp', hasConfig: true }); + + // eslint-disable-next-line no-unused-expressions + expect(scimHandler.idMap.get('con_Njd1bxE3QTqTRwAk')).to.be.undefined; // Because, it's a Non-SCIM connection. + + // eslint-disable-next-line no-unused-expressions + expect(scimHandler.idMap.get('con_d3tmuoAkaUQgxN1f')).to.be.undefined; + + getScimConfigurationStub.restore(); + }); + }); + + describe('#applyScimConfiguration', () => { + it('should apply SCIM configuration to connections', async () => { + const connections = [ + { id: 'con_KYp633cmKtnEQ31C', strategy: 'samlp' }, + { id: 'con_Njd1bxE3QTqTRwAk', strategy: 'oidc' }, + { id: 'con_d3tmuoAkaUQgxN1f', strategy: 'gmail' } + ]; + const getScimConfigurationStub = sinon.stub(scimHandler, 'getScimConfiguration'); + getScimConfigurationStub.withArgs({ id: 'con_KYp633cmKtnEQ31C' }).resolves({ user_id_attribute: 'externalId-1', mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] }); + getScimConfigurationStub.withArgs({ id: 'con_Njd1bxE3QTqTRwAk' }).resolves({ user_id_attribute: 'externalId-2', mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] }); + getScimConfigurationStub.withArgs({ id: 'con_d3tmuoAkaUQgxN1f' }).rejects({ response: { status: 404 } }); + + await scimHandler.applyScimConfiguration(connections); + + // eslint-disable-next-line no-unused-expressions + expect(connections[0].scim_configuration).to.deep.equal({ user_id_attribute: 'externalId-1', mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] }); + + // eslint-disable-next-line no-unused-expressions + expect(connections[1].scim_configuration).to.deep.equal({ user_id_attribute: 'externalId-2', mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] }); + + // eslint-disable-next-line no-unused-expressions + expect(connections[2].scim_configuration).to.be.undefined; + + getScimConfigurationStub.restore(); + }); + }); + + describe('#scimHttpRequest', () => { + it('should make HTTP request with correct authorization header', async () => { + const accessToken = 'mock_access_token'; + const axiosStub = sinon.stub(axios, 'get').resolves({ data: {} }); + const response = await scimHandler.scimHttpRequest('get', ['https://mock-domain/api/v2/connections/1/scim-configuration']); + + // eslint-disable-next-line no-unused-expressions + expect(response).to.exist; + + // eslint-disable-next-line no-unused-expressions + expect(axiosStub.calledOnce).to.be.true; + + // eslint-disable-next-line no-unused-expressions + expect(axiosStub.firstCall.args[1].headers.Authorization).to.equal(`Bearer ${accessToken}`); + + axiosStub.restore(); + }); + }); + + describe('#getScimConfiguration', () => { + it('should return SCIM configuration for existing connection', async () => { + const requestParams = { id: 'con_KYp633cmKtnEQ31C' }; + const scimConfiguration = { + connection_id: 'con_KYp633cmKtnEQ31C', + connection_name: 'okta', + strategy: 'okta', + tenant_name: 'test-tenant', + user_id_attribute: 'externalId-1', + mapping: [ + { + scim: 'scim_id', + auth0: 'auth0_id' + } + ] + }; + + const axiosStub = sinon.stub(axios, 'get').resolves({ data: scimConfiguration, status: 201 }); + const response = await scimHandler.getScimConfiguration(requestParams); + expect(response).to.deep.equal(scimConfiguration); + + axiosStub.restore(); + }); + + it('should throw error for non-existing SCIM configuration', async () => { + const requestParams = { id: 'con_KYp633cmKtnEQ31C' }; + const axiosStub = sinon.stub(axios, 'get').rejects({ response: { status: 404, errorCode: 'not-found', statusText: 'The connection does not exist' } }); + + try { + await scimHandler.getScimConfiguration(requestParams); + expect.fail('Expected getScimConfiguration to throw an error'); + } catch (error) { + expect(error.response.status).to.equal(404); + } + + axiosStub.restore(); + }); + }); + + describe('#createScimConfiguration', () => { + const requestParams = { + id: 'con_PKp644cmKtnEB11J' + }; + const payload = { + user_id_attribute: 'externalId-5', + mapping: [ + { + scim: 'scim_key', + auth0: 'auth0_key' + } + ] + }; + const responseBody = { + connection_id: 'con_PKp644cmKtnEB11J', + connection_name: 'okta-new-connection', + strategy: 'okta', + tenant_name: 'test-tenant', + ...requestParams, + ...payload, + created_at: new Date().getTime(), + updated_on: new Date().getTime() + }; + + it('should create new SCIM configuration', async () => { + const axiosStub = sinon.stub(axios, 'post').resolves({ data: responseBody, status: 201 }); + const response = await scimHandler.createScimConfiguration(requestParams, payload); + + expect(response.connection_id).to.equal(requestParams.id); + expect(response.user_id_attribute).to.equal(responseBody.user_id_attribute); + expect(response.mapping).to.deep.equal(responseBody.mapping); + + axiosStub.restore(); + }); + }); + + describe('#updateScimConfiguration', () => { + it('should update existing SCIM configuration', async () => { + const requestParams = { + id: 'con_PKp644cmKtnEB11J' + }; + const payload = { + user_id_attribute: 'externalId-5', + mapping: [ + { + scim: 'scim_key', + auth0: 'auth0_key' + } + ] + }; + const responseBody = { + connection_id: 'con_PKp644cmKtnEB11J', + connection_name: 'okta-new-connection', + strategy: 'okta', + tenant_name: 'test-tenant', + ...requestParams, + ...payload, + created_at: new Date().getTime(), + updated_on: new Date().getTime() + }; + const axiosStub = sinon.stub(axios, 'patch').resolves({ data: responseBody, status: 200 }); + const response = await scimHandler.updateScimConfiguration(requestParams, payload); + + expect(response.connection_id).to.equal(requestParams.id); + expect(response.user_id_attribute).to.equal(responseBody.user_id_attribute); + expect(response.mapping).to.deep.equal(responseBody.mapping); + + axiosStub.restore(); + }); + }); + + describe('#deleteScimConfiguration', () => { + it('should delete existing SCIM configuration', async () => { + const requestParams = { + id: 'con_PKp644cmKtnEB11J' + }; + const axiosStub = sinon.stub(axios, 'delete').resolves({ data: {}, status: 204 }); + const response = await scimHandler.deleteScimConfiguration(requestParams); + expect(response).to.deep.equal({}); + + axiosStub.restore(); + }); + }); + + describe('#updateOverride', () => { + it('should \'update\' connection and \'update\' SCIM configuration', async () => { + const requestParams = { id: 'con_PKp644cmKtnEB11J' }; + const bodyParams = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection', + scim_configuration: { + user_id_attribute: 'externalId-115', + mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] + } + }; + const connectionUpdatePayload = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection' + }; + const { scim_configuration: scimConfiguration } = bodyParams; + const idMapEntry = { + strategy: 'samlp', + hasConfig: true + }; + const idMapMock = new Map(); + idMapMock.set(requestParams.id, idMapEntry); + scimHandler.idMap = idMapMock; + + const updateScimStub = sinon.stub(scimHandler, 'updateScimConfiguration').resolves({ data: {} }); + const response = await scimHandler.updateOverride(requestParams, bodyParams); + + // eslint-disable-next-line no-unused-expressions + expect(response).to.deep.equal(connectionUpdatePayload); + + // eslint-disable-next-line no-unused-expressions + expect(updateScimStub.calledOnceWith(requestParams, scimConfiguration)).to.be.true; + + updateScimStub.restore(); + }); + + it('should \'update\' connection and \'create\' SCIM configuration', async () => { + const requestParams = { id: 'con_PKp644cmKtnEB11J' }; + const bodyParams = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection', + scim_configuration: { + user_id_attribute: 'externalId-115', + mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] + } + }; + const connectionUpdatePayload = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection' + }; + const { scim_configuration: scimConfiguration } = bodyParams; + const idMapEntry = { + strategy: 'samlp', + hasConfig: false + }; + const idMapMock = new Map(); + idMapMock.set(requestParams.id, idMapEntry); + scimHandler.idMap = idMapMock; + + const createScimStub = sinon.stub(scimHandler, 'createScimConfiguration').resolves({ data: {} }); + const response = await scimHandler.updateOverride(requestParams, bodyParams); + + // eslint-disable-next-line no-unused-expressions + expect(response).to.deep.equal(connectionUpdatePayload); + + // eslint-disable-next-line no-unused-expressions + expect(createScimStub.calledOnceWith(requestParams, scimConfiguration)).to.be.true; + + createScimStub.restore(); + }); + + it('should \'update\' connection and \'delete\' SCIM configuration', async () => { + const requestParams = { id: 'con_PKp644cmKtnEB11J' }; + const bodyParams = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection' + }; + const connectionUpdatePayload = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection' + }; + const idMapEntry = { + strategy: 'samlp', + hasConfig: true + }; + const idMapMock = new Map(); + idMapMock.set(requestParams.id, idMapEntry); + + scimHandler.idMap = idMapMock; + + const deleteScimStub = sinon.stub(scimHandler, 'deleteScimConfiguration').resolves({ data: {} }); + const response = await scimHandler.updateOverride(requestParams, bodyParams); + + // eslint-disable-next-line no-unused-expressions + expect(response).to.deep.equal(connectionUpdatePayload); + + // eslint-disable-next-line no-unused-expressions + expect(deleteScimStub.calledOnceWith(requestParams)).to.be.true; + + deleteScimStub.restore(); + }); + }); + + describe('#createOverride', () => { + it('should \'create\' connection and \'create\' SCIM configuration', async () => { + const requestParams = { id: 'con_PKp644cmKtnEB11J' }; + const bodyParams = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection', + scim_configuration: { + user_id_attribute: 'externalId-115', + mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] + } + }; + const connectionCreatePayload = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection' + }; + const { scim_configuration: scimConfiguration } = bodyParams; + const idMapEntry = { + strategy: 'samlp', + hasConfig: false + }; + const idMapMock = new Map(); + idMapMock.set(requestParams.id, idMapEntry); + scimHandler.idMap = idMapMock; + + const createScimStub = sinon.stub(scimHandler, 'createScimConfiguration').resolves({ data: {} }); + const response = await scimHandler.createOverride(bodyParams); + + // eslint-disable-next-line no-unused-expressions + expect(response).to.deep.equal(connectionCreatePayload); + + // eslint-disable-next-line no-unused-expressions + expect(createScimStub.calledOnceWith(requestParams, scimConfiguration)).to.be.true; + + createScimStub.restore(); + }); + + it('should \'create\' connection without SCIM configuration', async () => { + const requestParams = { id: 'con_PKp644cmKtnEB11J' }; + const bodyParams = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection' + }; + const connectionUpdatePayload = { + id: 'con_PKp644cmKtnEB11J', + name: 'test-connection' + }; + const idMapEntry = { + strategy: 'samlp', + hasConfig: false + }; + const idMapMock = new Map(); + idMapMock.set(requestParams.id, idMapEntry); + scimHandler.idMap = idMapMock; + + const createScimStub = sinon.stub(scimHandler, 'createScimConfiguration').resolves({ data: {} }); + const response = await scimHandler.createOverride(requestParams, bodyParams); + + // eslint-disable-next-line no-unused-expressions + expect(response).to.deep.equal(connectionUpdatePayload); + + // eslint-disable-next-line no-unused-expressions + expect(createScimStub.calledOnce).to.be.false; + + createScimStub.restore(); + }); + }); +});