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.
@@ -66,8 +66,10 @@ config = scimgateway.processExtConfig(pluginName, config) // add any external co
66
66
  scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no scimgateway authentication). scimgateway instead includes ctx (ctx.request.header) in plugin methods. Note, requires plugin-logic for handling/passing ctx.request.header.authorization to be used in endpoint communication
67
67
  // mandatory plugin initialization - end
68
68
 
69
- const sqlPassword = scimgateway.getPassword('endpoint.connection.authentication.options.password', configFile)
70
- config.connection.authentication.options.password = sqlPassword // Connection using config.connection
69
+ if (config?.connection?.authentication?.options?.password) {
70
+ const sqlPassword = scimgateway.getPassword('endpoint.connection.authentication.options.password', configFile)
71
+ config.connection.authentication.options.password = sqlPassword
72
+ }
71
73
 
72
74
  // =================================================
73
75
  // getUsers
@@ -119,7 +121,17 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
119
121
  Resources: [],
120
122
  totalResults: null
121
123
  }
122
- const connection = new Connection(config.connection)
124
+
125
+ const connectionCfg = scimgateway.copyObj(config.connection)
126
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
127
+ if (!connectionCfg.authentication) connectionCfg.authentication = {}
128
+ if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
129
+ if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
130
+ const [username, password] = getCtxAuth(ctx)
131
+ connectionCfg.authentication.options.password = password
132
+ if (username) connectionCfg.authentication.options.userName = username
133
+ }
134
+ const connection = new Connection(connectionCfg)
123
135
 
124
136
  connection.on('connect', function (err) {
125
137
  if (err) {
@@ -190,7 +202,16 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
190
202
  Email: (userObj.emails.work.value) ? `'${userObj.emails.work.value}'` : null
191
203
  }
192
204
 
193
- const connection = new Connection(config.connection)
205
+ const connectionCfg = scimgateway.copyObj(config.connection)
206
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
207
+ if (!connectionCfg.authentication) connectionCfg.authentication = {}
208
+ if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
209
+ if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
210
+ const [username, password] = getCtxAuth(ctx)
211
+ connectionCfg.authentication.options.password = password
212
+ if (username) connectionCfg.authentication.options.userName = username
213
+ }
214
+ const connection = new Connection(connectionCfg)
194
215
 
195
216
  connection.on('connect', function (err) {
196
217
  if (err) {
@@ -227,7 +248,16 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
227
248
 
228
249
  try {
229
250
  return await new Promise((resolve, reject) => {
230
- const connection = new Connection(config.connection)
251
+ const connectionCfg = scimgateway.copyObj(config.connection)
252
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
253
+ if (!connectionCfg.authentication) connectionCfg.authentication = {}
254
+ if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
255
+ if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
256
+ const [username, password] = getCtxAuth(ctx)
257
+ connectionCfg.authentication.options.password = password
258
+ if (username) connectionCfg.authentication.options.userName = username
259
+ }
260
+ const connection = new Connection(connectionCfg)
231
261
 
232
262
  connection.on('connect', function (err) {
233
263
  if (err) {
@@ -301,7 +331,17 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
301
331
  }
302
332
 
303
333
  sql = sql.substr(0, sql.length - 1) // remove trailing ","
304
- const connection = new Connection(config.connection)
334
+
335
+ const connectionCfg = scimgateway.copyObj(config.connection)
336
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
337
+ if (!connectionCfg.authentication) connectionCfg.authentication = {}
338
+ if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
339
+ if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
340
+ const [username, password] = getCtxAuth(ctx)
341
+ connectionCfg.authentication.options.password = password
342
+ if (username) connectionCfg.authentication.options.userName = username
343
+ }
344
+ const connection = new Connection(connectionCfg)
305
345
 
306
346
  connection.on('connect', function (err) {
307
347
  if (err) {
@@ -397,6 +437,18 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
397
437
  // helpers
398
438
  // =================================================
399
439
 
440
+ //
441
+ // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
442
+ //
443
+ const getCtxAuth = (ctx) => { // eslint-disable-line
444
+ if (!ctx?.request?.header?.authorization) return []
445
+ const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
446
+ let username, password
447
+ if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
448
+ if (username) return [username, password] // basic auth
449
+ else return [undefined, authToken] // bearer auth
450
+ }
451
+
400
452
  //
401
453
  // Cleanup on exit
402
454
  //
@@ -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
 
@@ -2604,7 +2604,7 @@ const addSchemas = (data, type, isScimv2, location) => {
2604
2604
  return data
2605
2605
  }
2606
2606
 
2607
- // addPrimaryAttrs cheks for primary attributes and add them as standalone attributes
2607
+ // addPrimaryAttrs cheks for primary attributes (only for roles) and add them as standalone attributes
2608
2608
  // some IdP's may check for these e.g. Azure
2609
2609
  // e.g. {roles: [{value: "val1", primary: "True"}]}
2610
2610
  // gives:
@@ -2613,33 +2613,19 @@ const addSchemas = (data, type, isScimv2, location) => {
2613
2613
  // roles[primary eq "True"].primary: "True"}]
2614
2614
  // }
2615
2615
  const addPrimaryAttrs = (obj) => {
2616
+ const key = 'roles'
2616
2617
  if (!obj || typeof obj !== 'object') return obj
2617
- let arrObj
2618
- if (!Array.isArray(obj)) arrObj = [obj]
2619
- else {
2620
- if (obj.length < 1) return obj
2621
- arrObj = obj
2622
- }
2623
- let arrRet = []
2624
- arrRet = arrObj.map(obj => {
2625
- const o = utils.copyObj(obj)
2626
- for (const key in o) {
2627
- if (Array.isArray(o[key]) && key !== 'groups' && key !== 'members') {
2628
- // check for primary attribute
2629
- const index = o[key].findIndex(el => (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')))
2630
- if (index >= 0) {
2631
- const prim = o[key][index]
2632
- for (const k in prim) {
2633
- const primKey = `${key}[primary eq ${typeof prim.primary === 'string' ? `"${prim.primary}"` : prim.primary}].${k}` // roles[primary eq true].value / roles[primary eq "True"].value``
2634
- o[primKey] = prim[k] // { roles[primary eq true].value : "some-value" }
2635
- }
2636
- }
2637
- }
2618
+ if (!obj[key] || !Array.isArray(obj[key])) return obj
2619
+ const o = utils.copyObj(obj)
2620
+ const index = o[key].findIndex(el => (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')))
2621
+ if (index >= 0) {
2622
+ const prim = o[key][index]
2623
+ for (const k in prim) {
2624
+ const primKey = `${key}[primary eq ${typeof prim.primary === 'string' ? `"${prim.primary}"` : prim.primary}].${k}` // roles[primary eq true].value / roles[primary eq "True"].value``
2625
+ o[primKey] = prim[k] // { roles[primary eq true].value : "some-value" }
2638
2626
  }
2639
- return o
2640
- })
2641
- if (!Array.isArray(obj)) return arrRet[0]
2642
- return arrRet
2627
+ }
2628
+ return o
2643
2629
  }
2644
2630
 
2645
2631
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.2.2",
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",