From 4f2daef2e16449ff532c57f506312934ce166f0a Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 9 Jul 2024 20:23:10 +0530 Subject: [PATCH 1/9] Supporting SCIM feature --- src/tools/auth0/handlers/connections.ts | 32 ++++ src/tools/auth0/handlers/scimHandler.ts | 193 ++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/tools/auth0/handlers/scimHandler.ts 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..ef8e36385 --- /dev/null +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -0,0 +1,193 @@ +import { Asset } from '../../../types'; +import axios, { AxiosResponse } from 'axios'; + +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; + 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(); + + await Promise.all( + connections.map(async (connection) => { + if (this.isScimStrategy(connection.strategy)) { + this.idMap.set(connection.id, { strategy: connection.strategy, hasConfig: false }); + try { + await this.getScimConfiguration({ id: connection.id }); + this.idMap.set(connection.id, { ...this.idMap.get(connection.id)!, hasConfig: true }); + } catch (err) { + if (!err.response || +err.response.status !== 404) throw err; + } + } + + return connection; + }) + ) + } + + /** + * 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[]) { + await Promise.all(connections.map(async (connection) => { + if (this.isScimStrategy(connection.strategy)) { + try { + const { user_id_attribute, mapping } = await this.getScimConfiguration({ id: connection.id }); + connection.scim_configuration = { user_id_attribute, mapping } + } catch (err) { + // Skip the connection if it returns 404. This can happen if `SCIM` is not enabled on a `SCIM` connection. + if (!err.response || +err.response.status !== 404) throw err; + } + } + + return connection; + })); + } + + /** + * HTTP request wrapper on axios. + */ + private async scimHttpRequest(method: string, options: [string, ...Record[]]): Promise { + // @ts-ignore + const accessToken = await this.tokenProvider.getAccessToken(); + const headers = { + 'Accept': 'application/json', + 'Authorization': `Bearer ${ accessToken }` + } + options = [...options, { headers }]; + return await axios[method](...options); + } + + /** + * 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 { + const url = this.getScimEndpoint(connection_id); + return await this.scimHttpRequest('post', [ url, { user_id_attribute, mapping } ]); + } + + /** + * Retrieves `SCIM` configuration of an enterprise connection. + */ + async getScimConfiguration({ id: connection_id }: scimRequestParams): Promise { + 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 { + const url = this.getScimEndpoint(connection_id); + return await this.scimHttpRequest('patch', [ url, { user_id_attribute, mapping } ]); + } + + /** + * Deletes an existing `SCIM` configuration. + */ + async deleteScimConfiguration({ id: connection_id }: scimRequestParams): Promise { + const url = this.getScimEndpoint(connection_id); + return await this.scimHttpRequest('delete', [ url ]); + } + + 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 { + await this.deleteScimConfiguration(requestParams); + } + } 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 From bf13191e27bbdd5ba00fde12c96533955baa9521 Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 16 Jul 2024 13:25:48 +0530 Subject: [PATCH 2/9] Handling rate limit on getScimConfiguration | Added unit test coverage --- src/tools/auth0/handlers/scimHandler.ts | 122 ++++-- test/context/yaml/context.test.js | 13 +- .../tools/auth0/handlers/connections.tests.js | 29 ++ .../tools/auth0/handlers/scimHandler.tests.js | 380 ++++++++++++++++++ 4 files changed, 502 insertions(+), 42 deletions(-) create mode 100644 test/tools/auth0/handlers/scimHandler.tests.js diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index ef8e36385..723235adf 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -1,5 +1,6 @@ import { Asset } from '../../../types'; import axios, { AxiosResponse } from 'axios'; +import log from '../../../logger'; interface IdMapValue { strategy: string; @@ -24,7 +25,7 @@ export default class ScimHandler { private readonly scimStrategies = ['samlp', 'oidc', 'okta', 'waad']; private tokenProvider: any; private config: any; - connectionsManager: any; + private connectionsManager: any; constructor(config, tokenProvider, connectionsManager) { this.config = config; @@ -33,6 +34,14 @@ export default class ScimHandler { this.idMap = new Map(); } + async wait (duration: number = 200) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, duration); + }); + } + /** * Check if the connection strategy is SCIM supported. * Only few of the enterprise connections are SCIM supported. @@ -51,21 +60,22 @@ export default class ScimHandler { async createIdMap(connections: Asset[]) { this.idMap.clear(); - await Promise.all( - connections.map(async (connection) => { - if (this.isScimStrategy(connection.strategy)) { - this.idMap.set(connection.id, { strategy: connection.strategy, hasConfig: false }); - try { - await this.getScimConfiguration({ id: connection.id }); - this.idMap.set(connection.id, { ...this.idMap.get(connection.id)!, hasConfig: true }); - } catch (err) { - if (!err.response || +err.response.status !== 404) throw err; - } - } - - return connection; - }) - ) + 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 this.wait(200); + } 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; + } + } } /** @@ -77,33 +87,61 @@ export default class ScimHandler { * This method mutates the incoming `connections`. */ async applyScimConfiguration(connections: Asset[]) { - await Promise.all(connections.map(async (connection) => { - if (this.isScimStrategy(connection.strategy)) { - try { - const { user_id_attribute, mapping } = await this.getScimConfiguration({ id: connection.id }); - connection.scim_configuration = { user_id_attribute, mapping } - } catch (err) { - // Skip the connection if it returns 404. This can happen if `SCIM` is not enabled on a `SCIM` connection. - if (!err.response || +err.response.status !== 404) throw err; - } - } - - return connection; - })); + 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 this.wait(200); + } 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 { - // @ts-ignore - const accessToken = await this.tokenProvider.getAccessToken(); - const headers = { - 'Accept': 'application/json', - 'Authorization': `Bearer ${ accessToken }` + 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; } - options = [...options, { headers }]; - return await axios[method](...options); } /** @@ -118,14 +156,16 @@ export default class ScimHandler { * 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 } ]); + 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; } @@ -134,16 +174,18 @@ export default class ScimHandler { * Updates an existing `SCIM` configuration. */ async updateScimConfiguration({ id: connection_id }: scimRequestParams, { user_id_attribute, mapping }: scimBodyParams): Promise { + log.debug(`Getting SCIM configuration on connection ${ connection_id }`); const url = this.getScimEndpoint(connection_id); - return await this.scimHttpRequest('patch', [ url, { user_id_attribute, mapping } ]); + 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(`Getting SCIM configuration of connection ${ connection_id }`); const url = this.getScimEndpoint(connection_id); - return await this.scimHttpRequest('delete', [ url ]); + return (await this.scimHttpRequest('delete', [ url ])).data; } async updateOverride(requestParams: scimRequestParams, bodyParams: Asset) { @@ -162,7 +204,7 @@ export default class ScimHandler { // If `scim_configuration` exists in local but remote -> createScimConfiguration(...) if (idMapEntry?.hasConfig) { if (scimBodyParams) { - await this.updateScimConfiguration(requestParams, scimBodyParams); + const x = await this.updateScimConfiguration(requestParams, scimBodyParams); } else { await this.deleteScimConfiguration(requestParams); } diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index 0d46ce645..645ef22b8 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -2,6 +2,7 @@ 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'; @@ -537,6 +538,12 @@ describe('#YAML context validation', () => { }); it('should preserve keywords when dumping', async () => { + const scimHandlerMock = { + applyScimConfiguration: (connections) => connections + } + const ScimHandler = require('../../../src/tools/auth0/handlers/scimHandler'); + sinon.stub(ScimHandler, 'default').returns(scimHandlerMock); + const dir = path.resolve(testDataDir, 'yaml', 'dump'); cleanThenMkdir(dir); const tenantFile = path.join(dir, 'tenant.yml'); @@ -585,10 +592,11 @@ describe('#YAML context validation', () => { }, }, ], - }), - }, + }) + } } ); + await context.dump(); const yaml = jsYaml.load(fs.readFileSync(tenantFile)); @@ -607,5 +615,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..6c7b109c8 100644 --- a/test/tools/auth0/handlers/connections.tests.js +++ b/test/tools/auth0/handlers/connections.tests.js @@ -1,6 +1,7 @@ /* eslint-disable consistent-return */ const { expect } = require('chai'); const connections = require('../../../../src/tools/auth0/handlers/connections'); +const sinon = require('sinon'); const pool = { addEachTask: (data) => { @@ -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..38b0be469 --- /dev/null +++ b/test/tools/auth0/handlers/scimHandler.tests.js @@ -0,0 +1,380 @@ +const { expect } = require('chai'); +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'); + expect(response).to.be.true; + }); + + it('should return false for non-SCIM strategy', () => { + const response = scimHandler.isScimStrategy('oauth'); + 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); + + expect(scimHandler.idMap.get('con_KYp633cmKtnEQ31C')).to.deep.equal({ strategy: 'samlp', hasConfig: true }); + expect(scimHandler.idMap.get('con_Njd1bxE3QTqTRwAk')).to.be.undefined; // Because, it's a Non-SCIM connection. + 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); + + expect(connections[0].scim_configuration).to.deep.equal({ user_id_attribute: 'externalId-1', mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] }); + expect(connections[1].scim_configuration).to.deep.equal({ user_id_attribute: 'externalId-2', mapping: [{ auth0: 'auth0_key', scim: 'scim_key' }] }); + 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 tokenProviderMock = { + getAccessToken: sinon.stub().resolves(accessToken) + }; + const axiosStub = sinon.stub(axios, 'get').resolves({ data: {} }); + const response = await scimHandler.scimHttpRequest('get', ['https://mock-domain/api/v2/connections/1/scim-configuration']); + + expect(response).to.exist; + expect(axiosStub.calledOnce).to.be.true; + 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({ status: 204 }); + const response = await scimHandler.deleteScimConfiguration(requestParams); + + 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); + + expect(response).to.deep.equal(connectionUpdatePayload); + 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); + + expect(response).to.deep.equal(connectionUpdatePayload); + 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); + + expect(response).to.deep.equal(connectionUpdatePayload); + 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); + + expect(response).to.deep.equal(connectionCreatePayload); + 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); + + expect(response).to.deep.equal(connectionUpdatePayload); + expect(createScimStub.calledOnce).to.be.false; + + createScimStub.restore(); + }); + }); +}); From f34da56c6b48a0b5848b0e4dce9525fbbb98a9d1 Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 16 Jul 2024 13:52:58 +0530 Subject: [PATCH 3/9] Removing un-used variables --- src/tools/auth0/handlers/scimHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index 723235adf..1471dcef9 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -204,7 +204,7 @@ export default class ScimHandler { // If `scim_configuration` exists in local but remote -> createScimConfiguration(...) if (idMapEntry?.hasConfig) { if (scimBodyParams) { - const x = await this.updateScimConfiguration(requestParams, scimBodyParams); + await this.updateScimConfiguration(requestParams, scimBodyParams); } else { await this.deleteScimConfiguration(requestParams); } From c5fb6324f84cf5dd1031d805c92580aebddd153d Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 16 Jul 2024 15:15:28 +0530 Subject: [PATCH 4/9] Fixing lint issues | Adding dependency axios --- package-lock.json | 18 ++--- package.json | 1 + src/tools/auth0/handlers/scimHandler.ts | 4 +- test/context/yaml/context.test.js | 8 +-- .../tools/auth0/handlers/connections.tests.js | 8 +-- .../tools/auth0/handlers/scimHandler.tests.js | 71 +++++++++++++------ 6 files changed, 69 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81b484fd6..4f20931dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "ajv": "^6.12.6", "auth0": "^3.0.0", + "axios": "^1.7.2", "dot-prop": "^5.2.0", "fs-extra": "^10.1.0", "global-agent": "^2.1.12", @@ -1508,11 +1509,12 @@ } }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.2", + "resolved": "https://a0us.jfrog.io/artifactory/api/npm/npm/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -8130,11 +8132,11 @@ "dev": true }, "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.2", + "resolved": "https://a0us.jfrog.io/artifactory/api/npm/npm/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" }, diff --git a/package.json b/package.json index b477f1b41..046ce6d24 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "dependencies": { "ajv": "^6.12.6", "auth0": "^3.0.0", + "axios": "^1.7.2", "dot-prop": "^5.2.0", "fs-extra": "^10.1.0", "global-agent": "^2.1.12", diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index 1471dcef9..ffcdf5169 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -70,7 +70,7 @@ export default class ScimHandler { // To avoid rate limiter error, we making API requests with a small delay. // TODO: However, this logic needs to be re-worked. - await this.wait(200); + await this.wait(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; @@ -96,7 +96,7 @@ export default class ScimHandler { // To avoid rate limiter error, we making API requests with a small delay. // TODO: However, this logic needs to be re-worked. - await this.wait(200); + await this.wait(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; diff --git a/test/context/yaml/context.test.js b/test/context/yaml/context.test.js index 645ef22b8..4f6021579 100644 --- a/test/context/yaml/context.test.js +++ b/test/context/yaml/context.test.js @@ -6,6 +6,7 @@ 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 () => { @@ -538,11 +539,8 @@ describe('#YAML context validation', () => { }); it('should preserve keywords when dumping', async () => { - const scimHandlerMock = { - applyScimConfiguration: (connections) => connections - } - const ScimHandler = require('../../../src/tools/auth0/handlers/scimHandler'); - sinon.stub(ScimHandler, 'default').returns(scimHandlerMock); + const applyScimConfiguration = (connections) => connections; + sinon.stub(ScimHandler.prototype, 'applyScimConfiguration').returns(applyScimConfiguration); const dir = path.resolve(testDataDir, 'yaml', 'dump'); cleanThenMkdir(dir); diff --git a/test/tools/auth0/handlers/connections.tests.js b/test/tools/auth0/handlers/connections.tests.js index 6c7b109c8..2c3537a0c 100644 --- a/test/tools/auth0/handlers/connections.tests.js +++ b/test/tools/auth0/handlers/connections.tests.js @@ -1,7 +1,7 @@ /* eslint-disable consistent-return */ const { expect } = require('chai'); -const connections = require('../../../../src/tools/auth0/handlers/connections'); const sinon = require('sinon'); +const connections = require('../../../../src/tools/auth0/handlers/connections'); const pool = { addEachTask: (data) => { @@ -67,11 +67,11 @@ describe('#connections handler', () => { connection_name: 'okta', strategy: 'okta', tenant_name: 'test-tenant', - user_id_attribute: "externalId-1", + user_id_attribute: 'externalId-1', mapping: [ { - scim: "scim_id", - auth0: "auth0_id" + scim: 'scim_id', + auth0: 'auth0_id' } ] }), diff --git a/test/tools/auth0/handlers/scimHandler.tests.js b/test/tools/auth0/handlers/scimHandler.tests.js index 38b0be469..277df419c 100644 --- a/test/tools/auth0/handlers/scimHandler.tests.js +++ b/test/tools/auth0/handlers/scimHandler.tests.js @@ -15,10 +15,10 @@ beforeEach(() => { }; scimHandler = new ScimHandler( - function() { return "https://test-host.auth0.com" }, + function() { return 'https://test-host.auth0.com'; }, { getAccessToken: async function () { - return 'mock_access_token' + return 'mock_access_token'; } }, connectionsManagerMock @@ -29,11 +29,13 @@ 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; }); }); @@ -51,9 +53,13 @@ describe('ScimHandler', () => { 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(); @@ -74,8 +80,13 @@ describe('ScimHandler', () => { 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(); @@ -85,14 +96,16 @@ describe('ScimHandler', () => { describe('#scimHttpRequest', () => { it('should make HTTP request with correct authorization header', async () => { const accessToken = 'mock_access_token'; - const tokenProviderMock = { - getAccessToken: sinon.stub().resolves(accessToken) - }; 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(); @@ -101,20 +114,20 @@ describe('ScimHandler', () => { describe('#getScimConfiguration', () => { it('should return SCIM configuration for existing connection', async () => { - const requestParams = { id: 'con_KYp633cmKtnEQ31C' } + 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", + user_id_attribute: 'externalId-1', mapping: [ { - scim: "scim_id", - auth0: "auth0_id" + scim: 'scim_id', + auth0: 'auth0_id' } ] - } + }; const axiosStub = sinon.stub(axios, 'get').resolves({ data: scimConfiguration, status: 201 }); const response = await scimHandler.getScimConfiguration(requestParams); @@ -160,7 +173,7 @@ describe('ScimHandler', () => { ...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 }); @@ -197,7 +210,7 @@ describe('ScimHandler', () => { ...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); @@ -214,15 +227,16 @@ describe('ScimHandler', () => { const requestParams = { id: 'con_PKp644cmKtnEB11J' }; - const axiosStub = sinon.stub(axios, 'delete').resolves({ status: 204 }); + 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 () => { + it('should \'update\' connection and \'update\' SCIM configuration', async () => { const requestParams = { id: 'con_PKp644cmKtnEB11J' }; const bodyParams = { id: 'con_PKp644cmKtnEB11J', @@ -245,17 +259,19 @@ describe('ScimHandler', () => { 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 () => { + it('should \'update\' connection and \'create\' SCIM configuration', async () => { const requestParams = { id: 'con_PKp644cmKtnEB11J' }; const bodyParams = { id: 'con_PKp644cmKtnEB11J', @@ -281,13 +297,16 @@ describe('ScimHandler', () => { 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 () => { + it('should \'update\' connection and \'delete\' SCIM configuration', async () => { const requestParams = { id: 'con_PKp644cmKtnEB11J' }; const bodyParams = { id: 'con_PKp644cmKtnEB11J', @@ -309,7 +328,10 @@ describe('ScimHandler', () => { 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(); @@ -317,7 +339,7 @@ describe('ScimHandler', () => { }); describe('#createOverride', () => { - it('should "create" connection and "create" SCIM configuration', async () => { + it('should \'create\' connection and \'create\' SCIM configuration', async () => { const requestParams = { id: 'con_PKp644cmKtnEB11J' }; const bodyParams = { id: 'con_PKp644cmKtnEB11J', @@ -343,13 +365,16 @@ describe('ScimHandler', () => { 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 () => { + it('should \'create\' connection without SCIM configuration', async () => { const requestParams = { id: 'con_PKp644cmKtnEB11J' }; const bodyParams = { id: 'con_PKp644cmKtnEB11J', @@ -367,11 +392,13 @@ describe('ScimHandler', () => { 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(); From 3e218c0b61a45000b4babe7e14e497ca18fe069d Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 16 Jul 2024 15:33:31 +0530 Subject: [PATCH 5/9] Using built-in "sleep" method. | Using 2 space indentation --- src/tools/auth0/handlers/scimHandler.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index ffcdf5169..2a41c4f9e 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -1,6 +1,7 @@ import { Asset } from '../../../types'; import axios, { AxiosResponse } from 'axios'; import log from '../../../logger'; +import { sleep } from '../../utils'; interface IdMapValue { strategy: string; @@ -34,14 +35,6 @@ export default class ScimHandler { this.idMap = new Map(); } - async wait (duration: number = 200) { - return new Promise((resolve) => { - setTimeout(() => { - resolve(true); - }, duration); - }); - } - /** * Check if the connection strategy is SCIM supported. * Only few of the enterprise connections are SCIM supported. @@ -70,7 +63,7 @@ export default class ScimHandler { // To avoid rate limiter error, we making API requests with a small delay. // TODO: However, this logic needs to be re-worked. - await this.wait(500); + 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; @@ -96,7 +89,7 @@ export default class ScimHandler { // To avoid rate limiter error, we making API requests with a small delay. // TODO: However, this logic needs to be re-worked. - await this.wait(500); + 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; From 8ce7ab44018800b56f0d63d6710c59d21e1c91d4 Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 16 Jul 2024 16:35:48 +0530 Subject: [PATCH 6/9] Removing axios from the dependency list --- package-lock.json | 18 ++++++++---------- package.json | 1 - test/tools/auth0/handlers/scimHandler.tests.js | 1 + 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f20931dc..81b484fd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "ajv": "^6.12.6", "auth0": "^3.0.0", - "axios": "^1.7.2", "dot-prop": "^5.2.0", "fs-extra": "^10.1.0", "global-agent": "^2.1.12", @@ -1509,12 +1508,11 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://a0us.jfrog.io/artifactory/api/npm/npm/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", - "license": "MIT", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dependencies": { - "follow-redirects": "^1.15.6", + "follow-redirects": "^1.15.0", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -8132,11 +8130,11 @@ "dev": true }, "axios": { - "version": "1.7.2", - "resolved": "https://a0us.jfrog.io/artifactory/api/npm/npm/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "requires": { - "follow-redirects": "^1.15.6", + "follow-redirects": "^1.15.0", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" }, diff --git a/package.json b/package.json index 046ce6d24..b477f1b41 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "dependencies": { "ajv": "^6.12.6", "auth0": "^3.0.0", - "axios": "^1.7.2", "dot-prop": "^5.2.0", "fs-extra": "^10.1.0", "global-agent": "^2.1.12", diff --git a/test/tools/auth0/handlers/scimHandler.tests.js b/test/tools/auth0/handlers/scimHandler.tests.js index 277df419c..db98afcc4 100644 --- a/test/tools/auth0/handlers/scimHandler.tests.js +++ b/test/tools/auth0/handlers/scimHandler.tests.js @@ -1,4 +1,5 @@ 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; From 92347d0d49be4ad1e88bda57a42224f10ad79700 Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 16 Jul 2024 19:56:01 +0530 Subject: [PATCH 7/9] Adding AUTH0_ALLOW_DELETE condition before deleting the scim_configuration --- src/tools/auth0/handlers/scimHandler.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index 2a41c4f9e..207f1c66b 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -199,7 +199,12 @@ export default class ScimHandler { if (scimBodyParams) { await this.updateScimConfiguration(requestParams, scimBodyParams); } else { - await this.deleteScimConfiguration(requestParams); + if (this.config('AUTH0_ALLOW_DELETE')) { + log.warn(`Deleting scim_configuration on connection ${ requestParams.id }.`); + await this.deleteScimConfiguration(requestParams); + } else { + log.debug('Skipping DELETE scim_configuration. Enable deletes by setting AUTH0_ALLOW_DELETE to true in your config.'); + } } } else if (scimBodyParams) { await this.createScimConfiguration(requestParams, scimBodyParams); From b0c7dec15d59db34f1cc19e2b90fc238b8d4ba78 Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 16 Jul 2024 19:59:17 +0530 Subject: [PATCH 8/9] Updating the debug logs --- src/tools/auth0/handlers/scimHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index 207f1c66b..bad9a0026 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -167,7 +167,7 @@ export default class ScimHandler { * Updates an existing `SCIM` configuration. */ async updateScimConfiguration({ id: connection_id }: scimRequestParams, { user_id_attribute, mapping }: scimBodyParams): Promise { - log.debug(`Getting SCIM configuration on connection ${ connection_id }`); + 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; } @@ -176,7 +176,7 @@ export default class ScimHandler { * Deletes an existing `SCIM` configuration. */ async deleteScimConfiguration({ id: connection_id }: scimRequestParams): Promise { - log.debug(`Getting SCIM configuration of connection ${ connection_id }`); + log.debug(`Deleting SCIM configuration of connection ${ connection_id }`); const url = this.getScimEndpoint(connection_id); return (await this.scimHttpRequest('delete', [ url ])).data; } From 1819069c1988dd136f751198c80797b53ec81857 Mon Sep 17 00:00:00 2001 From: Nandan Bhat Date: Tue, 16 Jul 2024 20:02:40 +0530 Subject: [PATCH 9/9] Updating the debug logs --- src/tools/auth0/handlers/scimHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/auth0/handlers/scimHandler.ts b/src/tools/auth0/handlers/scimHandler.ts index bad9a0026..064b1168a 100644 --- a/src/tools/auth0/handlers/scimHandler.ts +++ b/src/tools/auth0/handlers/scimHandler.ts @@ -203,7 +203,7 @@ export default class ScimHandler { log.warn(`Deleting scim_configuration on connection ${ requestParams.id }.`); await this.deleteScimConfiguration(requestParams); } else { - log.debug('Skipping DELETE scim_configuration. Enable deletes by setting AUTH0_ALLOW_DELETE to true in your config.'); + log.warn('Skipping DELETE scim_configuration. Enable deletes by setting AUTH0_ALLOW_DELETE to true in your config.'); } } } else if (scimBodyParams) {