scimgateway 4.2.5 → 4.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,9 +64,10 @@ Can be used as SCIM version-gateway e.g. 1.1=>2.0 or 2.0=>1.1
64
64
  Can be used to chain several SCIM Gateway's
65
65
 
66
66
 
67
- * **Forwardinc** (SOAP Webservice)
68
- Demonstrates user provisioning towards SOAP-Based endpoint
69
- Using endpoint Forwardinc that comes with Broadcom/CA IM SDK (SDKWS) - [wiki.ca.com](https://docops.ca.com/ca-identity-manager/12-6-8/EN/programming/connector-programming-reference/sdk-sample-connectors/sdkws-sdk-web-services-connector/sdkws-sample-connector-build-requirements "wiki.ca.com")
67
+ * **Soap** (SOAP Webservice)
68
+ Demonstrates user provisioning towards SOAP-Based endpoint
69
+ Excample WSDLs are included
70
+ Using endpoint "Forwardinc" as an example (comes with Symantec/Broadcom/CA IM SDK - SDKWS)
70
71
  Shows how to implement a highly configurable multi tenant or multi endpoint solution through `baseEntity` in URL
71
72
 
72
73
  * **MSSQL** (MSSQL Database)
@@ -188,7 +189,7 @@ When maintaining a set of modifications it useful to disable the postinstall ope
188
189
  const loki = require('./lib/plugin-loki')
189
190
  // const mongodb = require('./lib/plugin-mongodb')
190
191
  // const scim = require('./lib/plugin-scim')
191
- // const forwardinc = require('./lib/plugin-forwardinc')
192
+ // const soap = require('./lib/plugin-soap')
192
193
  // const mssql = require('./lib/plugin-mssql')
193
194
  // const saphana = require('./lib/plugin-saphana') // prereq: npm install hdb
194
195
  // const azureAD = require('./lib/plugin-azure-ad')
@@ -212,7 +213,8 @@ Below shows an example of config\plugin-saphana.json
212
213
  "version": "2.0",
213
214
  "customSchema": null,
214
215
  "skipTypeConvert" : false,
215
- "usePutSoftSync" : false
216
+ "usePutSoftSync" : false,
217
+ "usePutGroupMemberOfUser": false
216
218
  },
217
219
  "log": {
218
220
  "loglevel": {
@@ -342,7 +344,9 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
342
344
  ]
343
345
 
344
346
 
345
- - **scim.usePutSoftSync** - true or false, default false. `PUT /Users/bjensen` will replace the user bjensen with body content. If body contains groups, usePutSoftsync=true will prevent removing any existing groups that are not included in body.groups
347
+ - **scim.usePutSoftSync** - true or false, default false. `PUT /Users/bjensen` will replace the user bjensen with body content. If body contains groups, usePutSoftsync=true will prevent removing any existing groups that are not included in body.groups
348
+
349
+ - **scim."usePutGroupMemberOfUser** - true or false, default false. `PUT /Users/<user>` will replace the user with body content. If body contains groups and usePutGroupMemberOfUser=true, groups will be set on user object (groups are member of user) instead of default user member of groups
346
350
 
347
351
  - **log.loglevel.file** - off, error, info, or debug. Output to plugin-logfile e.g. `logs\plugin-saphana.log`
348
352
 
@@ -462,20 +466,20 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
462
466
  }
463
467
 
464
468
 
465
- secrets.json for plugin-forwardinc - example (dot notation):
469
+ secrets.json for plugin-soap - example (dot notation):
466
470
 
467
471
  {
468
- "plugin-forwardinc.scimgateway.auth.basic[0].username": "gwadmin",
469
- "plugin-forwardinc.scimgateway.auth.basic[0].password": "password",
470
- "plugin-forwardinc.endpoint.username": "superuser",
471
- "plugin-forwardinc.endpoint.password": "secret"
472
+ "plugin-soap.scimgateway.auth.basic[0].username": "gwadmin",
473
+ "plugin-soap.scimgateway.auth.basic[0].password": "password",
474
+ "plugin-soap.endpoint.username": "superuser",
475
+ "plugin-soap.endpoint.password": "secret"
472
476
  }
473
477
 
474
478
  - Custom schema attributes can be added by plugin configuration `scim.customSchema` having value set to filename of a JSON schema-file located in `<package-root>/config/schemas` e.g:
475
479
 
476
480
  "scim": {
477
481
  "version": "2.0",
478
- "customSchema": "plugin-forwardinc-schema.json"
482
+ "customSchema": "plugin-soap-schema.json"
479
483
  },
480
484
 
481
485
  JSON file have following syntax:
@@ -748,7 +752,7 @@ Username, password and port must correspond with plugin configuration file. For
748
752
  http://localhost:8880/client-a
749
753
  http://localhost:8880/client-b
750
754
 
751
- Each baseEntity should then be defined in the plugin configuration file with custom attributes needed. Please see examples in plugin-forwardinc.json
755
+ Each baseEntity should then be defined in the plugin configuration file with custom attributes needed. Please see examples in plugin-soap.json
752
756
 
753
757
  IM 12.6 SP7 (and above) also supports pagination for SCIM endpoint (data transferred in bulks - endpoint explore of users). Loki plugin supports pagination. Other plugin may ignore this setting.
754
758
 
