scimgateway 4.2.3 → 4.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.travis.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  language: node_js
2
2
 
3
3
  node_js:
4
- - "10"
4
+ - "14"
5
5
 
6
6
  sudo: false
7
7
 
package/README.md CHANGED
@@ -1165,6 +1165,24 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1165
1165
 
1166
1166
  ## Change log
1167
1167
 
1168
+ ### v4.2.6
1169
+
1170
+ [Fixed]
1171
+
1172
+ - cosmetics related to 401 error handling introduced in v4.2.4
1173
+
1174
+ ### v4.2.5
1175
+
1176
+ [Fixed]
1177
+
1178
+ - travis test build cosmetics
1179
+
1180
+ ### v4.2.4
1181
+
1182
+ [Added]
1183
+
1184
+ - provided plugins now supports Auth PassThrough. See helpers methods like getClientIdentifier(), getCtxAuth() and changes in doRequest() and getServiceClient(). In general, PassThrough is supported for both basic and bearer auth. Password/secret/client_secret are then not needed in configuration file. Username may still be needed in configuration file depended on how logic is implemented (ref. mongodb/mssql) and what auth beeing used (basic/bearer). Plugin scim, api and azure-ad are all REST plugins having the same helpers (but, some minor differences to azure-ad using OAuth and the getAccessToken() method)
1185
+
1168
1186
  ### v4.2.3
1169
1187
 
1170
1188
  [Fixed]
package/lib/plugin-api.js CHANGED
@@ -77,7 +77,7 @@ scimgateway.postApi = async (baseEntity, apiObj, ctx) => {
77
77
  Excerpt: apiObj.userID
78
78
  }
