1
+ import { Asset } from '../../../types' ;
2
+ import axios , { AxiosResponse } from 'axios' ;
3
+ import log from '../../../logger' ;
4
+ import { sleep } from '../../utils' ;
5
+
6
+ interface IdMapValue {
7
+ strategy : string ;
8
+ hasConfig : boolean ;
9
+ }
10
+
11
+ interface scimRequestParams {
12
+ id : string ;
13
+ }
14
+
15
+ interface scimBodyParams {
16
+ user_id_attribute : string ;
17
+ mapping : { scim : string ; auth0 : string ; } [ ] ;
18
+ }
19
+
20
+ /**
21
+ * The current version of this sdk use `node-auth0` v3. But `SCIM` features are not natively supported by v3.
22
+ * This is a workaround to make this SDK support SCIM without `node-auth0` upgrade.
23
+ */
24
+ export default class ScimHandler {
25
+ private idMap : Map < string , IdMapValue > ;
26
+ private readonly scimStrategies = [ 'samlp' , 'oidc' , 'okta' , 'waad' ] ;
27
+ private tokenProvider : any ;
28
+ private config : any ;
29
+ private connectionsManager : any ;
30
+
31
+ constructor ( config , tokenProvider , connectionsManager ) {
32
+ this . config = config ;
33
+ this . tokenProvider = tokenProvider ;
34
+ this . connectionsManager = connectionsManager ;
35
+ this . idMap = new Map < string , IdMapValue > ( ) ;
36
+ }
37
+
38
+ /**
39
+ * Check if the connection strategy is SCIM supported.
40
+ * Only few of the enterprise connections are SCIM supported.
41
+ */
42
+ isScimStrategy ( strategy : string ) {
43
+ return this . scimStrategies . includes ( strategy . toLowerCase ( ) ) ;
44
+ }
45
+
46
+ /**
47
+ * Creates connection_id -> { strategy, hasConfig } map.
48
+ * Store only the SCIM ids available on the existing / remote config.
49
+ * Payload received on `create` and `update` methods has the property `strategy` stripped.
50
+ * So, we need this map to perform `create`, `update` or `delete` actions on SCIM.
51
+ * @param connections
52
+ */
53
+ async createIdMap ( connections : Asset [ ] ) {
54
+ this . idMap . clear ( ) ;
55
+
56
+ for ( let connection of connections ) {
57
+ if ( ! this . isScimStrategy ( connection . strategy ) ) continue ;
58
+
59
+ try {
60
+ this . idMap . set ( connection . id , { strategy : connection . strategy , hasConfig : false } ) ;
61
+ await this . getScimConfiguration ( { id : connection . id } ) ;
62
+ this . idMap . set ( connection . id , { ...this . idMap . get ( connection . id ) ! , hasConfig : true } ) ;
63
+
64
+ // To avoid rate limiter error, we making API requests with a small delay.
65
+ // TODO: However, this logic needs to be re-worked.
66
+ await sleep ( 500 ) ;
67
+ } catch ( err ) {
68
+ // Skip the connection if it returns 404. This can happen if `SCIM` is not enabled on a `SCIM` connection.
69
+ if ( err !== 'SCIM_NOT_FOUND' ) throw err ;
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Iterate through all the connections and add property `scim_configuration` to only `SCIM` connections.
76
+ * The following conditions should be met to have `scim_configuration` set to a `connection`.
77
+ * 1. Connection `strategy` should be one of `scimStrategies`
78
+ * 2. Connection should have `SCIM` enabled.
79
+ *
80
+ * This method mutates the incoming `connections`.
81
+ */
82
+ async applyScimConfiguration ( connections : Asset [ ] ) {
83
+ for ( let connection of connections ) {
84
+ if ( ! this . isScimStrategy ( connection . strategy ) ) continue ;
85
+
86
+ try {
87
+ const { user_id_attribute, mapping } = await this . getScimConfiguration ( { id : connection . id } ) ;
88
+ connection . scim_configuration = { user_id_attribute, mapping }
89
+
90
+ // To avoid rate limiter error, we making API requests with a small delay.
91
+ // TODO: However, this logic needs to be re-worked.
92
+ await sleep ( 500 ) ;
93
+ } catch ( err ) {
94
+ // Skip the connection if it returns 404. This can happen if `SCIM` is not enabled on a `SCIM` connection.
95
+ if ( err !== 'SCIM_NOT_FOUND' ) throw err ;
96
+
97
+ const warningMessage = `SCIM configuration not found on connection \"${ connection . id } \".` ;
98
+ log . warn ( warningMessage ) ;
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * HTTP request wrapper on axios.
105
+ */
106
+ private async scimHttpRequest ( method : string , options : [ string , ...Record < string , any > [ ] ] ) : Promise < AxiosResponse > {
107
+ return await this . withErrorHandling ( async ( ) => {
108
+ // @ts -ignore
109
+ const accessToken = await this . tokenProvider ?. getAccessToken ( ) ;
110
+ const headers = {
111
+ 'Accept' : 'application/json' ,
112
+ 'Authorization' : `Bearer ${ accessToken } `
113
+ }
114
+ options = [ ...options , { headers } ] ;
115
+
116
+ return await axios [ method ] ( ...options ) ;
117
+ } ) ;
118
+ }
119
+
120
+ /**
121
+ * Error handler wrapper.
122
+ */
123
+ async withErrorHandling ( callback ) {
124
+ try {
125
+ return await callback ( ) ;
126
+ } catch ( error ) {
127
+ const errorData = error ?. response ?. data ;
128
+ if ( errorData ?. statusCode === 404 ) throw "SCIM_NOT_FOUND" ;
129
+
130
+ const statusCode = errorData ?. statusCode || error ?. response ?. status ;
131
+ const errorCode = errorData ?. errorCode || errorData ?. error || error ?. response ?. statusText ;
132
+ const errorMessage = errorData ?. message || error ?. response ?. statusText ;
133
+ const message = `SCIM request failed with statusCode ${ statusCode } (${ errorCode } ). ${ errorMessage } .` ;
134
+
135
+ log . error ( message ) ;
136
+ throw error ;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Returns formatted endpoint url.
142
+ */
143
+ private getScimEndpoint ( connection_id : string ) {
144
+ // Call `scim-configuration` endpoint directly to support `SCIM` features.
145
+ return `https://${ this . config ( 'AUTH0_DOMAIN' ) } /api/v2/connections/${ connection_id } /scim-configuration` ;
146
+ }
147
+
148
+ /**
149
+ * Creates a new `SCIM` configuration.
150
+ */
151
+ async createScimConfiguration ( { id : connection_id } : scimRequestParams , { user_id_attribute, mapping } : scimBodyParams ) : Promise < AxiosResponse > {
152
+ log . debug ( `Creating SCIM configuration for connection ${ connection_id } ` ) ;
153
+ const url = this . getScimEndpoint ( connection_id ) ;
154
+ return ( await this . scimHttpRequest ( 'post' , [ url , { user_id_attribute, mapping } ] ) ) . data ;
155
+ }
156
+
157
+ /**
158
+ * Retrieves `SCIM` configuration of an enterprise connection.
159
+ */
160
+ async getScimConfiguration ( { id : connection_id } : scimRequestParams ) : Promise < scimBodyParams > {
161
+ log . debug ( `Getting SCIM configuration from connection ${ connection_id } ` ) ;
162
+ const url = this . getScimEndpoint ( connection_id ) ;
163
+ return ( await this . scimHttpRequest ( 'get' , [ url ] ) ) . data ;
164
+ }
165
+
166
+ /**
167
+ * Updates an existing `SCIM` configuration.
168
+ */
169
+ async updateScimConfiguration ( { id : connection_id } : scimRequestParams , { user_id_attribute, mapping } : scimBodyParams ) : Promise < AxiosResponse > {
170
+ log . debug ( `Updating SCIM configuration on connection ${ connection_id } ` ) ;
171
+ const url = this . getScimEndpoint ( connection_id ) ;
172
+ return ( await this . scimHttpRequest ( 'patch' , [ url , { user_id_attribute, mapping } ] ) ) . data ;
173
+ }
174
+
175
+ /**
176
+ * Deletes an existing `SCIM` configuration.
177
+ */
178
+ async deleteScimConfiguration ( { id : connection_id } : scimRequestParams ) : Promise < AxiosResponse > {
179
+ log . debug ( `Deleting SCIM configuration of connection ${ connection_id } ` ) ;
180
+ const url = this . getScimEndpoint ( connection_id ) ;
181
+ return ( await this . scimHttpRequest ( 'delete' , [ url ] ) ) . data ;
182
+ }
183
+
184
+ async updateOverride ( requestParams : scimRequestParams , bodyParams : Asset ) {
185
+ // Extract `scim_configuration` from `bodyParams`.
186
+ // Remove `scim_configuration` from `bodyParams`, because `connections.update` doesn't accept it.
187
+ const { scim_configuration : scimBodyParams } = bodyParams ;
188
+ delete bodyParams . scim_configuration ;
189
+
190
+ // First, update `connections`.
191
+ const updated = await this . connectionsManager . update ( requestParams , bodyParams ) ;
192
+ const idMapEntry = this . idMap . get ( requestParams . id ) ;
193
+
194
+ // Now, update `scim_configuration` inside the updated connection.
195
+ // If `scim_configuration` exists in both local and remote -> updateScimConfiguration(...)
196
+ // If `scim_configuration` exists in remote but local -> deleteScimConfiguration(...)
197
+ // If `scim_configuration` exists in local but remote -> createScimConfiguration(...)
198
+ if ( idMapEntry ?. hasConfig ) {
199
+ if ( scimBodyParams ) {
200
+ await this . updateScimConfiguration ( requestParams , scimBodyParams ) ;
201
+ } else {
202
+ if ( this . config ( 'AUTH0_ALLOW_DELETE' ) ) {
203
+ log . warn ( `Deleting scim_configuration on connection ${ requestParams . id } .` ) ;
204
+ await this . deleteScimConfiguration ( requestParams ) ;
205
+ } else {
206
+ log . warn ( 'Skipping DELETE scim_configuration. Enable deletes by setting AUTH0_ALLOW_DELETE to true in your config.' ) ;
207
+ }
208
+ }
209
+ } else if ( scimBodyParams ) {
210
+ await this . createScimConfiguration ( requestParams , scimBodyParams ) ;
211
+ }
212
+
213
+ // Return response from connections.update(...).
214
+ return updated ;
215
+ }
216
+
217
+ async createOverride ( bodyParams : Asset ) {
218
+ // Extract `scim_configuration` from `bodyParams`.
219
+ // Remove `scim_configuration` from `bodyParams`, because `connections.create` doesn't accept it.
220
+ const { scim_configuration : scimBodyParams } = bodyParams ;
221
+ delete bodyParams . scim_configuration ;
222
+
223
+ // First, create the new `connection`.
224
+ const created = await this . connectionsManager . create ( bodyParams ) ;
225
+ if ( scimBodyParams ) {
226
+ // Now, create the `scim_configuration` for newly created `connection`.
227
+ await this . createScimConfiguration ( { id : created . id } , scimBodyParams ) ;
228
+ }
229
+
230
+ // Return response from connections.create(...).
231
+ return created ;
232
+ }
233
+ }
0 commit comments