scimgateway 4.2.3 → 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.
@@ -119,7 +119,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
119
119
  }
120
120
 
121
121
  try {
122
- const response = await doRequest(baseEntity, method, path, body)
122
+ const response = await doRequest(baseEntity, method, path, body, ctx)
123
123
  if (response.statusCode < 200 || response.statusCode > 299) {
124
124
  throw new Error(`${response.statusMessage} - ${JSON.stringify(response.body)}`)
125
125
  } else if (!response.body) {
@@ -220,7 +220,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
220
220
  }
221
221
 
222
222
  try {
223
- const response = await doRequest(baseEntity, method, path, body)
223
+ const response = await doRequest(baseEntity, method, path, body, ctx)
224
224
  if (response.statusCode < 200 || response.statusCode > 299) {
225
225
  throw new Error(`${response.statusMessage} - ${JSON.stringify(response.body)}`)
226
226
  }
@@ -330,7 +330,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
330
330
  }
331
331
 
332
332
  try {
333
- const response = await doRequest(baseEntity, method, path, body)
333
+ const response = await doRequest(baseEntity, method, path, body, ctx)
334
334
  if (response.statusCode < 200 || response.statusCode > 299) {
335
335
  throw new Error(`${response.statusMessage} - ${JSON.stringify(response.body)}`)
336
336
  }
@@ -394,7 +394,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
394
394
  }
395
395
 
396
396
  try {
397
- const response = await doRequest(baseEntity, method, path, body)
397
+ const response = await doRequest(baseEntity, method, path, body, ctx)
398
398
  if (response.statusCode < 200 || response.statusCode > 299) {
399
399
  throw new Error(`${response.statusMessage} - ${JSON.stringify(response.body)}`)
400
400
  } else if (!response.body) {
@@ -444,7 +444,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
444
444
  const body = { displayName: groupObj.displayName }
445
445
 
446
446
  try {
447
- const response = await doRequest(baseEntity, method, path, body)
447
+ const response = await doRequest(baseEntity, method, path, body, ctx)
448
448
  if (response.statusCode < 200 || response.statusCode > 299) {
449
449
  throw new Error(`${response.statusMessage} - ${JSON.stringify(response.body)}`)
450
450
  }
@@ -466,7 +466,7 @@ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
466
466
  const body = null
467
467
 
468
468
  try {
469
- const response = await doRequest(baseEntity, method, path, body)
469
+ const response = await doRequest(baseEntity, method, path, body, ctx)
470
470
  if (response.statusCode < 200 || response.statusCode > 299) {
471
471
  throw new Error(`${response.statusMessage} - ${JSON.stringify(response.body)}`)
472
472
  }
@@ -538,7 +538,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
538
538
  const path = `/Groups/${id}`
539
539
 
540
540
  try {
541
- const response = await doRequest(baseEntity, method, path, body)
541
+ const response = await doRequest(baseEntity, method, path, body, ctx)
542
542
  if (response.statusCode < 200 || response.statusCode > 299) {
543
543
  throw new Error(`${response.statusMessage} - ${JSON.stringify(response.body)}`)
544
544
  }
@@ -552,6 +552,24 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
552
552
  // helpers
553
553
  // =================================================
554
554
 
555
+ const getClientIdentifier = (ctx) => {
556
+ if (!ctx?.request?.header?.authorization) return undefined
557
+ const [user, secret] = getCtxAuth(ctx)
558
+ return `${encodeURIComponent(user)}_${encodeURIComponent(secret)}` // user_password or undefined_password
559
+ }
560
+
561
+ //
562
+ // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
563
+ //
564
+ const getCtxAuth = (ctx) => { // eslint-disable-line
565
+ if (!ctx?.request?.header?.authorization) return []
566
+ const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
567
+ let username, password
568
+ if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
569
+ if (username) return [username, password] // basic auth
570
+ else return [undefined, authToken] // bearer auth
571
+ }
572
+
555
573
  //
556
574
  // getServiceClient - returns options needed for connection parameters
557
575
  //
@@ -561,7 +579,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
561
579
  // path = url e.g. "http(s)://<host>:<port>/xxx/yyy", then using the url host/port/protocol
562
580
  // opt (options) may be needed e.g {auth: {username: "username", password: "password"} }
563
581
  //
564
- const getServiceClient = async (baseEntity, method, path, opt) => {
582
+ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
565
583
  const action = 'getServiceClient'
566
584
 
567
585
  let urlObj
@@ -572,7 +590,8 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
572
590
  //
573
591
  // path (no url) - default approach and client will be cached based on config
574
592
  //
575
- if (_serviceClient[baseEntity]) { // serviceClient already exist
593
+ const clientIdentifier = getClientIdentifier(ctx)
594
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist
576
595
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using existing client`)
577
596
  } else {
578
597
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Client have to be created`)
@@ -589,7 +608,8 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
589
608
  json: true, // json-object response instead of string
590
609
  headers: {
591
610
  'Content-Type': 'application/json',
592
- Authorization: 'Basic ' + Buffer.from(`${config.entity[baseEntity].username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)}`).toString('base64')
611
+ // Auth PassThrough or configuration, using ctx "AS-IS" header for PassThrough. For more advanced logic use getCtxAuth(ctx) - see examples in other plugins
612
+ 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')
593
613
  },
594
614
  host: urlObj.hostname,
595
615
  port: urlObj.port, // null if https and 443 defined in url
@@ -609,13 +629,14 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
609
629
  }
610
630
 
611
631
  if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
612
- _serviceClient[baseEntity] = param // serviceClient created
632
+ if (!_serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = {}
633
+ _serviceClient[baseEntity][clientIdentifier] = param // serviceClient created
613
634
  }
614
635
 
615
- const cli = scimgateway.copyObj(_serviceClient[baseEntity]) // client ready
636
+ const cli = scimgateway.copyObj(_serviceClient[baseEntity][clientIdentifier]) // client ready
616
637
 
617
638
  // failover support
618
- path = _serviceClient[baseEntity].baseUrl + path
639
+ path = _serviceClient[baseEntity][clientIdentifier].baseUrl + path
619
640
  urlObj = new URL(path)
620
641
  cli.options.host = urlObj.hostname
621
642
  cli.options.port = urlObj.port
@@ -668,16 +689,16 @@ const getServiceClient = async (baseEntity, method, path, opt) => {
668
689
  return cli // final client
669
690
  }
670
691
 
671
- const updateServiceClient = (baseEntity, obj) => {
672
- if (_serviceClient[baseEntity]) _serviceClient[baseEntity] = scimgateway.extendObj(_serviceClient[baseEntity], obj) // merge with argument options
692
+ const updateServiceClient = (baseEntity, clientIdentifier, obj) => {
693
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = scimgateway.extendObj(_serviceClient[baseEntity][clientIdentifier], obj) // merge with argument options
673
694
  }
674
695
 
675
696
  //
676
697
  // doRequest - execute REST service
677
698
  //
678
- const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
699
+ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) => {
679
700
  try {
680
- const cli = await getServiceClient(baseEntity, method, path, opt)
701
+ const cli = await getServiceClient(baseEntity, method, path, opt, ctx)
681
702
  const options = cli.options
682
703
  const result = await new Promise((resolve, reject) => {
683
704
  let dataString = ''
@@ -732,15 +753,18 @@ const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
732
753
  return result
733
754
  } catch (err) { // includes failover/retry logic based on config baseUrls array
734
755
  scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
756
+ let statusCode
757
+ try { statusCode = JSON.parse(err.message).statusCode } catch (e) {}
758
+ const clientIdentifier = getClientIdentifier(ctx)
735
759
  if (!retryCount) retryCount = 0
736
760
  let urlObj
737
761
  try { urlObj = new URL(path) } catch (err) {}
738
762
  if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND')) {
739
763
  if (retryCount < config.entity[baseEntity].baseUrls.length) {
740
764
  retryCount++
741
- updateServiceClient(baseEntity, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
765
+ updateServiceClient(baseEntity, clientIdentifier, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
742
766
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${(config.entity[baseEntity].baseUrls.length > 1) ? 'failover ' : ''}retry[${retryCount}] using baseUrl = ${_serviceClient[baseEntity].baseUrl}`)
743
- const ret = await doRequest(baseEntity, method, path, body, opt, retryCount) // retry
767
+ const ret = await doRequest(baseEntity, method, path, body, ctx, opt, retryCount) // retry
744
768
  return ret // problem fixed
745
769
  } else {
746
770
  const newerr = new Error(err.message)
@@ -748,7 +772,18 @@ const doRequest = async (baseEntity, method, path, body, opt, retryCount) => {
748
772
  newerr.message = newerr.message.replace('ENOTFOUND', 'UnableConnectingHost') // avoid returning ENOTFOUND error
749
773
  throw newerr
750
774
  }
751
- } else throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
775
+ } else {
776
+ if (statusCode === 401) {
777
+ if (_serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
778
+ err.message = JSON.stringify( // don't reveal original message
779
+ {
780
+ statusCode: 401,
781
+ error: 'Access denied'
782
+ }
783
+ )
784
+ }
785
+ throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
786
+ }
752
787
  }
753
788
  } // doRequest
754
789
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.2.3",
3
+ "version": "4.2.5",
4
4
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
5
5
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
6
6
  "homepage": "https://elshaug.xyz",
@@ -29,7 +29,7 @@
29
29
  "iga"
30
30
  ],
31
31
  "engines": {
32
- "node": ">=7.6.0"
32
+ "node": ">=14.0.0"
33
33
  },
34
34
  "dependencies": {
35
35
  "@godaddy/terminus": "^4.11.2",