@@ -965,7 +969,7 @@ For JavaScript coding editor you may use [Visual Studio Code](https://code.visua
965
969
 
966
970
  Preparation:
967
971
 
968
- * Copy "best matching" example plugin e.g. `lib\plugin-mssql.js` and `config\plugin-mssql.json` and rename both copies to your plugin name prefix e.g. plugin-mine.js and plugin-mine.json (for SOAP Webservice endpoint we might use plugin-forwardinc as a template)
972
+ * Copy "best matching" example plugin e.g. `lib\plugin-mssql.js` and `config\plugin-mssql.json` and rename both copies to your plugin name prefix e.g. plugin-mine.js and plugin-mine.json (for SOAP Webservice endpoint we might use plugin-soap as a template)
969
973
  * Edit plugin-mine.json and define a unique port number for the gateway setting
970
974
  * Edit index.js and add a new line for starting your plugin e.g. `let mine = require('./lib/plugin-mine');`
971
975
  * Start SCIM Gateway and verify. If using CA Provisioning you could setup a SCIM endpoint using the port number you defined
@@ -988,7 +992,7 @@ Please see plugin-saphana that do not use groups.
988
992
 
989
993
  Template used by CA Provisioning role should only include endpoint supported attributes defined in our plugin. Template should therefore have no links to global user for none supported attributes (e.g. remove %UT% from "Job Title" if our endpoint/code do not support title)
990
994
 
991
- CA Provisioning using default SCIM endpoint do not support SCIM Enterprise User Schema Extension (having attributes like employeeNumber, costCenter, organization, division, department and manager). If we need these or other attributes not found in CA Provisioning, we could define our own by using the free-text "type" definition in the multivalue entitlements or roles attribute. In the template entitlements definition, we could for example define type=Company and set value to %UCOMP%. Please see plugin-forwardinc.js using Company as a multivalue "type" definition.
995
+ CA Provisioning using default SCIM endpoint do not support SCIM Enterprise User Schema Extension (having attributes like employeeNumber, costCenter, organization, division, department and manager). If we need these or other attributes not found in CA Provisioning, we could define our own by using the free-text "type" definition in the multivalue entitlements or roles attribute. In the template entitlements definition, we could for example define type=Company and set value to %UCOMP%. Please see plugin-soap.js using Company as a multivalue "type" definition.
992
996
 
993
997
  Using CA Connector Xpress we could create a new SCIM endpoint type based on the original SCIM. We could then add/remove attributes and change from default assign "user to groups" to assign "groups to user". There are also other predefined endpoints based on the original SCIM. You may take a look at "ServiceNow - WSL7" and "Zendesk - WSL7".
994
998
 
@@ -1165,6 +1169,25 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1165
1169
 
1166
1170
  ## Change log
1167
1171
 
1172
+ ### v4.2.7
1173
+
1174
+ [Added]
1175
+
1176
+ - new plugin configuration **scim.usePutGroupMemberOfUser** can be set to true or false, default false. `PUT /Users/<user>` will replace the user bjensen with body content. If body contains groups and usePutGroupMemberOfUser=true, groups will be set on user object (groups are member of user) instead of default user member of groups
1177
+ - plugin-forwardinc renamed to plugin-soap
1178
+ - Dependencies bump
1179
+
1180
+ [Fixed]
1181
+
1182
+ - plugin-azure-ad fixed some issues introduced in v4.2.4
1183
+ - plugin-mongodb fixed some issues introduced in v4.2.4
1184
+
1185
+ ### v4.2.6
1186
+
1187
+ [Fixed]
1188
+
1189
+ - cosmetics related to 401 error handling introduced in v4.2.4
1190
+
1168
1191
  ### v4.2.5
1169
1192
 
1170
1193
  [Fixed]
@@ -7,7 +7,8 @@
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -7,7 +7,8 @@
7
7
  "version": "1.1",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -7,7 +7,8 @@
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -7,7 +7,8 @@
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -7,7 +7,8 @@
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -7,7 +7,8 @@
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -7,7 +7,8 @@
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -7,7 +7,8 @@
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -7,7 +7,8 @@
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
9
  "skipTypeConvert": false,
10
- "usePutSoftSync": false
10
+ "usePutSoftSync": false,
11
+ "usePutGroupMemberOfUser": false
11
12
  },
12
13
  "log": {
13
14
  "loglevel": {
@@ -156,4 +157,4 @@
156
157
  }
157
158
  }
158
159
  }
159
- }
160
+ }
package/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  const loki = require('./lib/plugin-loki')
13
13
  // const mongodb = require('./lib/plugin-mongodb')
14
14
  // const scim = require('./lib/plugin-scim')
15
- // const forwardinc = require('./lib/plugin-forwardinc')
15
+ // const soap = require('./lib/plugin-soap')
16
16
  // const mssql = require('./lib/plugin-mssql')
17
17
  // const saphana = require('./lib/plugin-saphana') // prereq: npm install hdb --save
18
18
  // const azureAD = require('./lib/plugin-azure-ad')
package/lib/plugin-api.js CHANGED
@@ -434,15 +434,7 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
434
434
  throw newerr
435
435
  }
436
436
  } else {
437
- if (statusCode === 401) {
438
- if (_serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
439
- err.message = JSON.stringify( // don't reveal original message
440
- {
441
- statusCode: 401,
442
- error: 'Access denied'
443
- }
444
- )
445
- }
437
+ if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
446
438
  throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
447
439
  }
448
440
  }
@@ -578,7 +578,9 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
578
578
  } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
579
579
  // mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
580
580
  // Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
581
- path = `/users/${getObj.value}/memberOf/microsoft.graph.group?$select=id,displayName&$expand=members($select=id,displayName)`
581
+ // not using below expand because Azure returns only a maximum of 20 items for the expanded relationship
582
+ // path = `/users/${getObj.value}/memberOf/microsoft.graph.group?$select=id,displayName&$expand=members($select=id,displayName)`
583
+ path = `/users/${getObj.value}/memberOf/microsoft.graph.group?$select=id,displayName`
582
584
  } else {
583
585
  // optional - simpel filtering
584
586
  throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
@@ -633,6 +635,10 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
633
635
  }
634
636
  })
635
637
  delete response.body.value[i].members