79
79
  try {
80
- const response = await doRequest(baseEntity, method, path, body)
80
+ const response = await doRequest(baseEntity, method, path, body, ctx)
81
81
  return response.body
82
82
  } catch (err) {
83
83
  const newErr = err
@@ -111,7 +111,7 @@ scimgateway.putApi = async (baseEntity, id, apiObj, ctx) => {
111
111
  Excerpt: apiObj.userID
112
112
  }
113
113
  try {
114
- const response = await doRequest(baseEntity, method, path, body)
114
+ const response = await doRequest(baseEntity, method, path, body, ctx)
115
115
  return response.body
116
116
  } catch (err) {
117
117
  const newErr = err
@@ -144,7 +144,7 @@ scimgateway.patchApi = async (baseEntity, id, apiObj, ctx) => {
144
144
  if (apiObj.userID) body.Excerpt = apiObj.userID
145
145
 
146
146
  try { // note, Books example do not support patch
147
- const response = await doRequest(baseEntity, method, path, body)
147
+ const response = await doRequest(baseEntity, method, path, body, ctx)
148
148
  return response.body
149
149
  } catch (err) {
150
150
  const newErr = err
@@ -170,13 +170,13 @@ scimgateway.getApi = async (baseEntity, id, apiQuery, apiObj, ctx) => {
170
170
  if (id) {
171
171
  const path = `/api/v1/Books/${id}`
172
172
  const body = null
173
- const response = await doRequest(baseEntity, method, path, body)
173
+ const response = await doRequest(baseEntity, method, path, body, ctx)
174
174
  return response.body
175
175
  } else {
176
176
  const path = '/api/Books'
177
177
  const body = null
178
178
  if (apiQuery) { /* some logic here */ }
179
- const response = await doRequest(baseEntity, method, path, body)
179
+ const response = await doRequest(baseEntity, method, path, body, ctx)
180
180
  return response.body
181
181
  }
182
182
  } catch (err) {
@@ -201,7 +201,7 @@ scimgateway.deleteApi = async (baseEntity, id, ctx) => {
201
201
  const body = null
202
202
 
203
203
  try {
204
- const response = await doRequest(baseEntity, method, path, body)
204
+ const response = await doRequest(baseEntity, method, path, body, ctx)
205
205
  return response.body
206
206
  } catch (err) {
207
207
  const newErr = err
@@ -213,6 +213,24 @@ scimgateway.deleteApi = async (baseEntity, id, ctx) => {
213
213
  // helpers
214
214
  // =================================================
215
215
 
216
+ const getClientIdentifier = (ctx) => {
217
+ if (!ctx?.request?.header?.authorization) return undefined
218
+ const [user, secret] = getCtxAuth(ctx)
219
+ return `${encodeURIComponent(user)}_${encodeURIComponent(secret)}` // user_password or undefined_password
220
+ }
221
+
222
+ //
223
+ // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
224
+ //
225
+ const getCtxAuth = (ctx) => { // eslint-disable-line
226
+ if (!ctx?.request?.header?.authorization) return []
227
+ const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
228
+ let username, password
229
+ if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
230
+ if (username) return [username, password] // basic auth
231
+ else return [undefined, authToken] // bearer auth
232
+ }
233
+
216
234
  //
217
235
  // getServiceClient - returns options needed for connection parameters
218
236
  //
@@ -222,7 +240,7 @@ scimgateway.deleteApi = async (baseEntity, id, ctx) => {
222
240
  // path = url e.g. "http(s)://<host>:<port>/xxx/yyy", then using the url host/port/protocol
223
241
  // opt (options) may be needed e.g {auth: {username: "username", password: "password"} }
224
242
  //
225
- const getServiceClient = async (baseEntity, method, path, opt) => {
243
+ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
226
244
  const action = 'getServiceClient'
227
245
 
228
246
  let urlObj
@@ -233,15 +251,15 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
233
251
  //
234
252
  // path (no url) - default approach and client will be cached based on config
235
253
  //
236
- if (_serviceClient[baseEntity]) { // serviceClient already exist
254
+ const clientIdentifier = getClientIdentifier(ctx)
255
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist
237
256
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using existing client`)
238
257
  } else {
239
258
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Client have to be created`)
240
259
  let client = null
241
260
  if (config.entity && config.entity[baseEntity]) client = config.entity[baseEntity]
242
261
  if (!client) {
243
- const err = new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`)
244
- throw err
262
+ throw new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`)
245
263
  }
246
264
 
247
265
  urlObj = new URL(config.entity[baseEntity].baseUrls[0])
@@ -251,7 +269,8 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
251
269
  json: true, // json-object response instead of string
252
270
  headers: {
253
271
  'Content-Type': 'application/json',
254
- Authorization: 'Basic ' + Buffer.from(`${config.entity[baseEntity].username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)}`).toString('base64')
272
+ // Auth PassThrough or configuration, using ctx "AS-IS" header for PassThrough. For more advanced logic use getCtxAuth(ctx) - see examples in other plugins
273
+ Authorization: ctx?.request?.header?.authorization ? ctx.request.header.authorization : 'Basic ' + Buffer.from(`${config.entity[baseEntity].username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)}`).toString('base64')
255
274
  },
256
275
  host: urlObj.hostname,
257
276
  port: urlObj.port, // null if https and 443 defined in url
@@ -271,13 +290,14 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
271
290
  }
272
291
 
273
292
  if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
274
- _serviceClient[baseEntity] = param // serviceClient created
293
+ if (!_serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = {}
294
+ _serviceClient[baseEntity][clientIdentifier] = param // serviceClient created
275
295
  }
276
296
 
277
- const cli = scimgateway.copyObj(_serviceClient[baseEntity]) // client ready
297
+ const cli = scimgateway.copyObj(_serviceClient[baseEntity][clientIdentifier]) // client ready
278
298
 
279
299
  // failover support
280
- path = _serviceClient[baseEntity].baseUrl + path
300
+ path = _serviceClient[baseEntity][clientIdentifier].baseUrl + path
281
301
  urlObj = new URL(path)
282
302
  cli.options.host = urlObj.hostname
283
303
  cli.options.port = urlObj.port
@@ -330,16 +350,16 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
330
350
  return cli // final client
331
351
  }
332
352
 
333
- const updateServiceClient = (baseEntity, obj) => {
334
- if (_serviceClient[baseEntity]) _serviceClient[baseEntity] = scimgateway.extendObj(_serviceClient[baseEntity], obj) // merge with argument options
353
+ const updateServiceClient = (baseEntity, clientIdentifier, obj) => {
354
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = scimgateway.extendObj(_serviceClient[baseEntity][clientIdentifier], obj) // merge with argument options
335
355
  }
336
356
 
337
357
  //
338
358
  // doRequest - execute REST service
339
359
  //
340
- const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
360
+ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) => {
341
361
  try {
342
- const cli = await getServiceClient(baseEntity, method, path, opt)
362
+ const cli = await getServiceClient(baseEntity, method, path, opt, ctx)
343
363
  const options = cli.options
344
364
  const result = await new Promise((resolve, reject) => {
345
365
  let dataString = ''
@@ -394,15 +414,18 @@ const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
394
414
  return result
395
415
  } catch (err) { // includes failover/retry logic based on config baseUrls array
396
416
  scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
417
+ let statusCode
418
+ try { statusCode = JSON.parse(err.message).statusCode } catch (e) {}
419
+ const clientIdentifier = getClientIdentifier(ctx)
397
420
  if (!retryCount) retryCount = 0
398
421
  let urlObj
399
422
  try { urlObj = new URL(path) } catch (err) {}
400
423
  if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND')) {
401
424
  if (retryCount < config.entity[baseEntity].baseUrls.length) {
402
425
  retryCount++
403
- updateServiceClient(baseEntity, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
426
+ updateServiceClient(baseEntity, clientIdentifier, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
404
427
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${(config.entity[baseEntity].baseUrls.length > 1) ? 'failover ' : ''}retry[${retryCount}] using baseUrl = ${_serviceClient[baseEntity].baseUrl}`)
405
- const ret = await doRequest(baseEntity, method, path, body, opt, retryCount) // retry
428
+ const ret = await doRequest(baseEntity, method, path, body, ctx, opt, retryCount) // retry
406
429
  return ret // problem fixed
407
430
  } else {
408
431
  const newerr = new Error(err.message)
@@ -410,7 +433,10 @@ const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
410
433
  newerr.message = newerr.message.replace('ENOTFOUND', 'UnableConnectingHost') // avoid returning ENOTFOUND error
411
434
  throw newerr
412
435
  }
413
- } else throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
436
+ } else {
437
+ if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
438
+ throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
439
+ }
414
440
  }
415
441
  } // doRequest
416
442
 
@@ -128,7 +128,9 @@ for (const key in config.map.user) { // userAttributes = ['country', 'preferredL
128
128
  if (config.map.user[key].mapTo) userAttributes.push(config.map.user[key].mapTo)
129
129
  }
130
130
 
131
- const graphUrl = 'https://graph.microsoft.com/beta' // beta instead ov 'v1.0' gives all user attributes when no $select
131
+ for (const baseEntity in config.entity) { // ensure we have baseUrls in config and for this azure-ad plugin we overwrite any existing to hardcoded value ['https://graph.microsoft.com/beta']
132
+ config.entity[baseEntity].baseUrls = ['https://graph.microsoft.com/beta'] // beta instead of 'v1.0' gives all user attributes when no $select
133
+ }
132
134
 
133
135
  const _serviceClient = {}
134
136
  const lock = new scimgateway.Lock()
@@ -211,9 +213,9 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
211
213
  let response
212
214
  if (path === 'getUser') { // special
213
215
  response = { body: { value: [] } }
214
- const userObj = await getUser(baseEntity, getObj.value, attributes)
216
+ const userObj = await getUser(baseEntity, getObj.value, attributes, ctx)
215
217
  if (userObj) response.body.value.push(userObj)
216
- } else response = await doRequest(baseEntity, method, path, body)
218
+ } else response = await doRequest(baseEntity, method, path, body, ctx)
217
219
  if (!response.body.value) {
218
220
  throw new Error(`invalid response: ${JSON.stringify(response)}`)
219
221
  }
@@ -258,7 +260,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
258
260
  const [body] = scimgateway.endpointMapper('outbound', userObj, config.map.user)
259
261
 
260
262
  try {
261
- await doRequest(baseEntity, method, path, body)
263
+ await doRequest(baseEntity, method, path, body, ctx)
262
264
  if (Object.keys(addonObj).length > 0) {
263
265
  await scimgateway.modifyUser(baseEntity, userObj.userName, addonObj, ctx) // manager, servicePlan
264
266
  return null
@@ -281,7 +283,7 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
281
283
  const body = null
282
284
 
283
285
  try {
284
- await doRequest(baseEntity, method, path, body)
286
+ await doRequest(baseEntity, method, path, body, ctx)
285
287
  return (null)
286
288
  } catch (err) {
287
289
  throw new Error(`${action} error: ${err.message}`)
@@ -326,7 +328,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
326
328
  const method = 'GET'
327
329
  const path = `/users/${id}?$select=${key}`
328
330
  try {
329
- const res = await doRequest(baseEntity, method, path, null)
331
+ const res = await doRequest(baseEntity, method, path, null, ctx)
330
332
  if (res && res.body && res.body[key]) {
331
333
  const fullKeyObj = Object.assign(res.body[key], parsedAttrObj[key]) // merge original with modified
332
334
  if (fullKeyObj && Object.keys(fullKeyObj).length > 0) {
@@ -341,7 +343,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
341
343
  const method = 'PATCH'
342
344
  const path = `/users/${id}`
343
345
  try {
344
- await doRequest(baseEntity, method, path, parsedAttrObj)
346
+ await doRequest(baseEntity, method, path, parsedAttrObj, ctx)
345
347
  resolve(null)
346
348
  } catch (err) {
347
349
  return reject(err)
@@ -357,6 +359,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
357
359
  let path = null
358
360
  let body = null
359
361
  if (objManager.manager) { // new manager
362
+ const graphUrl = config.entity[baseEntity].baseUrls[0]
360
363
  method = 'PUT'
361
364
  path = `/users/${id}/manager/$ref`
362
365
  body = { '@odata.id': `${graphUrl}/users/${objManager.manager}` }
@@ -366,7 +369,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
366
369
  body = null
367
370
  } else return resolve(null)
368
371
  try {
369
- await doRequest(baseEntity, method, path, body)
372
+ await doRequest(baseEntity, method, path, body, ctx)
370
373
  resolve(null)
371
374
  } catch (err) {
372
375
  return reject(err)
@@ -387,7 +390,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
387
390
  try { // build currentLic
388
391
  let response
389
392
  try {
390
- response = await doRequest(baseEntity, method, path, null)
393
+ response = await doRequest(baseEntity, method, path, null, ctx)
391
394
  } catch (err) {
392
395
  let statusCode
393
396
  try { statusCode = JSON.parse(err.message).statusCode } catch (e) {}
@@ -419,7 +422,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
419
422
  const addLic = {}
420
423
  const removeLic = {}
421
424
 
422
- response = await doRequest(baseEntity, method, path, null)
425
+ response = await doRequest(baseEntity, method, path, null, ctx)
423
426
  if (!response.body.value) {
424
427
  const err = new Error(`${action}: Got empty response on REST request`)
425
428
  return reject(err)
@@ -504,7 +507,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
504
507
  method = 'POST'
505
508
  path = `/users/${id}/assignLicense`
506
509
  const body = lic
507
- await doRequest(baseEntity, method, path, body)
510
+ await doRequest(baseEntity, method, path, body, ctx)
508
511
 
509
512
  resolve(null)
510
513
  } catch (err) {
@@ -611,7 +614,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
611
614
  if (!path) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
612
615
 
613
616
  try {
614
- let response = await doRequest(baseEntity, method, path, body)
617
+ let response = await doRequest(baseEntity, method, path, body, ctx)
615
618
  if (!response.body) {
616
619
  throw new Error(`invalid response: ${JSON.stringify(response)}`)
617
620
  }
@@ -672,7 +675,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
672
675
  if (res && res.Resources && res.Resources.length > 0) {
673
676
  throw new Error(`group ${groupObj.displayName} already exist`)
674
677
  }
675
- await doRequest(baseEntity, method, path, body)
678
+ await doRequest(baseEntity, method, path, body, ctx)
676
679
  return null
677
680
  } catch (err) {
678
681
  const newErr = new Error(`${action} error: ${err.message}`)
@@ -720,10 +723,11 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
720
723
  if (arrGrpAdd.length < 1) return resolve(null)
721
724
  const method = 'POST'
722
725
  const path = `/groups/${id}/members/$ref`
726
+ const graphUrl = config.entity[baseEntity].baseUrls[0]
723
727
  for (let i = 0, len = arrGrpAdd.length; i < len; i++) {
724
728
  const body = { '@odata.id': `${graphUrl}/directoryObjects/${arrGrpAdd[i]}` }
725
729
  try {
726
- await doRequest(baseEntity, method, path, body)
730
+ await doRequest(baseEntity, method, path, body, ctx)
727
731
  if (i === len - 1) resolve(null) // loop completed
728
732
  } catch (err) {
729
733
  return reject(err)
@@ -742,7 +746,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
742
746
  for (let i = 0, len = arrGrpDel.length; i < len; i++) {
743
747
  const path = `/groups/${id}/members/${arrGrpDel[i]}/$ref`
744
748
  try {
745
- await doRequest(baseEntity, method, path, body)
749
+ await doRequest(baseEntity, method, path, body, ctx)
746
750
  if (i === len - 1) resolve(null) // loop completed
747
751
  } catch (err) {
748
752
  return reject(err)
@@ -821,7 +825,7 @@ scimgateway.getServicePlans = async (baseEntity, getObj, attributes, ctx) => {
821
825
  response = { body: { value: [] } }
822
826
  path = '/subscribedSkus'
823
827
 
824
- const res = await doRequest(baseEntity, method, path, body)
828
+ const res = await doRequest(baseEntity, method, path, body, ctx)
825
829
  if (!res.body.value) {
826
830
  throw new Error('got empty response on REST request')
827
831
  }
@@ -850,7 +854,7 @@ scimgateway.getServicePlans = async (baseEntity, getObj, attributes, ctx) => {
850
854
  }
851
855
  if (planObj) ret.Resources.push(planObj)
852
856
  } else {
853
- response = await doRequest(baseEntity, method, path, body)
857
+ response = await doRequest(baseEntity, method, path, body, ctx)
854
858
 
855
859
  if (!response.body.value) {
856
860
  throw new Error('got empty response on REST request')
@@ -908,6 +912,24 @@ scimgateway.modifyServicePlan = async (baseEntity, id, ctx) => {
908
912
  // helpers
909
913
  // =================================================
910
914
 
915
+ const getClientIdentifier = (ctx) => {
916
+ if (!ctx?.request?.header?.authorization) return undefined
917
+ const [user, secret] = getCtxAuth(ctx)
918
+ return `${encodeURIComponent(user)}_${encodeURIComponent(secret)}` // user_password or undefined_password
919
+ }
920
+
921
+ //
922
+ // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
923
+ //
924
+ const getCtxAuth = (ctx) => {
925
+ if (!ctx?.request?.header?.authorization) return []
926
+ const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
927
+ let username, password
928
+ if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
929
+ if (username) return [username, password] // basic auth
930
+ else return [undefined, authToken] // bearer auth
931
+ }
932
+
911
933
  //
912
934
  // getServiceClient - returns options needed for connection parameters
913
935
  //
@@ -917,7 +939,7 @@ scimgateway.modifyServicePlan = async (baseEntity, id, ctx) => {
917
939
  // path = url e.g. "http(s)://<host>:<port>/xxx/yyy", then using the url host/port/protocol
918
940
  // opt (options) may be needed e.g {auth: {username: "username", password: "password"} }
919
941
  //
920
- const getServiceClient = async (baseEntity, method, path, opt) => {
942
+ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
921
943
  const action = 'getServiceClient'
922
944
 
923
945
  let urlObj
@@ -928,17 +950,20 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
928
950
  //
929
951
  // path (no url) - default approach and client will be cached based on config
930
952
  //
931
- if (_serviceClient[baseEntity] && _serviceClient[baseEntity].accessToken) { // serviceClient already exist - Azure plugin specific
953
+
954
+ const clientIdentifier = getClientIdentifier(ctx)
955
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken) { // serviceClient already exist - Azure plugin specific
932
956
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using existing client`)
933
957
  // check if token refresh is needed
934
958
  const d = new Date() / 1000 // seconds (unix time)
935
- if (_serviceClient[baseEntity].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
936
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Accesstoken about to expire in ${_serviceClient[baseEntity].accessToken.validTo - d} seconds`)
959
+ if (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
960
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Accesstoken about to expire in ${_serviceClient[baseEntity][clientIdentifier].accessToken.validTo - d} seconds`)
937
961
  try {
938
- const accessToken = await getAccessToken(baseEntity)
939
- _serviceClient[baseEntity].accessToken = accessToken
940
- _serviceClient[baseEntity].options.headers.Authorization = ` Bearer ${accessToken.access_token}`
962
+ const accessToken = await getAccessToken(baseEntity, ctx)
963
+ _serviceClient[baseEntity][clientIdentifier].accessToken = accessToken
964
+ _serviceClient[baseEntity][clientIdentifier].options.headers.Authorization = ` Bearer ${accessToken.access_token}`
941
965
  } catch (err) {
966
+ delete _serviceClient[baseEntity][clientIdentifier]
942
967
  const newErr = err
943
968
  throw newErr
944
969
  }
@@ -953,8 +978,7 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
953
978
  }
954
979
 
955
980
  // Azure plugin specific
956
- const accessToken = await getAccessToken(baseEntity)
957
- if (!config.entity[baseEntity].baseUrls) config.entity[baseEntity].baseUrls = [graphUrl] // Azure plugin avoid config file and keep baseUrls logic
981
+ const accessToken = await getAccessToken(baseEntity, ctx)
958
982
 
959
983
  urlObj = new URL(config.entity[baseEntity].baseUrls[0])
960
984
  const param = {
@@ -983,18 +1007,20 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
983
1007
  }
984
1008
 
985
1009
  if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
986
- _serviceClient[baseEntity] = param // serviceClient created
1010
+ if (!_serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = {}
987
1011
 
988
- // Azure plugin specific
1012
+ _serviceClient[baseEntity][clientIdentifier] = param // serviceClient created
1013
+
1014
+ // Azure plugin specific (note, not using [clientIdentifier])
989
1015
  _serviceClient[baseEntity].nextLink = {}
990
1016
  _serviceClient[baseEntity].nextLink.users = null // Azure users pagination
991
1017
  _serviceClient[baseEntity].nextLink.groups = null // Azure groups pagination
992
1018
  }
993
1019
 
994
- const cli = scimgateway.copyObj(_serviceClient[baseEntity]) // client ready
1020
+ const cli = scimgateway.copyObj(_serviceClient[baseEntity][clientIdentifier]) // client ready
995
1021
 
996
1022
  // failover support
997
- path = _serviceClient[baseEntity].baseUrl + path
1023
+ path = _serviceClient[baseEntity][clientIdentifier].baseUrl + path
998
1024
  urlObj = new URL(path)
999
1025
  cli.options.host = urlObj.hostname
1000
1026
  cli.options.port = urlObj.port
@@ -1047,16 +1073,16 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
1047
1073
  return cli // final client
1048
1074
  }
1049
1075
 
1050
- const updateServiceClient = (baseEntity, obj) => {
1051
- if (_serviceClient[baseEntity]) _serviceClient[baseEntity] = scimgateway.extendObj(_serviceClient[baseEntity], obj) // merge with argument options
1076
+ const updateServiceClient = (baseEntity, clientIdentifier, obj) => {
1077
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = scimgateway.extendObj(_serviceClient[baseEntity][clientIdentifier], obj) // merge with argument options
1052
1078
  }
1053
1079
 
1054
1080
  //
1055
1081
  // doRequest - execute REST service
1056
1082
  //
1057
- const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
1083
+ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) => {
1058
1084
  try {
1059
- const cli = await getServiceClient(baseEntity, method, path, opt)
1085
+ const cli = await getServiceClient(baseEntity, method, path, opt, ctx)
1060
1086
  const options = cli.options
1061
1087
 
1062
1088
  const result = await new Promise((resolve, reject) => {
@@ -1116,15 +1142,16 @@ const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
1116
1142
  if (statusCode === 404) { // not logged as error, let caller decide e.g. getUser-manager
1117
1143
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
1118
1144
  } else scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
1145
+ const clientIdentifier = getClientIdentifier(ctx)
1119
1146
  if (!retryCount) retryCount = 0
1120
1147
  let urlObj
1121
1148
  try { urlObj = new URL(path) } catch (err) {}
1122
1149
  if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')) {
1123
1150
  if (retryCount < config.entity[baseEntity].baseUrls.length) {
1124
1151
  retryCount++
1125
- updateServiceClient(baseEntity, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
1152
+ updateServiceClient(baseEntity, clientIdentifier, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
1126
1153
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${(config.entity[baseEntity].baseUrls.length > 1) ? 'failover ' : ''}retry[${retryCount}] using baseUrl = ${_serviceClient[baseEntity].baseUrl}`)
1127
- const ret = await doRequest(baseEntity, method, path, body, opt, retryCount) // retry
1154
+ const ret = await doRequest(baseEntity, method, path, body, ctx, opt, retryCount) // retry
1128
1155
  return ret // problem fixed
1129
1156
  } else {
1130
1157
  const newerr = new Error(err.message)
@@ -1132,17 +1159,21 @@ const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
1132
1159
  newerr.message = newerr.message.replace('ENOTFOUND', 'UnableConnectingHost') // avoid returning ENOTFOUND error
1133
1160
  throw newerr
1134
1161
  }
1135
- } else throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
1162
+ } else {
1163
+ if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
1164
+ throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
1165
+ }
1136
1166
  }
1137
1167
  } // doRequest
1138
1168
 
1139
1169
  //
1140
1170
  // getAccessToken - returns oauth jwt accesstoken
1141
1171
  //
1142
- const getAccessToken = async (baseEntity) => {
1172
+ const getAccessToken = async (baseEntity, ctx) => {
1143
1173
  await lock.acquire()
1174
+ const clientIdentifier = getClientIdentifier(ctx)
1144
1175
  const d = new Date() / 1000 // seconds (unix time)
1145
- if (_serviceClient[baseEntity] && _serviceClient[baseEntity].accessToken &&
1176
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken &&
1146
1177
  (_serviceClient[baseEntity].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
1147
1178
  lock.release()
1148
1179
  return _serviceClient[baseEntity].accessToken
@@ -1154,10 +1185,11 @@ const getAccessToken = async (baseEntity) => {
1154
1185
  const req = `https://login.microsoftonline.com/${config.entity[baseEntity].tenantIdGUID}/oauth2/token`
1155
1186
  const method = 'POST'
1156
1187
 
1157
- const form = { // to be query string formatted
1188
+ const [, secret] = getCtxAuth(ctx) // if Auth PassTrough, secret from basic or bearer auth
1189
+ const form = { // query string formatted
1158
1190
  grant_type: 'client_credentials',
1159
1191
  client_id: config.entity[baseEntity].clientId,
1160
- client_secret: scimgateway.getPassword(`endpoint.entity.${baseEntity}.clientSecret`, configFile),
1192
+ client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.clientSecret`, configFile), // using config if no Auth PassThrough
1161
1193
  resource: 'https://graph.microsoft.com'
1162
1194
  }
1163
1195
 
@@ -1168,7 +1200,7 @@ const getAccessToken = async (baseEntity) => {
1168
1200
  }
1169
1201
 
1170
1202
  try {
1171
- const response = await doRequest(baseEntity, method, req, form, options)
1203
+ const response = await doRequest(baseEntity, method, req, form, ctx, options)
1172
1204
  if (!response.body) {
1173
1205
  const err = new Error(`[${action}] No data retrieved from: ${method} ${req}`)
1174
1206
  throw (err)
@@ -1194,7 +1226,7 @@ const getAccessToken = async (baseEntity) => {
1194
1226
  }
1195
1227
  }
1196
1228
 
1197
- const getUser = async (baseEntity, uid, attributes) => { // uid = id, userName (upn) or externalId (upn)
1229
+ const getUser = async (baseEntity, uid, attributes, ctx) => { // uid = id, userName (upn) or externalId (upn)
1198
1230
  if (attributes.length < 1) {
1199
1231
  attributes = userAttributes
1200
1232
  }
@@ -1207,7 +1239,7 @@ const getUser = async (baseEntity, uid, attributes) => { // uid = id, userName (
1207
1239
  const path = `/users/${querystring.escape(uid)}?$expand=manager($select=userPrincipalName)` // beta returns all attributes or use: ?$select=${attrs.join()}
1208
1240
  const body = null
1209
1241
  try {
1210
- const response = await doRequest(baseEntity, method, path, body)
1242
+ const response = await doRequest(baseEntity, method, path, body, ctx)
1211
1243
  const userObj = response.body
1212
1244
  if (!userObj) {
1213
1245
  const err = new Error('Got empty response when retrieving data for ' + uid)
@@ -1237,7 +1269,7 @@ const getUser = async (baseEntity, uid, attributes) => { // uid = id, userName (
1237
1269
  const retObj = { servicePlan: [] }
1238
1270
 
1239
1271
  try {
1240
- const response = await doRequest(baseEntity, method, path, body)
1272
+ const response = await doRequest(baseEntity, method, path, body, ctx)
1241
1273
  if (!response.body.value) {
1242
1274
  const err = new Error('No content for license information ' + uid)
1243
1275
  return reject(err)