scimgateway 4.2.2 → 4.2.5

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.
@@ -64,7 +64,10 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
64
64
 
65
65
  const wsdlDir = path.join(`${configDir}`, 'wsdls')
66
66
  const endpointUsername = config.username
67
- const endpointPassword = scimgateway.getPassword('endpoint.password', configFile)
67
+ let endpointPassword
68
+ if (!scimgateway.authPassThroughAllowed) { // not using Auth PassThrough
69
+ endpointPassword = scimgateway.getPassword('endpoint.password', configFile)
70
+ }
68
71
  const _serviceClient = {}
69
72
 
70
73
  // =================================================
@@ -119,7 +122,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
119
122
  totalResults: null // not used - paging not supported
120
123
  }
121
124
 
122
- const serviceClient = await getServiceClient(baseEntity, soapAction)
125
+ const serviceClient = await getServiceClient(baseEntity, soapAction, ctx)
123
126
 
124
127
  let result = await serviceClient[config[soapAction].method + 'Async'](soapRequest)
125
128
  if (!Array.isArray(result) || result.length < 4) {
@@ -221,7 +224,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
221
224
  }
222
225
  }
223
226
 
224
- const serviceClient = await getServiceClient(baseEntity, action)
227
+ const serviceClient = await getServiceClient(baseEntity, action, ctx)
225
228
 
226
229
  let result = await serviceClient[config[action].method + 'Async'](soapRequest)
227
230
  if (!Array.isArray(result) || result.length < 4) {
@@ -246,7 +249,7 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
246
249
  const action = 'deleteUser'
247
250
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
248
251
  try {
249
- const serviceClient = await getServiceClient(baseEntity, action)
252
+ const serviceClient = await getServiceClient(baseEntity, action, ctx)
250
253
 
251
254
  const soapRequest = { userID: id }
252
255
 
@@ -311,7 +314,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
311
314
  } else userObj[key1] = attrObj[key1] // merge modified attr into userObj
312
315
  }
313
316
 
314
- const serviceClient = await getServiceClient(baseEntity, action)
317
+ const serviceClient = await getServiceClient(baseEntity, action, ctx)
315
318
 
