-
Notifications
You must be signed in to change notification settings - Fork 163
Supporting SCIM feature | Workaround on node-auth0@v3 #921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
4f2daef
Supporting SCIM feature
nandan-bhat bf13191
Handling rate limit on getScimConfiguration | Added unit test coverage
nandan-bhat f34da56
Removing un-used variables
nandan-bhat c5fb632
Fixing lint issues | Adding dependency axios
nandan-bhat 3e218c0
Using built-in "sleep" method. | Using 2 space indentation
nandan-bhat 8ce7ab4
Removing axios from the dependency list
nandan-bhat 92347d0
Adding AUTH0_ALLOW_DELETE condition before deleting the scim_configur…
nandan-bhat b0c7dec
Updating the debug logs
nandan-bhat 1819069
Updating the debug logs
nandan-bhat 1c70019
Merge branch 'master' into feature/DXCDT-643
developerkunal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
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<string, IdMapValue>; | ||
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<string, IdMapValue>(); | ||
} | ||
|
||
/** | ||
* 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<string, any>[]]): Promise<AxiosResponse> { | ||
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<AxiosResponse> { | ||
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<scimBodyParams> { | ||
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<AxiosResponse> { | ||
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 } ])).data; | ||
} | ||
|
||
/** | ||
* Deletes an existing `SCIM` configuration. | ||
*/ | ||
async deleteScimConfiguration({ id: connection_id }: scimRequestParams): Promise<AxiosResponse> { | ||
log.debug(`Getting 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 { | ||
await this.deleteScimConfiguration(requestParams); | ||
} | ||
} else if (scimBodyParams) { | ||
await this.createScimConfiguration(requestParams, scimBodyParams); | ||
} | ||
|
||
// Return response from connections.update(...). | ||
return updated; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can add a comment, explaining the general intention of this override function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code block contains more details about |
||
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; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add debug logs to this and few other newly added methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateScimConfiguration
,createScimConfiguration
, anddeleteScimConfiguration
are called withinupdateOverride
, which already has a debug logger in place.