638
+ } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') { // Not using expand-members. Only includes current user as member, but should have requested all...
639
+ members = [{
640
+ value: getObj.value
641
+ }]
636
642
  }
637
643
 
638
644
  const [scimObj] = scimgateway.endpointMapper('inbound', response.body.value[i], config.map.group) // endpoint => SCIM/CustomSCIM attribute standard
@@ -1160,15 +1166,7 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
1160
1166
  throw newerr
1161
1167
  }
1162
1168
  } else {
1163
- if (statusCode === 401) {
1164
- if (_serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
1165
- err.message = JSON.stringify( // don't reveal original message
1166
- {
1167
- statusCode: 401,
1168
- error: 'Access denied'
1169
- }
1170
- )
1171
- }
1169
+ if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
1172
1170
  throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
1173
1171
  }
1174
1172
  }
@@ -1182,9 +1180,9 @@ const getAccessToken = async (baseEntity, ctx) => {
1182
1180
  const clientIdentifier = getClientIdentifier(ctx)
1183
1181
  const d = new Date() / 1000 // seconds (unix time)
1184
1182
  if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken &&
1185
- (_serviceClient[baseEntity].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
1183
+ (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
1186
1184
  lock.release()
1187
- return _serviceClient[baseEntity].accessToken
1185
+ return _serviceClient[baseEntity][clientIdentifier].accessToken
1188
1186
  }
1189
1187
 
1190
1188
  const action = 'getAccessToken'
@@ -48,15 +48,16 @@ scimgateway.authPassThroughAllowed = false // true enables auth passThrough (no
48
48
 
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
- if (!config.entity) throw new Error('error: configuration entity is missing')
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)
55
- }
56
- }
57
-
58
51
  async function loadHandler (baseEntity, ctx) {
59
52
  const action = 'loadHander'
53
+
54
+ const clientIdentifier = getClientIdentifier(ctx)
55
+ if (config.entity[baseEntity].isLoaded) { // loadHandler only once
56
+ if (!clientIdentifier) return clientIdentifier // not using Auth PassThrough
57
+ if (config.entity[baseEntity][clientIdentifier]) return clientIdentifier // authenticated
58
+ throw new Error('{"error":"Access denied","statusCode":401}') // string: "statusCode":401 ensure gateway returns 401
59
+ }
60
+
60
61
  if (!config.entity[baseEntity].baseUrl) { // mongodb://host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]] - e.g: mongodb://localhost:27017/db?tls=true&tlsInsecure=true
61
62
  throw new Error(`${action} error: configuration entity.${baseEntity}.baseUrl is missing`)
62
63
  }
@@ -102,6 +103,9 @@ async function loadHandler (baseEntity, ctx) {
102
103
  groups.createIndex({ id: 1 }, { unique: true })
103
104
  }
104
105
  } catch (error) {
106
+ if (clientIdentifier && error.message.includes('Authentication')) {
107
+ throw new Error('{"error":"Access denied","statusCode":401}') // string: "statusCode":401 ensure gateway returns 401
108
+ }
105
109
  throw new Error(`${action} error: failed to connect to database '${client.s.options.dbName}' - ${error.message}`)
106
110
  }
107
111
 
@@ -147,11 +151,12 @@ async function loadHandler (baseEntity, ctx) {
147
151
  }
148
152
  }
149
153
  }
150
- const clientIdentifier = getClientIdentifier(ctx)
151
154
  if (!config.entity[baseEntity][clientIdentifier]) config.entity[baseEntity][clientIdentifier] = {}
152
155
  config.entity[baseEntity][clientIdentifier].collection = {}
153
156
  config.entity[baseEntity][clientIdentifier].collection.users = users
154
157
  config.entity[baseEntity][clientIdentifier].collection.groups = groups
158
+ config.entity[baseEntity].isLoaded = true
159
+ return clientIdentifier
155
160
  }
156
161
 
157
162
  // =================================================
@@ -173,6 +178,8 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
173
178
  const action = 'getUsers'
174
179
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
175
180
 
181
+ const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
182
+
176
183
  if (getObj.operator) { // convert to plugin supported syntax
177
184
  switch (getObj.operator) {
178
185
  case 'co':
@@ -203,10 +210,6 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
203
210
  }
204
211
  }
205
212
 
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
213
  const users = config.entity[baseEntity][clientIdentifier].collection.users
211
214
  let findObj
212
215
 
@@ -275,6 +278,8 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
275
278
  const action = 'createUser'
276
279
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" userObj=${JSON.stringify(userObj)}`)
277
280
 
281
+ const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
282
+
278
283
  const notValid = scimgateway.notValidAttributes(userObj, validScimAttr) // We should check for unsupported endpoint attributes