316
319
  const soapRequest = {
317
320
  user: {
@@ -394,7 +397,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
394
397
  }
395
398
 
396
399
  try {
397
- const serviceClient = await getServiceClient(baseEntity, soapAction)
400
+ const serviceClient = await getServiceClient(baseEntity, soapAction, ctx)
398
401
 
399
402
  let result = await serviceClient[config[soapAction].method + 'Async'](soapRequest)
400
403
  if (!Array.isArray(result) || result.length < 4) {
@@ -482,7 +485,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
482
485
  }
483
486
 
484
487
  try {
485
- const serviceClient = await getServiceClient(baseEntity, action)
488
+ const serviceClient = await getServiceClient(baseEntity, action, ctx)
486
489
 
487
490
  attrObj.members.forEach(async function (el) {
488
491
  if (el.operation && el.operation === 'delete') { // delete member from group
@@ -526,7 +529,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
526
529
  // helpers
527
530
  // =================================================
528
531
 
529
- const getServiceClient = async (baseEntity, action) => {
532
+ const getServiceClient = async (baseEntity, action, ctx) => {
530
533
  try {
531
534
  const entityService = config[action].service
532
535
 
@@ -575,13 +578,19 @@ const getServiceClient = async (baseEntity, action) => {
575
578
 
576
579
  try {
577
580
  const serviceClient = await soap.createClientAsync(urlToWsdl, wsdlOptions)
578
- serviceClient.setSecurity(new soap.WSSecurity(endpointUsername, endpointPassword, { passwordType: 'PasswordText', hasTimeStamp: false })) // ForwardInc using WSSecurity
581
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough
582
+ const [user, secret] = getCtxAuth(ctx)
583
+ serviceClient.setSecurity(new soap.WSSecurity(user || endpointUsername, secret, { passwordType: 'PasswordText', hasTimeStamp: false })) // ForwardInc using WSSecurity
584
+ } else {
585
+ serviceClient.setSecurity(new soap.WSSecurity(endpointUsername, endpointPassword, { passwordType: 'PasswordText', hasTimeStamp: false })) // ForwardInc using WSSecurity
586
+ }
579
587
  serviceClient.addSoapHeader(customHeader)
580
588
  serviceClient.setEndpoint(serviceEndpoint) // https://FQDN/path/to/service (wsdl name without ?wsdl extension)
581
-
582
- if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
583
- _serviceClient[baseEntity][entityService] = serviceClient // serviceClient created
584
- return _serviceClient[baseEntity][entityService]
589
+ if (!ctx?.request?.header?.authorization) { // not using Auth PassThrough, store serviceClient and will be reused on subsequent requests
590
+ if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
591
+ _serviceClient[baseEntity][entityService] = serviceClient // serviceClient created
592
+ }
593
+ return serviceClient
585
594
  } catch (err) {
586
595
  if (err.message) throw new Error(`createClient ${urlToWsdl} errorMessage: ${err.message}`)
587
596
  else throw new Error(`createClient ${urlToWsdl} errorMessage: invalid service definition - wsdl maybe not found?`)
@@ -590,7 +599,19 @@ const getServiceClient = async (baseEntity, action) => {
590
599
  const newErr = err
591
600
  throw newErr
592
601
  }
593
- } // getServiceClient
602
+ }
603
+
604
+ //
605
+ // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
606
+ //
607
+ const getCtxAuth = (ctx) => {
608
+ if (!ctx?.request?.header?.authorization) return []
609
+ const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
610
+ let username, password
611
+ if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
612
+ if (username) return [username, password] // basic auth
613
+ else return [undefined, authToken] // bearer auth
614
+ }
594
615
 
595
616
  //
596
617
  // Cleanup on exit
@@ -225,7 +225,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
225
225
  if (!ldapOptions) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
226
226
 
227
227
  try {
228
- const users = await doRequest(baseEntity, method, base, ldapOptions) // ignoring SCIM paging startIndex/count - get all
228
+ const users = await doRequest(baseEntity, method, base, ldapOptions, ctx) // ignoring SCIM paging startIndex/count - get all
229
229
  result.totalResults = users.length
230
230
  result.Resources = await Promise.all(users.map(async (user) => { // Promise.all because of async map
231
231
  if (user.name) delete user.name // because mapper converts to SCIM name.xxx
@@ -245,13 +245,13 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
245
245
  try {
246
246
  if (Array.isArray(user.memberOf)) {
247
247
  for (let i = 0; i < user.memberOf.length; i++) {
248
- const id = await dnToSidGuid(baseEntity, user.memberOf[i])
248
+ const id = await dnToSidGuid(baseEntity, user.memberOf[i], ctx)
249
249
  if (!id) throw new Error(`dnToGuid did not return any objectGUID value for dn=${user.memberOf[i]}`)
250
250
  arr.push(id)
251
251
  }
252
252
  user.memberOf = arr
253
253
  } else {
254
- const id = await dnToSidGuid(baseEntity, user.memberOf)
254
+ const id = await dnToSidGuid(baseEntity, user.memberOf, ctx)
255
255
  if (!id) throw new Error(`dnToGuid did not return any objectGUID value for dn=${user.memberOf}`)
256
256
  user.memberOf = [id]
257
257
  }
@@ -322,7 +322,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
322
322
  const ldapOptions = endpointObj
323
323
 
324
324
  try {
325
- await doRequest(baseEntity, method, base, ldapOptions)
325
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
326
326
  return null
327
327
  } catch (err) {
328
328
  const newErr = new Error(`${action} error: ${err.message}`)
@@ -351,7 +351,7 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
351
351
  const ldapOptions = {}
352
352
 
353
353
  try {
354
- await doRequest(baseEntity, method, base, ldapOptions)
354
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
355
355
  return null
356
356
  } catch (err) {
357
357
  throw new Error(`${action} error: ${err.message}`)
@@ -403,14 +403,14 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
403
403
  operation: 'add',
404
404
  modification: body.add
405
405
  }
406
- await doRequest(baseEntity, method, base, ldapOptions)
406
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
407
407
  }
408
408
  if (body.remove[groupsAttr].length > 0) {
409
409
  const ldapOptions = {
410
410
  operation: 'delete',
411
411
  modification: body.remove
412
412
  }
413
- await doRequest(baseEntity, method, base, ldapOptions)
413
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
414
414
  }
415
415
  } catch (err) {
416
416
  throw new Error(`${action} error: ${err.message}`)
@@ -442,7 +442,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
442
442
  attributes: activeAttr
443
443
  }
444
444
 
445
- const users = await doRequest(baseEntity, method, base, ldapOptions)
445
+ const users = await doRequest(baseEntity, method, base, ldapOptions, ctx)
446
446
  if (users.length === 0) throw new Error(`${action} error: ${id} not found`)
447
447
  else if (users.length > 1) throw new Error(`${action} error: ${ldapOptions.filter} returned more than one user for ${id}`)
448
448
  const usr = users[0]
@@ -480,7 +480,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
480
480
  }
481
481
 
482
482
  try {
483
- await doRequest(baseEntity, method, base, ldapOptions)
483
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
484
484
  return null
485
485
  } catch (err) {
486
486
  throw new Error(`${action} error: ${err.message}`)
@@ -601,22 +601,22 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
601
601
  if (!ldapOptions) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
602
602
 
603
603
  try {
604
- if (ldapOptions === 'getMemberOfGroups') result.Resources = await getMemberOfGroups(baseEntity, getObj.value)
604
+ if (ldapOptions === 'getMemberOfGroups') result.Resources = await getMemberOfGroups(baseEntity, getObj.value, ctx)
605
605
  else {
606
- const groups = await doRequest(baseEntity, method, base, ldapOptions)
606
+ const groups = await doRequest(baseEntity, method, base, ldapOptions, ctx)
607
607
  result.Resources = await Promise.all(groups.map(async (group) => { // Promise.all because of async map
608
608
  if (config.useSID_id || config.useGUID_id) {
609
609
  if (group.member) {
610
610
  const arr = []
611
611
  if (Array.isArray(group.member)) {
612
612
  for (let i = 0; i < group.member.length; i++) {
613
- const id = await dnToSidGuid(baseEntity, group.member[i])
613
+ const id = await dnToSidGuid(baseEntity, group.member[i], ctx)
614
614
  if (!id) throw new Error(`dnToSidGuid() did not return any ${config.useSID_id ? 'objectSid' : 'objectGUID'} value for dn=${group.member[i]}`)
615
615
  arr.push(id)
616
616
  }
617
617
  group.member = arr
618
618
  } else {
619
- const id = await dnToSidGuid(baseEntity, group.member)
619
+ const id = await dnToSidGuid(baseEntity, group.member, ctx)
620
620
  if (!id) throw new Error(`dnToSidGuid() did not return any ${config.useSID_id ? 'objectSid' : 'objectGUID'} value for ${group.member}`)
621
621
  group.member = [id]
622
622
  }
@@ -654,7 +654,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
654
654
  const ldapOptions = endpointObj
655
655
 
656
656
  try {
657
- await doRequest(baseEntity, method, base, ldapOptions)
657
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
658
658
  return null
659
659
  } catch (err) {
660
660
  const newErr = new Error(`${action} error: ${err.message}`)
@@ -684,7 +684,7 @@ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
684
684
  const ldapOptions = {}
685
685
 
686
686
  try {
687
- await doRequest(baseEntity, method, base, ldapOptions)
687
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
688
688
  return null
689
689
  } catch (err) {
690
690
  throw new Error(`${action} error: ${err.message}`)
@@ -716,7 +716,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
716
716
  for (let i = 0; i < attrObj.members.length; i++) {
717
717
  const el = attrObj.members[i]
718
718
  if (config.useSID_id || config.useGUID_id) {
719
- const dn = await sidGuidToDn(baseEntity, el.value)
719
+ const dn = await sidGuidToDn(baseEntity, el.value, ctx)
720
720
  if (!dn) throw new Error(`${action} error: sidGuidToDn() did not return any objectGUID value for dn=${el.value}`)
721
721
  el.value = dn
722
722
  }
@@ -739,14 +739,14 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
739
739
  operation: 'add',
740
740
  modification: body.add
741
741
  }
742
- await doRequest(baseEntity, method, base, ldapOptions)
742
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
743
743
  }
744
744
  if (body.remove[memberAttr].length > 0) {
745
745
  const ldapOptions = { // using ldap lookup (dn) instead of search
746
746
  operation: 'delete',
747
747
  modification: body.remove
748
748
  }
749
- await doRequest(baseEntity, method, base, ldapOptions)
749
+ await doRequest(baseEntity, method, base, ldapOptions, ctx)
750
750
  }
751
751
  return null
752
752
  } catch (err) {
@@ -781,7 +781,7 @@ const getObjClassFilter = (baseEntity, type) => {
781
781
  //
782
782
  // dnToSidGuid is used for Active Directory to return objectGUID based on dn
783
783
  //
784
- const dnToSidGuid = async (baseEntity, dn) => {
784
+ const dnToSidGuid = async (baseEntity, dn, ctx) => {
785
785
  const method = 'search'
786
786
  const ldapOptions = {}
787
787
  if (config.useSID_id) ldapOptions.attributes = ['objectSid']
@@ -790,7 +790,7 @@ const dnToSidGuid = async (baseEntity, dn) => {
790
790
 
791
791
  try {
792
792
  const base = dn
793
- const objects = await doRequest(baseEntity, method, base, ldapOptions)
793
+ const objects = await doRequest(baseEntity, method, base, ldapOptions, ctx)
794
794
  if (objects.length !== 1) throw new Error(`did not find unique object having dn=${base}`)
795
795
  if (config.useSID_id) return objects[0].objectSid
796
796
  else return objects[0].objectGUID
@@ -803,7 +803,7 @@ const dnToSidGuid = async (baseEntity, dn) => {
803
803
  //
804
804
  // guidToDn is used for Active Directory to return dn based on objectGUID
805
805
  //
806
- const sidGuidToDn = async (baseEntity, id) => {
806
+ const sidGuidToDn = async (baseEntity, id, ctx) => {
807
807
  const method = 'search'
808
808
  const ldapOptions = {
809
809
  attributes: ['dn']
@@ -818,7 +818,7 @@ const sidGuidToDn = async (baseEntity, id) => {
818
818
  const guid = Buffer.from(id, 'base64').toString('hex')
819
819
  base = `<GUID=${guid}>`
820
820
  } else throw new Error('invalid call to sidGuidToDn(), configuration not using objectSid or objectGUID')
821
- const objects = await doRequest(baseEntity, method, base, ldapOptions)
821
+ const objects = await doRequest(baseEntity, method, base, ldapOptions, ctx)
822
822
  if (objects.length !== 1) throw new Error(`did not find unique object having ${config.useSID_id ? 'objectSid' : 'objectGUID'} =${id}`)
823
823
  return objects[0].dn
824
824
  } catch (err) {
@@ -900,7 +900,7 @@ const convertStringToSid = (sidStr) => {
900
900
  // getMemberOfGroups returns all groups the user is member of
901
901
  // [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
902
902
  //
903
- const getMemberOfGroups = async (baseEntity, id) => {
903
+ const getMemberOfGroups = async (baseEntity, id, ctx) => {
904
904
  const action = 'getMemberOfGroups'
905
905
  if (!config.map.group) throw new Error('missing configuration endpoint.map.group') // not using groups
906
906
 
@@ -922,7 +922,7 @@ const getMemberOfGroups = async (baseEntity, id) => {
922
922
  }
923
923
 
924
924
  try {
925
- const users = await doRequest(baseEntity, method, base, ldapOptions)
925
+ const users = await doRequest(baseEntity, method, base, ldapOptions, ctx)
926
926
  if (users.length !== 1) throw new Error(`${action} error: did not find unique user having ${config.useSID_id ? 'objectSid' : 'objectGUID'} =${id}`)
927
927
  idDn = users[0].dn
928
928
  } catch (err) {
@@ -948,7 +948,7 @@ const getMemberOfGroups = async (baseEntity, id) => {
948
948
  }
949
949
 
950
950
  try {
951
- const groups = await doRequest(baseEntity, method, base, ldapOptions)
951
+ const groups = await doRequest(baseEntity, method, base, ldapOptions, ctx)
952
952
  return groups.map((grp) => {
953
953
  return { // { id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }
954
954
  id: encodeURIComponent(grp[attrs[0]]), // not mandatory, but included anyhow
@@ -962,10 +962,22 @@ const getMemberOfGroups = async (baseEntity, id) => {
962
962
  }
963
963
  }
964
964
 
965
+ //
966
+ // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
967
+ //
968
+ const getCtxAuth = (ctx) => { // eslint-disable-line
969
+ if (!ctx?.request?.header?.authorization) return []
970
+ const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
971
+ let username, password
972
+ if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
973
+ if (username) return [username, password] // basic auth
974
+ else return [undefined, authToken] // bearer auth
975
+ }
976
+
965
977
  //
966
978
  // getServiceClient returns LDAP client used by doRequest
967
979
  //
968
- const getServiceClient = async (baseEntity) => {
980
+ const getServiceClient = async (baseEntity, ctx) => {
969
981
  const action = 'getServiceClient'
970
982
  if (!config.entity[baseEntity].passwordDecrypted) config.entity[baseEntity].passwordDecrypted = scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)
971
983
  if (!config.entity[baseEntity].baseUrl) config.entity[baseEntity].baseUrl = config.entity[baseEntity].baseUrls[0] // failover logic also updates baseUrl
@@ -983,7 +995,11 @@ const getServiceClient = async (baseEntity) => {
983
995
  strictDN: false // false => allows none standard ldap base dn e.g. <SID=...> / <GUID=...> ref. objectSid/objectGUID
984
996
  })
985
997
  await new Promise((resolve, reject) => {
986
- cli.bind(config.entity[baseEntity].username, config.entity[baseEntity].passwordDecrypted, (err, res) => err ? reject(err) : resolve(res))
998
+ if (ctx?.request?.header?.authorization) { // using ctx authentication PassThrough
999
+ const [username, password] = getCtxAuth(ctx)
1000
+ if (username) cli.bind(username, password, (err, res) => err ? reject(err) : resolve(res)) // basic auth
1001
+ else cli.bind(config.entity[baseEntity].username, password, (err, res) => err ? reject(err) : resolve(res)) // bearer token, using username from configuration
1002
+ } else cli.bind(config.entity[baseEntity].username, config.entity[baseEntity].passwordDecrypted, (err, res) => err ? reject(err) : resolve(res))
987
1003
  cli.on('error', (err) => reject(err))
988
1004
  })
989
1005
  return cli // client OK
@@ -1013,7 +1029,7 @@ const getServiceClient = async (baseEntity) => {
1013
1029
  // "attributes": ["sAMAccountName","displayName","mail"]
1014
1030
  // }
1015
1031
  //
1016
- const doRequest = async (baseEntity, method, base, ldapOptions) => {
1032
+ const doRequest = async (baseEntity, method, base, ldapOptions, ctx) => {
1017
1033
  let result = null
1018
1034
  let client = null
1019
1035
 
@@ -1029,7 +1045,7 @@ const doRequest = async (baseEntity, method, base, ldapOptions) => {
1029
1045
  }
1030
1046
 
1031
1047
  try {
1032
- client = await getServiceClient(baseEntity)
1048
+ client = await getServiceClient(baseEntity, ctx)
1033
1049
  switch (method) {
1034
1050
  case 'search':
1035
1051
  options.paged = { pageSize: 200, pagePause: false } // parse entire directory calling 'page' method for each page
@@ -267,27 +267,27 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
267
267
  const userObj = res[0]
268
268
 
269
269
  for (const key in attrObj) {
270
- if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups)
271
- attrObj[key].forEach(el => {
272
- if (el.operation === 'delete') {
273
- userObj[key] = userObj[key].filter(e => {
274
- if (Object.prototype.hasOwnProperty.call(el, 'value') && el.value !== e.value) return true
275
- if (Object.prototype.hasOwnProperty.call(el, 'type') && el.type !== e.type) return true
276
- if (Object.prototype.hasOwnProperty.call(el, 'display') && el.display !== e.display) return true
277
- if (Object.prototype.hasOwnProperty.call(el, 'primary') && el.primary !== e.primary) return true
278
- return false
279
- })
280
- } else { // add
281
- if (!userObj[key]) userObj[key] = []
282
- if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
283
- if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
284
- // delete any existing primary before adding
285
- const index = userObj[key].findIndex(e => e.primary === el.primary)
286
- if (index >= 0) userObj[key].splice(index, 1)
270
+ if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
271
+ const delArr = attrObj[key].filter(el => el.operation === 'delete')
272
+ const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
273
+ if (!userObj[key]) userObj[key] = []
274
+ // delete
275
+ userObj[key] = userObj[key].filter(el => {
276
+ if (delArr.findIndex(e => e.value === el.value) >= 0) return false
277
+ return true
278
+ })
279
+ // add
280
+ addArr.forEach(el => {
281
+ if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
282
+ if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
283
+ const index = userObj[key].findIndex(e => e.primary === el.primary)
284
+ if (index >= 0) {
285
+ if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
286
+ else userObj[key][index].primary = undefined // remove primary attribute, only one primary
287
287
  }
288
288
  }
289
- userObj[key].push(el)
290
289
  }
290
+ userObj[key].push(el)
291
291
  })
292
292
  } else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
293
293
  if (!attrObj[key]) delete userObj[key] // blank or null
@@ -300,6 +300,15 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
300
300
  if (userObj[key].length < 1) delete userObj[key]
301
301
  } else { // modify/create multivalue
302
302
  if (!userObj[key]) userObj[key] = []
303
+ if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
304
+ const primVal = attrObj[key][el].primary
305
+ if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
306
+ const index = userObj[key].findIndex(e => e.primary === primVal)
307
+ if (index >= 0) {
308
+ userObj[key][index].primary = undefined
309
+ }
310
+ }
311
+ }
303
312
  const found = userObj[key].find((e, i) => {
304
313
  if (e.type === el || (!e.type && el === 'undefined')) {
305
314
  for (const k in attrObj[key][el]) {
@@ -49,20 +49,34 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
49
49
  const validFilterOperators = ['eq', 'ne', 'aeq', 'dteq', 'gt', 'gte', 'lt', 'lte', 'between', 'jgt', 'jgte', 'jlt', 'jlte', 'jbetween', 'regex', 'in', 'nin', 'keyin', 'nkeyin', 'definedin', 'undefinedin', 'contains', 'containsAny', 'type', 'finite', 'size', 'len', 'exists']
50
50
 
51
51
  if (!config.entity) throw new Error('error: configuration entity is missing')
52
- for (const baseEntity in config.entity) {
53
- if (config.entity[baseEntity].baseUrl) { // mongodb://host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]] - e.g: mongodb://localhost:27017/db?tls=true&tlsInsecure=true
54
- const arr = config.entity[baseEntity].baseUrl.split('//')
55
- if (arr.length !== 2 || arr[0] !== 'mongodb:') throw new Error('error: configuration baseUrls is not using expected format mongodb://hostname:port')
56
- const username = config.entity[baseEntity].username
57
- const password = scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)
58
- const dbConn = `${arr[0]}//${encodeURIComponent(username)}:${encodeURIComponent(password)}@${arr[1]}` // percent encoded username/password
59
- const client = new MongoClient(dbConn, { useUnifiedTopology: true })
60
- loadHandler(baseEntity, client)
52
+ if (!scimgateway.authPassThroughAllowed) { // not using Auth PassThrough, loading db handler at startup using username/password from config
53
+ for (const baseEntity in config.entity) {
54
+ loadHandler(baseEntity)
61
55
  }
62
56
  }
63
57
 
64
- async function loadHandler (baseEntity, client) {
58
+ async function loadHandler (baseEntity, ctx) {
65
59
  const action = 'loadHander'
60
+ if (!config.entity[baseEntity].baseUrl) { // mongodb://host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]] - e.g: mongodb://localhost:27017/db?tls=true&tlsInsecure=true
61
+ throw new Error(`${action} error: configuration entity.${baseEntity}.baseUrl is missing`)
62
+ }
63
+ const arr = config.entity[baseEntity].baseUrl.split('//')
64
+ if (arr.length !== 2 || arr[0] !== 'mongodb:') throw new Error('error: configuration baseUrls is not using expected format mongodb://hostname:port')
65
+
66
+ let username
67
+ let password
68
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough
69
+ const [user, secret] = getCtxAuth(ctx)
70
+ if (user) username = user
71
+ else username = config.entity[baseEntity].username // bearer token, using username from configuration
72
+ password = secret
73
+ } else {
74
+ username = config.entity[baseEntity].username
75
+ password = scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)
76
+ }
77
+ const dbConn = `${arr[0]}//${encodeURIComponent(username)}:${encodeURIComponent(password)}@${arr[1]}` // percent encoded username/password
78
+ const client = new MongoClient(dbConn, { useUnifiedTopology: true })
79
+
66
80
  const dbName = config.entity[baseEntity].database ? config.entity[baseEntity].database : 'scim'
67
81
  let db
68
82
  let users
@@ -72,7 +86,9 @@ async function loadHandler (baseEntity, client) {
72
86
  await client.connect()
73
87
  db = await client.db(dbName)
74
88
 
75
- config.entity[baseEntity].client = client
89
+ const clientIdentifier = getClientIdentifier(ctx)
90
+ if (!config.entity[baseEntity][clientIdentifier]) config.entity[baseEntity][clientIdentifier] = {}
91
+ config.entity[baseEntity][clientIdentifier].client = client
76
92
  config.entity[baseEntity].db = db
77
93
 
78
94
  if (await isMongoCollection(baseEntity, 'users')) users = await db.collection('users')
@@ -131,10 +147,11 @@ async function loadHandler (baseEntity, client) {
131
147
  }
132
148
  }
133
149
  }
134
-
135
- config.entity[baseEntity].collection = {}
136
- config.entity[baseEntity].collection.users = users
137
- config.entity[baseEntity].collection.groups = groups
150
+ const clientIdentifier = getClientIdentifier(ctx)
151
+ if (!config.entity[baseEntity][clientIdentifier]) config.entity[baseEntity][clientIdentifier] = {}
152
+ config.entity[baseEntity][clientIdentifier].collection = {}
153
+ config.entity[baseEntity][clientIdentifier].collection.users = users
154
+ config.entity[baseEntity][clientIdentifier].collection.groups = groups
138
155
  }
139
156
 
140
157
  // =================================================
@@ -186,7 +203,11 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
186
203
  }
187
204
  }
188
205
 
189
- const users = config.entity[baseEntity].collection.users
206
+ const clientIdentifier = getClientIdentifier(ctx)
207
+ if (ctx && !config.entity[baseEntity][clientIdentifier]) { // first (or previous failed) PassThrough attempt - have to load connection
208
+ await loadHandler(baseEntity, ctx)
209
+ }
210
+ const users = config.entity[baseEntity][clientIdentifier].collection.users
190
211
  let findObj
191
212
 
192
213
  // mandatory if-else logic - start
@@ -353,27 +374,27 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
353
374
  let userObj = decodeDotDate(res[0])
354
375
 
355
376
  for (const key in attrObj) {
356
- if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups)
357
- attrObj[key].forEach(el => {
358
- if (el.operation === 'delete') {
359
- userObj[key] = userObj[key].filter(e => {
360
- if (Object.prototype.hasOwnProperty.call(el, 'value') && el.value !== e.value) return true
361
- if (Object.prototype.hasOwnProperty.call(el, 'type') && el.type !== e.type) return true
362
- if (Object.prototype.hasOwnProperty.call(el, 'display') && el.display !== e.display) return true
363
- if (Object.prototype.hasOwnProperty.call(el, 'primary') && el.primary !== e.primary) return true
364
- return false
365
- })
366
- } else { // add
367
- if (!userObj[key]) userObj[key] = []
368
- if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
369
- if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
370
- // delete any existing primary before adding
371
- const index = userObj[key].findIndex(e => e.primary === el.primary)
372
- if (index >= 0) userObj[key].splice(index, 1)
377
+ if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
378
+ const delArr = attrObj[key].filter(el => el.operation === 'delete')
379
+ const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
380
+ if (!userObj[key]) userObj[key] = []
381
+ // delete
382
+ userObj[key] = userObj[key].filter(el => {
383
+ if (delArr.findIndex(e => e.value === el.value) >= 0) return false
384
+ return true
385
+ })
386
+ // add
387
+ addArr.forEach(el => {
388
+ if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
389
+ if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
390
+ const index = userObj[key].findIndex(e => e.primary === el.primary)
391
+ if (index >= 0) {
392
+ if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
393
+ else userObj[key][index].primary = undefined // remove primary attribute, only one primary
373
394
  }
374
395
  }
375
- userObj[key].push(el)
376
396
  }
397
+ userObj[key].push(el)
377
398
  })
378
399
  } else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
379
400
  if (!attrObj[key]) delete userObj[key] // blank or null
@@ -386,6 +407,15 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
386
407
  if (userObj[key].length < 1) delete userObj[key]
387
408
  } else { // modify/create multivalue
388
409
  if (!userObj[key]) userObj[key] = []
410
+ if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
411
+ const primVal = attrObj[key][el].primary
412
+ if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
413
+ const index = userObj[key].findIndex(e => e.primary === primVal)
414
+ if (index >= 0) {
415
+ userObj[key][index].primary = undefined
416
+ }
417
+ }
418
+ }
389
419
  const found = userObj[key].find((e, i) => {
390
420
  if (e.type === el || (!e.type && el === 'undefined')) {
391
421
  for (const k in attrObj[key][el]) {
@@ -709,11 +739,30 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
709
739
  // =================================================
710
740
  // helpers
711
741
  // =================================================
742
+
743
+ const getClientIdentifier = (ctx) => {
744
+ if (!ctx?.request?.header?.authorization) return undefined
745
+ const [user, secret] = getCtxAuth(ctx)
746
+ return `${encodeURIComponent(user)}_${encodeURIComponent(secret)}` // user_password or undefined_password
747
+ }
748
+
749
+ //
750
+ // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
751
+ //
752
+ const getCtxAuth = (ctx) => {
753
+ if (!ctx?.request?.header?.authorization) return []
754
+ const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
755
+ let username, password
756
+ if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
757
+ if (username) return [username, password] // basic auth
758
+ else return [undefined, authToken] // bearer auth
759
+ }
760
+
712
761
  const decodeDotDate = (obj) => { // replace dot with unicode
713
762
  const retObj = JSON.parse(JSON.stringify(obj)) // new object - don't modify source
714
763
  Object.keys(retObj).forEach(function (key) {
715
764
  if (key.includes('·')) {
716
- retObj[key.replace(/\·/g, '.')] = retObj[key]
765
+ retObj[key.replace(/\·/g, '.')] = retObj[key] // eslint-disable-line
717
766
  delete retObj[key]
718
767
  }
719
768
  })
@@ -785,12 +834,20 @@ async function dropMongoCollection (baseEntity, collection) {
785
834
  process.on('SIGTERM', () => {
786
835
  // kill
787
836
  for (const baseEntity in config.entity) {
788
- if (config.entity[baseEntity].db) config.entity[baseEntity].db.close()
837
+ for (const key in config.entity[baseEntity]) {
838
+ if (config.entity[baseEntity][key].client) {
839
+ config.entity[baseEntity][key].client.close()
840
+ }
841
+ }
789
842
  }
790
843
  })
791
844
  process.on('SIGINT', () => {
792
845
  // Ctrl+C
793
846
  for (const baseEntity in config.entity) {
794
- if (config.entity[baseEntity].db) config.entity[baseEntity].db.close()
847
+ for (const key in config.entity[baseEntity]) {
848
+ if (config.entity[baseEntity][key].client) {
849
+ config.entity[baseEntity][key].client.close()
850
+ }
851
+ }
795
852
  }
796
853
  })