scimgateway 4.2.17 → 4.3.0

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.
@@ -48,49 +48,66 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
48
48
 
49
49
  // let endpointPasswordExample = scimgateway.getPassword('endpoint.password', configFile); // example how to encrypt configfile having "endpoint.password"
50
50
 
51
- let users
52
- let groups
53
51
  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']
54
52
 
55
- let dbname = (config.dbname ? config.dbname : 'loki.db')
56
- dbname = path.join(`${configDir}`, `${dbname}`)
57
- const db = new Loki(dbname, {
58
- env: 'NODEJS',
59
- autoload: config.persistence !== false,
60
- autoloadCallback: loadHandler,
61
- autosave: config.persistence !== false,
62
- autosaveInterval: 10000, // 10 seconds
63
- adapter: (config.persistence !== false) ? new Loki.LokiFsAdapter() : new Loki.LokiMemoryAdapter()
64
- })
65
-
66
- function loadHandler () {
67
- users = db.getCollection('users')
68
- if (users === null) { // if database do not exist it will be empty so intitialize here
69
- users = db.addCollection('users', {
70
- unique: ['id', 'userName']
71
- })
53
+ const dbNames = []
54
+ for (const baseEntity in config.entity) {
55
+ let dbname = (config.entity[baseEntity].dbname ? config.entity[baseEntity].dbname : 'loki.db')
56
+ if (dbNames.includes(dbname)) {
57
+ scimgateway.logger.error(`${pluginName}[${baseEntity}] initialization error: database '${dbname}' is already used by another baseEntity configuration`)
58
+ continue
72
59
  }
60
+ dbNames.push(dbname)
61
+ dbname = path.join(`${configDir}`, `${dbname}`)
62
+ const isPersisence = config.entity[baseEntity].persistence !== false
63
+
64
+ const db = new Loki(dbname, {
65
+ env: baseEntity === 'undefined' ? 'N/A' : baseEntity, // avoid default NODEJS
66
+ autoload: isPersisence,
67
+ autoloadCallback: isPersisence ? loadHandler : null,
68
+ autosave: isPersisence,
69
+ autosaveInterval: 10000, // 10 seconds
70
+ adapter: isPersisence ? new Loki.LokiFsAdapter() : new Loki.LokiMemoryAdapter()
71
+ })
72
+ config.entity[baseEntity].db = db
73
+
74
+ function loadHandler () {
75
+ let users = db.getCollection('users')
76
+ if (users === null) { // if database do not exist it will be empty so intitialize here
77
+ users = db.addCollection('users', {
78
+ unique: ['id', 'userName']
79
+ })
80
+ }
73
81
 
74
- groups = db.getCollection('groups')
75
- if (groups === null) {
76
- groups = db.addCollection('groups', {
77
- unique: ['displayName']
78
- })
79
- }
82
+ let groups = db.getCollection('groups')
83
+ if (groups === null) {
84
+ groups = db.addCollection('groups', {
85
+ unique: ['displayName']
86
+ })
87
+ }
80
88
 
81
- if (db.options.autoload === false) { // not using persistence (physical database) => load testusers
82
- scimgateway.testmodeusers.forEach(record => {
83
- if (record.meta) delete record.meta
84
- users.insert(record)
85
- })
86
- scimgateway.testmodegroups.forEach(record => {
87
- if (record.meta) delete record.meta
88
- groups.insert(record)
89
- })
89
+ if (!isPersisence) { // load testusers
90
+ scimgateway.testmodeusers.forEach(record => {
91
+ const r = scimgateway.copyObj(record)
92
+ if (r.meta) delete r.meta
93
+ users.insert(r)
94
+ })
95
+ scimgateway.testmodegroups.forEach(record => {
96
+ const r = scimgateway.copyObj(record)
97
+ if (r.meta) delete r.meta
98
+ groups.insert(r)
99
+ })
100
+ }
101
+
102
+ let baseEntity = db.ENV
103
+ if (baseEntity === 'N/A') baseEntity = undefined
104
+ config.entity[baseEntity].users = users
105
+ config.entity[baseEntity].groups = groups
90
106
  }
91
- }
92
107
 
93
- if (db.options.autoload === false) loadHandler()
108
+ // if (db.options.autoload === false) loadHandler(baseEntity)
109
+ if (!isPersisence) loadHandler()
110
+ }
94
111
 
95
112
  // =================================================
96
113
  // getUsers
@@ -111,6 +128,9 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
111
128
  const action = 'getUsers'
112
129
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
113
130
 
131
+ if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
132
+ const users = config.entity[baseEntity].users
133
+
114
134
  if (getObj.operator) { // convert to plugin supported syntax
115
135
  switch (getObj.operator) {
116
136
  case 'co':
@@ -200,6 +220,8 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
200
220
  const action = 'createUser'
201
221
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" userObj=${JSON.stringify(userObj)}`)
202
222
 
223
+ if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
224
+ const users = config.entity[baseEntity].users
203
225
  const notValid = scimgateway.notValidAttributes(userObj, validScimAttr) // We should check for unsupported endpoint attributes
204
226
  if (notValid) {
205
227
  throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
@@ -241,6 +263,8 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
241
263
  const action = 'deleteUser'
242
264
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
243
265
 
266
+ if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
267
+ const users = config.entity[baseEntity].users
244
268
  const res = users.find({ id: id })
245
269
  if (res.length !== 1) throw new Error(`${action} error: failed for user id=${id}`)
246
270
  const userObj = res[0]
@@ -255,6 +279,8 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
255
279
  const action = 'modifyUser'
256
280
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
257
281
 
282
+ if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
283
+ const users = config.entity[baseEntity].users
258
284
  const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr) // We should check for unsupported endpoint attributes
259
285
  if (notValid) {
260
286
  throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
@@ -374,6 +400,9 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
374
400
  const action = 'getGroups'
375
401
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
376
402
 
403
+ if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
404
+ const groups = config.entity[baseEntity].groups
405
+
377
406
  if (getObj.operator) { // convert to plugin supported syntax
378
407
  switch (getObj.operator) {
379
408
  case 'co':
@@ -463,6 +492,8 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
463
492
  const action = 'createGroup'
464
493
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
465
494
 
495
+ if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
496
+ const groups = config.entity[baseEntity].groups
466
497
  if (groupObj.externalId) groupObj.id = groupObj.externalId // for loki-plugin (scim endpoint) id is mandatory and set to displayName
467
498
  else groupObj.id = groupObj.displayName
468
499
 
@@ -485,6 +516,8 @@ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
485
516
  const action = 'deleteGroup'
486
517
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
487
518
 
519
+ if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
520
+ const groups = config.entity[baseEntity].groups
488
521
  const res = groups.find({ id: id })
489
522
  if (res.length !== 1) throw new Error(`${action} error: failed for id=${id}`)
490
523
  const groupObj = res[0]
@@ -499,6 +532,8 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
499
532
  const action = 'modifyGroup'
500
533
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
501
534
 
535
+ if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
536
+ const groups = config.entity[baseEntity].groups
502
537
  const res = groups.find({ id: id })
503
538
  if (res.length === 0) throw new Error(`${action} error: group id=${id} - group does not exist`)
504
539
  if (res.length > 1) throw new Error(`${action} error: group id=${id} - group is not unique, more than one have been found`)
@@ -569,8 +604,12 @@ const stripLoki = (obj) => { // remove loki meta data and insert scim
569
604
  // Cleanup on exit
570
605
  //
571
606
  process.on('SIGTERM', () => { // kill
572
- db.close()
607
+ for (const baseEntity in config.entity) {
608
+ if (config.entity[baseEntity].db) config.entity[baseEntity].db.close()
609
+ }
573
610
  })
574
611
  process.on('SIGINT', () => { // Ctrl+C
575
- db.close()
612
+ for (const baseEntity in config.entity) {
613
+ if (config.entity[baseEntity].db) config.entity[baseEntity].db.close()
614
+ }
576
615
  })
@@ -65,6 +65,7 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
65
65
  // mandatory plugin initialization - end
66
66
 
67
67
  const _serviceClient = {}
68
+ const lock = new scimgateway.Lock()
68
69
 
69
70
  // =================================================
70
71
  // getUsers
@@ -552,6 +553,10 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
552
553
  // helpers
553
554
  // =================================================
554
555
 
556
+ //
557
+ // start - REST endpoint template
558
+ //
559
+
555
560
  const getClientIdentifier = (ctx) => {
556
561
  if (!ctx?.request?.header?.authorization) return undefined
557
562
  const [user, secret] = getCtxAuth(ctx)
@@ -561,7 +566,7 @@ const getClientIdentifier = (ctx) => {
561
566
  //
562
567
  // getCtxAuth returns username/secret from ctx header when using Auth PassThrough
563
568
  //
564
- const getCtxAuth = (ctx) => { // eslint-disable-line
569
+ const getCtxAuth = (ctx) => {
565
570
  if (!ctx?.request?.header?.authorization) return []
566
571
  const [authType, authToken] = (ctx.request.header.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
567
572
  let username, password
@@ -570,6 +575,85 @@ const getCtxAuth = (ctx) => { // eslint-disable-line
570
575
  else return [undefined, authToken] // bearer auth
571
576
  }
572
577
 
578
+ //
579
+ // getAccessToken - returns oauth accesstoken
580
+ //
581
+ const getAccessToken = async (baseEntity, ctx) => {
582
+ await lock.acquire()
583
+ const clientIdentifier = getClientIdentifier(ctx)
584
+ const d = new Date() / 1000 // seconds (unix time)
585
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken &&
586
+ (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
587
+ lock.release()
588
+ return _serviceClient[baseEntity][clientIdentifier].accessToken
589
+ }
590
+
591
+ const action = 'getAccessToken'
592
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Retrieving accesstoken`)
593
+
594
+ const method = 'POST'
595
+ const [, secret] = getCtxAuth(ctx) // if Auth PassTrough, secret from basic or bearer auth
596
+ let tokenUrl
597
+ let form
598
+
599
+ if (config.entity[baseEntity].oauth) {
600
+ let resource
601
+ try {
602
+ const urlObj = new URL(config.entity[baseEntity].baseUrls[0])
603
+ resource = urlObj.origin
604
+ } catch (err) {
605
+ resource = null
606
+ }
607
+ if (config.entity[baseEntity].oauth.tenantIdGUID) { // Azure
608
+ tokenUrl = `https://login.microsoftonline.com/${config.entity[baseEntity].oauth.tenantIdGUID}/oauth2/token`
609
+ } else {
610
+ tokenUrl = `https://login.microsoftonline.com/${config.entity[baseEntity].oauth.tokenUrl}`
611
+ }
612
+ form = {
613
+ grant_type: 'client_credentials',
614
+ client_id: config.entity[baseEntity].oauth.clientId,
615
+ client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.oauth.clientSecret`, configFile), // using config if no Auth PassThrough
616
+ resource: resource // "https://graph.microsoft.com"
617
+ }
618
+ } else {
619
+ const err = new Error(`[${action}] missing supported endpoint authentication configuration`)
620
+ lock.release()
621
+ throw (err)
622
+ }
623
+
624
+ const options = {
625
+ headers: {
626
+ 'Content-Type': 'application/x-www-form-urlencoded' // body must be query string formatted (no JSON)
627
+ }
628
+ }
629
+
630
+ try {
631
+ const response = await doRequest(baseEntity, method, tokenUrl, form, ctx, options)
632
+ if (!response.body) {
633
+ const err = new Error(`[${action}] No data retrieved from: ${method} ${tokenUrl}`)
634
+ throw (err)
635
+ }
636
+ const jbody = response.body
637
+ if (jbody.error) {
638
+ const err = new Error(`[${action}] Error message: ${jbody.error_description}`)
639
+ throw (err)
640
+ } else if (!jbody.access_token || !jbody.expires_in) {
641
+ const err = new Error(`[${action}] Error message: Retrieved invalid token response`)
642
+ throw (err)
643
+ }
644
+
645
+ const d = new Date() / 1000 // seconds (unix time)
646
+ jbody.validTo = d + parseInt(jbody.expires_in) // instead of using expires_on (clock may not be in sync with NTP, AAD default expires_in = 3600 seconds)
647
+ scimgateway.logger.silly(`${pluginName}[${baseEntity}] ${action}: AccessToken = ${jbody.access_token}`)
648
+
649
+ lock.release()
650
+ return jbody
651
+ } catch (err) {
652
+ lock.release()
653
+ throw (err)
654
+ }
655
+ }
656
+
573
657
  //
574
658
  // getServiceClient - returns options needed for connection parameters
575
659
  //
@@ -582,6 +666,11 @@ const getCtxAuth = (ctx) => { // eslint-disable-line
582
666
  const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
583
667
  const action = 'getServiceClient'
584
668
 
669
+ let authType
670
+ if (config.entity[baseEntity].basicAuth) authType = 'basicAuth'
671
+ else if (config.entity[baseEntity].oauth) authType = 'oauth'
672
+ else if (config.entity[baseEntity].bearerAuth) authType = 'bearerAuth'
673
+
585
674
  let urlObj
586
675
  if (!path) path = ''
587
676
  try {
@@ -590,17 +679,38 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
590
679
  //
591
680
  // path (no url) - default approach and client will be cached based on config
592
681
  //
682
+
593
683
  const clientIdentifier = getClientIdentifier(ctx)
594
- if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist
684
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist - Azure plugin specific
595
685
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using existing client`)
686
+ if (_serviceClient[baseEntity][clientIdentifier].accessToken) {
687
+ // check if token refresh is needed when using oauth
688
+ const d = new Date() / 1000 // seconds (unix time)
689
+ if (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
690
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Accesstoken about to expire in ${_serviceClient[baseEntity][clientIdentifier].accessToken.validTo - d} seconds`)
691
+ try {
692
+ const accessToken = await getAccessToken(baseEntity, ctx)
693
+ _serviceClient[baseEntity][clientIdentifier].accessToken = accessToken
694
+ _serviceClient[baseEntity][clientIdentifier].options.headers.Authorization = ` Bearer ${accessToken.access_token}`
695
+ } catch (err) {
696
+ delete _serviceClient[baseEntity][clientIdentifier]
697
+ const newErr = err
698
+ throw newErr
699
+ }
700
+ }
701
+ }
596
702
  } else {
597
703
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Client have to be created`)
598
704
  let client = null
599
705
  if (config.entity && config.entity[baseEntity]) client = config.entity[baseEntity]
600
706
  if (!client) {
601
- throw new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`)
707
+ const err = new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`)
708
+ throw err
709
+ }
710
+ if (!config.entity[baseEntity].baseUrls || !Array.isArray(config.entity[baseEntity].baseUrls) || config.entity[baseEntity].baseUrls.length < 1) {
711
+ const err = new Error(`missing configuration entity.${baseEntity}.baseUrls`)
712
+ throw err
602
713
  }
603
-
604
714
  urlObj = new URL(config.entity[baseEntity].baseUrls[0])
605
715
  const param = {
606
716
  baseUrl: config.entity[baseEntity].baseUrls[0],
@@ -608,17 +718,46 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
608
718
  json: true, // json-object response instead of string
609
719
  headers: {
610
720
  'Content-Type': 'application/json',
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')
721
+ Accept: 'application/json'
613
722
  },
614
723
  host: urlObj.hostname,
615
724
  port: urlObj.port, // null if https and 443 defined in url
616
- protocol: urlObj.protocol, // http: or https:
617
- rejectUnauthorized: false // accepts self-siged certificates
725
+ protocol: urlObj.protocol // http: or https:
618
726
  // 'method' and 'path' added at the end
619
727
  }
620
728
  }
621
729
 
730
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough using ctx header
731
+ param.options.headers.Authorization = ctx.request.header.authorization
732
+ } else {
733
+ switch (authType) {
734
+ case 'basicAuth':
735
+ if (!config.entity[baseEntity].basicAuth.username || !config.entity[baseEntity].basicAuth.password) {
736
+ const err = new Error(`missing configuration entity.${baseEntity}.basicAuth.username/password`)
737
+ throw err
738
+ }
739
+ param.options.headers.Authorization = 'Basic ' + Buffer.from(`${config.entity[baseEntity].basicAuth.username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.basicAuth.password`, configFile)}`).toString('base64')
740
+ break
741
+ case 'oauth':
742
+ if (!config.entity[baseEntity].oauth.clientId || !config.entity[baseEntity].oauth.clientSecret) {
743
+ const err = new Error(`missing configuration entity.${baseEntity}.oauth.clientId/clientSecret`)
744
+ throw err
745
+ }
746
+ param.accessToken = await getAccessToken(baseEntity, ctx)
747
+ param.options.headers.Authorization = `Bearer ${param.accessToken.access_token}`
748
+ break
749
+ case 'bearerAuth':
750
+ if (!config.entity[baseEntity].bearerAuth.token) {
751
+ const err = new Error(`missing configuration entity.${baseEntity}.bearerAuth.token`)
752
+ throw err
753
+ }
754
+ param.options.headers.Authorization = 'Bearer ' + Buffer.from(`${scimgateway.getPassword(`endpoint.entity.${baseEntity}.bearerAuth.token`, configFile)}`).toString('base64')
755
+ break
756
+ default:
757
+ // no auth
758
+ }
759
+ }
760
+
622
761
  // proxy
623
762
  if (config.entity[baseEntity].proxy && config.entity[baseEntity].proxy.host) {
624
763
  const agent = new HttpsProxyAgent(config.entity[baseEntity].proxy.host)
@@ -628,9 +767,17 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
628
767
  }
629
768
  }
630
769
 
770
+ // config options
771
+ if (config.entity[baseEntity].options) param.options = scimgateway.extendObj(param.options, config.entity[baseEntity].options)
772
+
631
773
  if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
632
774
  if (!_serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = {}
633
775
  _serviceClient[baseEntity][clientIdentifier] = param // serviceClient created
776
+
777
+ // OData support - note, not using [clientIdentifier]
778
+ _serviceClient[baseEntity].nextLink = {}
779
+ _serviceClient[baseEntity].nextLink.users = null // Azure users pagination
780
+ _serviceClient[baseEntity].nextLink.groups = null // Azure groups pagination
634
781
  }
635
782
 
636
783
  const cli = scimgateway.copyObj(_serviceClient[baseEntity][clientIdentifier]) // client ready
@@ -700,6 +847,7 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
700
847
  try {
701
848
  const cli = await getServiceClient(baseEntity, method, path, opt, ctx)
702
849
  const options = cli.options
850
+
703
851
  const result = await new Promise((resolve, reject) => {
704
852
  let dataString = ''
705
853
  if (body) {
@@ -749,17 +897,19 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
749
897
  req.end()
750
898
  }) // Promise
751
899
 
752
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${options.protocol}//${options.host}${(options.port ? `:${options.port}` : '')}${path} Body = ${JSON.stringify(body)} Response = ${JSON.stringify(result)}`)
900
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${options.protocol}//${options.host}${(options.port ? `:${options.port}` : '')}${options.path} Body = ${JSON.stringify(body)} Response = ${JSON.stringify(result)}`)
753
901
  return result
754
902
  } catch (err) { // includes failover/retry logic based on config baseUrls array
755
- scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
756
903
  let statusCode
757
904
  try { statusCode = JSON.parse(err.message).statusCode } catch (e) {}
905
+ if (statusCode === 404) { // not logged as error, let caller decide e.g. getUser-manager
906
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
907
+ } else scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
758
908
  const clientIdentifier = getClientIdentifier(ctx)
759
909
  if (!retryCount) retryCount = 0
760
910
  let urlObj
761
911
  try { urlObj = new URL(path) } catch (err) {}
762
- if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND')) {
912
+ if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')) {
763
913
  if (retryCount < config.entity[baseEntity].baseUrls.length) {
764
914
  retryCount++
765
915
  updateServiceClient(baseEntity, clientIdentifier, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] })
@@ -774,11 +924,15 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
774
924
  }
775
925
  } else {
776
926
  if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
777
- throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
927
+ throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
778
928
  }
779
929
  }
780
930
  } // doRequest
781
931
 
932
+ //
933
+ // end - REST endpoint template
934
+ //
935
+
782
936
  //
783
937
  // Cleanup on exit
784
938
  //
@@ -33,7 +33,7 @@ if (!fsExistsSync('../../config/plugin-soap.json')) fs.writeFileSync('../../conf
33
33
  if (!fsExistsSync('../../config/plugin-mssql.json')) fs.writeFileSync('../../config/plugin-mssql.json', fs.readFileSync('./config/plugin-mssql.json'))
34
34
  if (!fsExistsSync('../../config/plugin-saphana.json')) fs.writeFileSync('../../config/plugin-saphana.json', fs.readFileSync('./config/plugin-saphana.json'))
35
35
  if (!fsExistsSync('../../config/plugin-api.json')) fs.writeFileSync('../../config/plugin-api.json', fs.readFileSync('./config/plugin-api.json'))
36
- if (!fsExistsSync('../../config/plugin-azure-ad.json')) fs.writeFileSync('../../config/plugin-azure-ad.json', fs.readFileSync('./config/plugin-azure-ad.json')) // keep existing
36
+ if (!fsExistsSync('../../config/plugin-entra-id.json')) fs.writeFileSync('../../config/plugin-entra-id.json', fs.readFileSync('./config/plugin-entra-id.json')) // keep existing
37
37
  if (!fsExistsSync('../../config/plugin-ldap.json')) fs.writeFileSync('../../config/plugin-ldap.json', fs.readFileSync('./config/plugin-ldap.json'))
38
38
  if (!fsExistsSync('../../config/plugin-mongodb.json')) fs.writeFileSync('../../config/plugin-mongodb.json', fs.readFileSync('./config/plugin-mongodb.json'))
39
39
 
@@ -43,7 +43,7 @@ fs.writeFileSync('../../lib/plugin-soap.js', fs.readFileSync('./lib/plugin-soap.
43
43
  fs.writeFileSync('../../lib/plugin-mssql.js', fs.readFileSync('./lib/plugin-mssql.js'))
44
44
  fs.writeFileSync('../../lib/plugin-saphana.js', fs.readFileSync('./lib/plugin-saphana.js'))
45
45
  fs.writeFileSync('../../lib/plugin-api.js', fs.readFileSync('./lib/plugin-api.js'))
46
- fs.writeFileSync('../../lib/plugin-azure-ad.js', fs.readFileSync('./lib/plugin-azure-ad.js'))
46
+ fs.writeFileSync('../../lib/plugin-entra-id.js', fs.readFileSync('./lib/plugin-entra-id.js'))
47
47
  fs.writeFileSync('../../lib/plugin-ldap.js', fs.readFileSync('./lib/plugin-ldap.js'))
48
48
  fs.writeFileSync('../../lib/plugin-mongodb.js', fs.readFileSync('./lib/plugin-mongodb.js'))
49
49