279
284
  if (notValid) {
280
285
  throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
@@ -308,7 +313,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
308
313
  userObj = encodeDotDate(userObj)
309
314
 
310
315
  try {
311
- const users = config.entity[baseEntity].collection.users
316
+ const users = config.entity[baseEntity][clientIdentifier].collection.users
312
317
  await users.insertOne(userObj)
313
318
  return null
314
319
  } catch (err) {
@@ -327,7 +332,9 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
327
332
  const action = 'deleteUser'
328
333
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
329
334
 
330
- const users = config.entity[baseEntity].collection.users
335
+ const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
336
+
337
+ const users = config.entity[baseEntity][clientIdentifier].collection.users
331
338
  try {
332
339
  /*
333
340
  const now = Date.now()
@@ -354,6 +361,8 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
354
361
  const action = 'modifyUser'
355
362
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
356
363
 
364
+ const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
365
+
357
366
  const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr) // We should check for unsupported endpoint attributes
358
367
  if (notValid) {
359
368
  throw new Error(`${action} error: unsupported scim attributes: ${notValid} (supporting only these attributes: ${validScimAttr.toString()})`)
@@ -363,7 +372,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
363
372
  let res
364
373
 
365
374
  try {
366
- const users = config.entity[baseEntity].collection.users
375
+ const users = config.entity[baseEntity][clientIdentifier].collection.users
367
376
  res = await users.find({ id }, { projection: { _id: 0 } }).toArray()
368
377
  if (res.length === 0) throw new Error('user does not exist')
369
378
  if (res.length > 1) throw new Error('user is not unique, more than one have been found')
@@ -474,7 +483,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
474
483
  userObj = encodeDotDate(userObj)
475
484
 
476
485
  try {
477
- const users = config.entity[baseEntity].collection.users
486
+ const users = config.entity[baseEntity][clientIdentifier].collection.users
478
487
  await users.replaceOne({ id: id }, userObj)
479
488
  return null
480
489
  } catch (err) {
@@ -501,6 +510,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
501
510
  const action = 'getGroups'
502
511
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
503
512
 
513
+ const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
514
+
504
515
  if (getObj.operator) { // convert to plugin supported syntax
505
516
  switch (getObj.operator) {
506
517
  case 'co':
@@ -574,7 +585,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
574
585
 
575
586
  try {
576
587
  const projection = attributes.length > 0 ? getProjectionFromAttributes(attributes) : { _id: 0 }
577
- const groups = config.entity[baseEntity].collection.groups
588
+ const groups = config.entity[baseEntity][clientIdentifier].collection.groups
578
589
  const groupsArr = await groups.find(findObj, { projection: projection }).sort({ _id: 1 }).skip(getObj.startIndex - 1).limit(getObj.count).toArray()
579
590
  const totalResults = await groups.countDocuments(findObj, { projection: projection })
580
591
  const arr = groupsArr.map((obj) => {
@@ -595,6 +606,8 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
595
606
  const action = 'createGroup'
596
607
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
597
608
 
609
+ const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
610
+
598
611
  if (!groupObj.meta) {
599
612
  const now = Date.now()
600
613
  groupObj.meta = {
@@ -608,7 +621,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
608
621
  groupObj = encodeDotDate(groupObj)
609
622
 
610
623
  try {
611
- const groups = config.entity[baseEntity].collection.groups
624
+ const groups = config.entity[baseEntity][clientIdentifier].collection.groups
612
625
  await groups.insertOne(groupObj)
613
626
  return null
614
627
  } catch (err) {
@@ -627,7 +640,9 @@ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
627
640
  const action = 'deleteGroup'
628
641
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
629
642
 
630
- const groups = config.entity[baseEntity].collection.groups
643
+ const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
644
+
645
+ const groups = config.entity[baseEntity][clientIdentifier].collection.groups
631
646
  try {
632
647
  /*
633
648
  const now = Date.now()
@@ -654,8 +669,10 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
654
669
  const action = 'modifyGroup'
655
670
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
656
671
 
657
- const users = config.entity[baseEntity].collection.users
658
- const groups = config.entity[baseEntity].collection.groups
672
+ const clientIdentifier = await loadHandler(baseEntity, ctx) // includes Auth PassThrough logic and loaded only once
673
+
674
+ const users = config.entity[baseEntity][clientIdentifier].collection.users
675
+ const groups = config.entity[baseEntity][clientIdentifier].collection.groups
659
676
  let res
660
677
  let isModified = false
661
678
 
@@ -851,3 +868,11 @@ process.on('SIGINT', () => {
851
868
  }
852
869
  }
853
870
  })
871
+
872
+ // connect MongoDb and load users/groups
873
+ if (!config.entity) throw new Error('error: configuration entity is missing')
874
+ if (!scimgateway.authPassThroughAllowed) { // not using Auth PassThrough, loading db handler at startup using username/password from config
875
+ for (const baseEntity in config.entity) {
876
+ loadHandler(baseEntity)
877
+ }
878
+ }
@@ -1,5 +1,5 @@
1
1
  // =================================================================================
2
- // File: plugin-restful.js
2
+ // File: plugin-scim.js
3
3
  //
4
4
  // Author: Jarle Elshaug
5
5
  //
@@ -773,15 +773,7 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
773
773
  throw newerr
774
774
  }
775
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
- }
776
+ if (statusCode === 401 && _serviceClient[baseEntity]) delete _serviceClient[baseEntity][clientIdentifier]
785
777
  throw err // CA IM retries getUsers failure once (retry 6 times on ECONNREFUSED)
786
778
  }
787
779
  }
@@ -1,5 +1,5 @@
1
1
  // =================================================================================
2
- // File: plugin-forwardinc.js
2
+ // File: plugin-soap.js
3
3
  //
4
4
  // Author: Jarle Elshaug
5
5
  //
@@ -29,7 +29,7 @@ if (!fsExistsSync('../../lib')) fs.mkdirSync('../../lib')
29
29
 
30
30
  if (!fsExistsSync('../../config/plugin-loki.json')) fs.writeFileSync('../../config/plugin-loki.json', fs.readFileSync('./config/plugin-loki.json'))
31
31
  if (!fsExistsSync('../../config/plugin-scim.json')) fs.writeFileSync('../../config/plugin-scim.json', fs.readFileSync('./config/plugin-scim.json'))
32
- if (!fsExistsSync('../../config/plugin-forwardinc.json')) fs.writeFileSync('../../config/plugin-forwardinc.json', fs.readFileSync('./config/plugin-forwardinc.json'))
32
+ if (!fsExistsSync('../../config/plugin-soap.json')) fs.writeFileSync('../../config/plugin-soap.json', fs.readFileSync('./config/plugin-soap.json'))
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'))
@@ -39,7 +39,7 @@ if (!fsExistsSync('../../config/plugin-mongodb.json')) fs.writeFileSync('../../c
39
39
 
40
40
  fs.writeFileSync('../../lib/plugin-loki.js', fs.readFileSync('./lib/plugin-loki.js'))
41
41
  fs.writeFileSync('../../lib/plugin-scim.js', fs.readFileSync('./lib/plugin-scim.js'))
42
- fs.writeFileSync('../../lib/plugin-forwardinc.js', fs.readFileSync('./lib/plugin-forwardinc.js'))
42
+ fs.writeFileSync('../../lib/plugin-soap.js', fs.readFileSync('./lib/plugin-soap.js'))
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'))
@@ -318,11 +318,27 @@ const ScimGateway = function () {
318
318
  if (!userName && authType === 'Bearer') userName = 'token'
319
319
  if (ctx.request.url !== '/favicon.ico') {
320
320
  if (ctx.response.status < 200 || ctx.response.status > 299) {
321
+ let isEndpointAccessDenied = false
322
+ if (res.body.detail) {
323
+ if (res.body.detail.includes('\"statusCode\":401')) isEndpointAccessDenied= true // eslint-disable-line
324
+ } else if (res.body.Errors) {
325
+ if (Array.isArray(res.body.Errors) && res.body.Errors[0].description && res.body.Errors[0].description.includes('\"statusCode\":401')) { // eslint-disable-line
326
+ isEndpointAccessDenied = true
327
+ }
328
+ }
329
+ if (isEndpointAccessDenied) { // don't reveal original SCIM error message details related to access denied (e.g. using Auth PassThrough)
330
+ ctx.response.set('Content-Type', 'application/json; charset=utf-8')
331
+ ctx.response.status = 401 // ctx.response.message becomes default 'Unauthorized'
332
+ ctx.response.body = { error: 'Access denied' }
333
+ res.statusCode = ctx.response.status
334
+ res.statusMessage = ctx.response.message
335
+ res.body = ctx.response.body
336
+ }
321
337
  logger.error(`${gwName}[${pluginName}] ${ellapsed} ${ctx.request.ipcli} ${userName} ${ctx.request.method} ${ctx.request.href} Inbound = ${JSON.stringify(ctx.request.body)} Outbound = ${JSON.stringify(res)}${(config.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
322
338
  } else logger.info(`${gwName}[${pluginName}] ${ellapsed} ${ctx.request.ipcli} ${userName} ${ctx.request.method} ${ctx.request.href} Inbound = ${JSON.stringify(ctx.request.body)} Outbound = ${JSON.stringify(res)}${(config.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
323
339
  requestCounter += 1 // logged on exit (not win process termination)
324
340
  }
325
- if (ctx.response.body && typeof ctx.response.body === 'object') ctx.set('Content-Type', 'application/scim+json; charset=utf-8')
341
+ if (ctx.response.body && typeof ctx.response.body === 'object' && ctx.response.status !== 401) ctx.set('Content-Type', 'application/scim+json; charset=utf-8')
326
342
  }
327
343
 
328
344
  // start auth methods - used by auth
@@ -1274,22 +1290,22 @@ const ScimGateway = function () {
1274
1290
  }
1275
1291
  logger.debug(`${gwName}[${pluginName}] calling "${handle.getMethod}" and awaiting result`)
1276
1292
  const res = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'id', operator: 'eq', value: id }, ctx.query.attributes ? ctx.query.attributes.split(',').map(item => item.trim()) : [], ctx.ctxCopy)
1277
- scimdata = {
1278
- Resources: [],
1279
- totalResults: null
1293
+
1294
+ if (!res || !res.Resources || !Array.isArray(res.Resources)) {
1295
+ throw new Error(`using ${handle.getMethod} to retrive ${id} after ${handle.modifyMethod} - got invalid response: ${res}`)
1280
1296
  }
1281
- if (res) {
1282
- if (res.Resources && Array.isArray(res.Resources)) {
1283
- scimdata.Resources = res.Resources
1284
- scimdata.totalResults = res.totalResults
1285
- } else if (Array.isArray(res)) scimdata.Resources = res
1286
- else if (typeof (res) === 'object' && Object.keys(res).length > 0) scimdata.Resources[0] = res
1297
+ if (res.Resources.length > 1) {
1298
+ throw new Error(`using ${handle.getMethod} to retrive ${id} after ${handle.modifyMethod} - response returned ${res.Resources.length} objects`)
1287
1299
  }
1288
- if (scimdata.Resources.length !== 1) throw new Error(`using ${handle.getMethod} to retrive user ${id} after ${handle.modifyMethod} but response did not include user object`)
1300
+ if (res.Resources.length === 0) {
1301
+ ctx.status = 204
1302
+ return
1303
+ }
1304
+
1289
1305
  const location = ctx.origin + ctx.path
1290
1306
  ctx.set('Location', location)
1291
- scimdata.Resources[0] = addPrimaryAttrs(scimdata.Resources[0])
1292
- scimdata = utils.stripObj(scimdata.Resources[0], ctx.query.attributes, ctx.query.excludedAttributes)
1307
+ res.Resources[0] = addPrimaryAttrs(res.Resources[0])
1308
+ scimdata = utils.stripObj(res.Resources[0], ctx.query.attributes, ctx.query.excludedAttributes)
1293
1309
  scimdata = addSchemas(scimdata, handle.description, isScimv2)
1294
1310
  ctx.status = 200
1295
1311
  ctx.body = scimdata
@@ -1346,6 +1362,16 @@ const ScimGateway = function () {
1346
1362
  }
1347
1363
  }
1348
1364
  }
1365
+ if (config.scim.usePutGroupMemberOfUser) { // group member of user instead of default user member of group
1366
+ if (clearedObj.groups && Array.isArray(clearedObj.groups)) {
1367
+ for (let i = 0; i < clearedObj.groups.length; i++) {
1368
+ if (clearedObj.groups[i].operation && clearedObj.groups[i].operation === 'delete') {
1369
+ clearedObj.groups.splice(i, 1) // delete
1370
+ i -= 1
1371
+ }
1372
+ }
1373
+ }
1374
+ }
1349
1375
  }
1350
1376
 
1351
1377
  // merge cleared object with the new
@@ -1353,9 +1379,9 @@ const ScimGateway = function () {
1353
1379
  delete newObj.id
1354
1380
  delete newObj.userName
1355
1381
  delete newObj.externalId
1356
- delete newObj.groups // do not support "group member of users"
1357
1382
  delete newObj.schemas
1358
1383
  delete newObj.meta
1384
+ if (!config.scim.usePutGroupMemberOfUser) delete newObj.groups
1359
1385
  if (handle.getMethod === handler.groups.getMethod) delete newObj.displayName
1360
1386
 
1361
1387
  let [scimdata, err] = ScimGateway.prototype.convertedScim(newObj)
@@ -1366,125 +1392,135 @@ const ScimGateway = function () {
1366
1392
  await this[handle.modifyMethod](ctx.params.baseEntity, id, scimdata, ctx.ctxCopy)
1367
1393
 
1368
1394
  // add/remove groups
1369
- if (jsonBody.groups && Array.isArray(jsonBody.groups)) { // only if groups included, { "groups": [] } will remove all existing
1370
- if (typeof this[handler.groups.getMethod] !== 'function' || typeof this[handler.groups.modifyMethod] !== 'function') {
1371
- throw new Error('replaceUser error: put operation can not be fully completed for the user`s groups, methods like getGroups() and modifyGroup() are not implemented')
1372
- }
1373
- let currentGroups
1374
- if (currentObj.groups && Array.isArray(currentObj.groups)) currentGroups = currentObj.groups
1375
- else { // try to get current groups the standard way
1376
- let res
1377
- try {
1378
- res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }, ['id', 'displayName'], ctx.ctxCopy)
1379
- } catch (err) {} // method may be implemented but throwing error like groups not supported/implemented
1380
- currentGroups = []
1381
- if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
1382
- for (let i = 0; i < res.Resources.length; i++) {
1383
- if (!res.Resources[i].id) continue
1384
- const el = {}
1385
- el.value = res.Resources[i].id
1386
- if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
1387
- currentGroups.push(el) // { "value": "Admins", "display": "Admins"}
1388
- }
1395
+ if (!config.scim.usePutGroupMemberOfUser) { // default user member of group
1396
+ if (jsonBody.groups && Array.isArray(jsonBody.groups)) { // only if groups included, { "groups": [] } will remove all existing
1397
+ if (typeof this[handler.groups.getMethod] !== 'function' || typeof this[handler.groups.modifyMethod] !== 'function') {
1398
+ throw new Error('replaceUser error: put operation can not be fully completed for the user`s groups, methods like getGroups() and modifyGroup() are not implemented')
1389
1399
  }
1390
- }
1391
- currentGroups = currentGroups.map((el) => {
1392
- if (el.value) {
1393
- el.value = decodeURIComponent(el.value)
1400
+ let currentGroups
1401
+ if (currentObj.groups && Array.isArray(currentObj.groups)) currentGroups = currentObj.groups
1402
+ else { // try to get current groups the standard way
1403
+ let res
1404
+ try {
1405
+ res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }, ['id', 'displayName'], ctx.ctxCopy)
1406
+ } catch (err) {} // method may be implemented but throwing error like groups not supported/implemented
1407
+ currentGroups = []
1408
+ if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
1409
+ for (let i = 0; i < res.Resources.length; i++) {
1410
+ if (!res.Resources[i].id) continue
1411
+ const el = {}
1412
+ el.value = res.Resources[i].id
1413
+ if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
1414
+ currentGroups.push(el) // { "value": "Admins", "display": "Admins"}
1415
+ }
1416
+ }
1394
1417
  }
1395
- return el
1396
- })
1418
+ currentGroups = currentGroups.map((el) => {
1419
+ if (el.value) {
1420
+ el.value = decodeURIComponent(el.value)
1421
+ }
1422
+ return el
1423
+ })
1397
1424
 
1398
- const addGrps = []
1399
- const removeGrps = []
1400
- // add
1401
- for (let i = 0; i < jsonBody.groups.length; i++) {
1402
- if (!jsonBody.groups[i].value) continue
1403
- jsonBody.groups[i].value = decodeURIComponent(jsonBody.groups[i].value)
1404
- let found = false
1405
- for (let j = 0; j < currentGroups.length; j++) {
1406
- if (jsonBody.groups[i].value === currentGroups[j].value) {
1407
- found = true
1408
- break
1425
+ const addGrps = []
1426
+ const removeGrps = []
1427
+ // add
1428
+ for (let i = 0; i < jsonBody.groups.length; i++) {
1429
+ if (!jsonBody.groups[i].value) continue
1430
+ jsonBody.groups[i].value = decodeURIComponent(jsonBody.groups[i].value)
1431
+ let found = false
1432
+ for (let j = 0; j < currentGroups.length; j++) {
1433
+ if (jsonBody.groups[i].value === currentGroups[j].value) {
1434
+ found = true
1435
+ break
1436
+ }
1409
1437
  }
1438
+ if (!found && jsonBody.groups[i].value) addGrps.push(jsonBody.groups[i].value)
1410
1439
  }
1411
- if (!found && jsonBody.groups[i].value) addGrps.push(jsonBody.groups[i].value)
1412
- }
1413
- // remove
1414
- for (let i = 0; i < currentGroups.length; i++) {
1415
- let found = false
1416
- for (let j = 0; j < jsonBody.groups.length; j++) {
1417
- if (!jsonBody.groups[j].value) continue
1418
- jsonBody.groups[j].value = decodeURIComponent(jsonBody.groups[j].value)
1419
- if (currentGroups[i].value === jsonBody.groups[j].value) {
1420
- found = true
1421
- break
1440
+ // remove
1441
+ for (let i = 0; i < currentGroups.length; i++) {
1442
+ let found = false
1443
+ for (let j = 0; j < jsonBody.groups.length; j++) {
1444
+ if (!jsonBody.groups[j].value) continue
1445
+ jsonBody.groups[j].value = decodeURIComponent(jsonBody.groups[j].value)
1446
+ if (currentGroups[i].value === jsonBody.groups[j].value) {
1447
+ found = true
1448
+ break
1449
+ }
1422
1450
  }
1451
+ if (!found && currentGroups[i].value) removeGrps.push(currentGroups[i].value)
1423
1452
  }
1424
- if (!found && currentGroups[i].value) removeGrps.push(currentGroups[i].value)
1425
- }
1426
1453
 
1427
- const addGroups = async (grp) => {
1428
- const obj = { members: [{ value: id }] }
1429
- return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj, ctx.ctxCopy)
1430
- }
1454
+ const addGroups = async (grp) => {
1455
+ const obj = { members: [{ value: id }] }
1456
+ return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj, ctx.ctxCopy)
1457
+ }
1431
1458
 
1432
- const removeGroups = async (grp) => {
1433
- const obj = { members: [{ operation: 'delete', value: id }] }
1434
- return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj, ctx.ctxCopy)
1435
- }
1459
+ const removeGroups = async (grp) => {
1460
+ const obj = { members: [{ operation: 'delete', value: id }] }
1461
+ return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj, ctx.ctxCopy)
1462
+ }
1463
+
1464
+ let errRemove
1465
+ if (!config.scim.usePutSoftSync) { // default will remove any existing groups not included, usePutSoftSync=true prevents removing existing groups (only add groups)
1466
+ await Promise.all(removeGrps.map((grp) => removeGroups(grp)))
1467
+ .then()
1468
+ .catch((err) => {
1469
+ errRemove = err
1470
+ })
1471
+ }
1436
1472
 
1437
- let errRemove
1438
- if (!config.scim.usePutSoftSync) { // default will remove any existing groups not included, usePutSoftSync=true prevents removing existing groups (only add groups)
1439
- await Promise.all(removeGrps.map((grp) => removeGroups(grp)))
1473
+ let errAdd
1474
+ await Promise.all(addGrps.map((grp) => addGroups(grp)))
1440
1475
  .then()
1441
1476
  .catch((err) => {
1442
- errRemove = err
1477
+ errAdd = err
1443
1478
  })
1444
- }
1445
-
1446
- let errAdd
1447
- await Promise.all(addGrps.map((grp) => addGroups(grp)))
1448
- .then()
1449
- .catch((err) => {
1450
- errAdd = err
1451
- })
1452
1479
 
1453
- let errMsg = ''
1454
- if (errRemove) errMsg = `removeGroups error: ${errRemove.message}`
1455
- if (errAdd) errMsg += `${errMsg ? ' ' : ''}addGroups error: ${errAdd.message}`
1456
- if (errMsg) throw new Error(errMsg)
1480
+ let errMsg = ''
1481
+ if (errRemove) errMsg = `removeGroups error: ${errRemove.message}`
1482
+ if (errAdd) errMsg += `${errMsg ? ' ' : ''}addGroups error: ${errAdd.message}`
1483
+ if (errMsg) throw new Error(errMsg)
1484
+ }
1457
1485
  }
1458
1486
 
1459
1487
  // get updated object
1460
1488
  logger.debug(`${gwName}[${pluginName}] calling "${handle.getMethod}" and awaiting result`)
1461
1489
  res = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'id', operator: 'eq', value: id }, [], ctx.ctxCopy)
1462
1490
 
1463
- scimdata = {}
1464
- if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length === 1) scimdata = res.Resources[0]
1465
- else if (Array.isArray(res) && res.length === 1) scimdata = res[0]
1466
- else if (res && typeof (res) === 'object' && Object.keys(res).length > 0) scimdata = res
1467
- else throw Error(`put using method ${handle.getMethod} got unexpected response: ${JSON.stringify(res)}`)
1491
+ if (!res || !res.Resources || !Array.isArray(res.Resources)) {
1492
+ throw new Error(`put using method ${handle.getMethod} - got unexpected response: ${JSON.stringify(res)}`)
1493
+ }
1494
+ if (res.Resources.length > 1) {
1495
+ throw new Error(`put using method ${handle.getMethod} to retrive ${id} - response returned ${res.Resources.length} objects`)
1496
+ }
1497
+ if (res.Resources.length === 0) {
1498
+ ctx.status = 204
1499
+ return
1500
+ }
1501
+ scimdata = res.Resources[0]
1468
1502
 
1469
1503
  // include groups
1470
- if (handle.getMethod === handler.users.getMethod && typeof this[handler.groups.getMethod] === 'function') {
1471
- logger.debug(`${gwName}[${pluginName}] calling "${handler.groups.getMethod}" and awaiting result`)
1472
- const res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: id }, ['id', 'displayName'], ctx.ctxCopy)
1473
- let grps = []
1474
- if (res && res.Resources && Array.isArray(res.Resources)) grps = res.Resources
1475
- else if (Array.isArray(res)) grps = res
1476
- else if (res && typeof (res) === 'object' && Object.keys(res).length > 0) grps = [res]
1477
- else throw Error(`put using method ${handler.groups.getMethod} got unexpected response: ${JSON.stringify(res)}`)
1478
-
1479
- if (grps.length > 0) {
1480
- scimdata.groups = []
1481
- for (let i = 0; i < grps.length; i++) {
1482
- const el = {}
1483
- el.value = grps[i].id
1484
- if (grps[i].displayName) el.display = grps[i].displayName
1485
- if (isScimv2) el.type = 'direct'
1486
- else el.type = { value: 'direct' }
1487
- if (el.value) scimdata.groups.push(el) // { "value": "Admins", "display": "Admins", "type": "direct"}
1504
+ if (!config.scim.usePutGroupMemberOfUser) { // default user member of group
1505
+ if (handle.getMethod === handler.users.getMethod && typeof this[handler.groups.getMethod] === 'function') {
1506
+ logger.debug(`${gwName}[${pluginName}] calling "${handler.groups.getMethod}" and awaiting result`)
1507
+ const res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: id }, ['id', 'displayName'], ctx.ctxCopy)
1508
+ let grps = []
1509
+ if (res && res.Resources && Array.isArray(res.Resources)) grps = res.Resources
1510
+ else if (Array.isArray(res)) grps = res
1511
+ else if (res && typeof (res) === 'object' && Object.keys(res).length > 0) grps = [res]
1512
+ else throw Error(`put using method ${handler.groups.getMethod} - got unexpected response: ${JSON.stringify(res)}`)
1513
+
1514
+ if (grps.length > 0) {
1515
+ scimdata.groups = []
1516
+ for (let i = 0; i < grps.length; i++) {
1517
+ const el = {}
1518
+ el.value = grps[i].id
1519
+ if (grps[i].displayName) el.display = grps[i].displayName
1520
+ if (isScimv2) el.type = 'direct'
1521
+ else el.type = { value: 'direct' }
1522
+ if (el.value) scimdata.groups.push(el) // { "value": "Admins", "display": "Admins", "type": "direct"}
1523
+ }
1488
1524
  }
1489
1525
  }
1490
1526
  }
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "scimgateway",
3
- "version": "4.2.5",
4
- "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
5
- "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
6
- "homepage": "https://elshaug.xyz",
7
- "license": "MIT",
8
- "main": "lib/scimgateway.js",
9
- "scripts": {
10
- "postinstall": "node lib/postinstall.js",
11
- "start": "node index.js",
12
- "test": "mocha -R spec ./test"
13
- },
14
- "bin": {
15
- "scimgateway": "./index.js"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/jelhub/scimgateway.git"
20
- },
21
- "keywords": [
22
- "scim",
23
- "gateway",
24
- "proxy",
25
- "azure",
26
- "identity",
27
- "manager",
28
- "provisioning",
29
- "iga"
30
- ],
31
- "engines": {
32
- "node": ">=14.0.0"
33
- },
34
- "dependencies": {
35
- "@godaddy/terminus": "^4.11.2",
36
- "callsite": "^1.0.0",
37
- "dot-object": "^2.1.4",
38
- "https-proxy-agent": "^5.0.1",
39
- "is-in-subnet": "^4.0.1",
40
- "jsonwebtoken": "^9.0.0",
41
- "koa": "^2.14.0",
42
- "koa-bodyparser": "^4.3.0",
43
- "koa-router": "^12.0.0",
44
- "ldapjs": "^2.3.3",
45
- "lokijs": "^1.5.12",
46
- "mongodb": "^4.12.1",
47
- "node-machine-id": "1.1.9",
48
- "nodemailer": "^6.8.0",
49
- "passport": "^0.6.0",
50
- "passport-azure-ad": "^4.3.4",
51
- "soap": "^0.45.0",
52
- "tedious": "^15.1.2",
53
- "winston": "^3.8.2"
54
- },
55
- "devDependencies": {
56
- "chai": "^4.2.0",
57
- "mocha": "^9.2.0",
58
- "supertest": "^4.0.2"
59
- }
60
- }
1
+ {
2
+ "name": "scimgateway",
3
+ "version": "4.2.7",
4
+ "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
5
+ "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
6
+ "homepage": "https://elshaug.xyz",
7
+ "license": "MIT",
8
+ "main": "lib/scimgateway.js",
9
+ "scripts": {
10
+ "postinstall": "node lib/postinstall.js",
11
+ "start": "node index.js",
12
+ "test": "mocha -R spec ./test"
13
+ },
14
+ "bin": {
15
+ "scimgateway": "./index.js"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/jelhub/scimgateway.git"
20
+ },
21
+ "keywords": [
22
+ "scim",
23
+ "gateway",
24
+ "proxy",
25
+ "azure",
26
+ "identity",
27
+ "manager",
28
+ "provisioning",
29
+ "iga"
30
+ ],
31
+ "engines": {
32
+ "node": ">=14.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@godaddy/terminus": "^4.12.0",
36
+ "callsite": "^1.0.0",
37
+ "dot-object": "^2.1.4",
38
+ "https-proxy-agent": "^5.0.1",
39
+ "is-in-subnet": "^4.0.1",
40
+ "jsonwebtoken": "^9.0.0",
41
+ "koa": "^2.14.2",
42
+ "koa-bodyparser": "^4.4.0",
43
+ "koa-router": "^12.0.0",
44
+ "ldapjs": "^3.0.2",
45
+ "lokijs": "^1.5.12",
46
+ "mongodb": "^5.6.0",
47
+ "node-machine-id": "1.1.9",
48
+ "nodemailer": "^6.9.3",
49
+ "passport": "^0.6.0",
50
+ "passport-azure-ad": "^4.3.5",
51
+ "soap": "^1.0.0",
52
+ "tedious": "^16.1.0",
53
+ "winston": "^3.9.0"
54
+ },
55
+ "devDependencies": {
56
+ "chai": "^4.2.0",
57
+ "mocha": "^9.2.0",
58
+ "supertest": "^6.3.3"
59
+ }
60
+ }