scimgateway 4.1.2 → 4.1.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.
package/README.md CHANGED
@@ -16,8 +16,9 @@ Validated through IdP's:
16
16
 
17
17
  Latest news:
18
18
 
19
+ - **BREAKING**: [SCIM Stream](https://elshaug.xyz/docs/scim-stream) is the modern way of user provisioning letting clients subscribe to messages instead of traditional IGA top-down provisioning. SCIM Stream includes **SCIM Stream Gateway**, the next generation SCIM Gateway that supports message subscription and automated provisioning
19
20
  - Supporting OAuth Client Credentials authentication
20
- - Major version v4.0.0. getUsers() and getGroups() replacing some deprecated methods. No limitations on filtering/sorting. Admin user access can be limited to specific baseEntities. New MongoDB plugin
21
+ - Major version v4.0.0. getUsers() and getGroups() replacing some deprecated methods. No limitations on filtering/sorting. Admin user access can be linked to specific baseEntities. New MongoDB plugin
21
22
  - ipAllowList for restricting access to allowlisted IP addresses or subnets e.g. Azure AD IP-range
22
23
  - General LDAP plugin configured for Active Directory
23
24
  - [PlugSSO](https://elshaug.xyz/docs/plugsso) using SCIM Gateway
@@ -39,7 +40,7 @@ Using Identity Manager, we could setup one or more endpoints of type SCIM pointi
39
40
 
40
41
  ![](https://jelhub.github.io/images/ScimGateway.svg)
41
42
 
42
- SCIM Gateway is based on the popular asynchronous event driven framework [Node.js](https://nodejs.dev/) using JavaScript. It is firewall friendly using REST webservices. Runs on almost all operating systems, and may load balance between hosts (horizontal) and cpu's (vertical). Could even be uploaded and run as a cloud application.
43
+ SCIM Gateway is based on the popular asynchronous event driven framework [Node.js](https://nodejs.dev/) using JavaScript. It is cloud and firewall friendly using REST webservices. Runs on almost all operating systems, and may load balance between hosts (horizontal) and cpu's (vertical).
43
44
 
44
45
  **Following example plugins are included:**
45
46
 
@@ -52,18 +53,18 @@ Setting `{"persistence": true}` gives persistence file store (no test users)
52
53
  Example of a fully functional SCIM Gateway plugin
53
54
 
54
55
  * **MongoDB** (NoSQL Document-Oriented Database)
55
- Same as plugin "Loki" but using MongoDB
56
+ Same as plugin "Loki" but using external MongoDB
56
57
  Shows how to implement a highly configurable multi tenant or multi endpoint solution through `baseEntity` in URL
57
58
 
58
59
  * **SCIM** (REST Webservice)
59
- Demonstrates user provisioning towards a SCIM endpoint using REST
60
+ Demonstrates user provisioning towards REST-Based endpoint (type SCIM)
60
61
  Using plugin "Loki" as SCIM endpoint
61
62
  Can be used as SCIM version-gateway e.g. 1.1=>2.0 or 2.0=>1.1
62
63
  Can be used to chain several SCIM Gateway's
63
64
 
64
65
 
65
66
  * **Forwardinc** (SOAP Webservice)
66
- Demonstrates provisioning towards SOAP-Based endpoint
67
+ Demonstrates user provisioning towards SOAP-Based endpoint
67
68
  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")
68
69
  Shows how to implement a highly configurable multi tenant or multi endpoint solution through `baseEntity` in URL
69
70
 
@@ -208,7 +209,8 @@ Below shows an example of config\plugin-saphana.json
208
209
  "scim": {
209
210
  "version": "2.0",
210
211
  "customSchema": null,
211
- "skipTypeConvert" : false
212
+ "skipTypeConvert" : false,
213
+ "usePutSoftSync" : false
212
214
  },
213
215
  "log": {
214
216
  "loglevel": {
@@ -326,6 +328,8 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
326
328
  ]
327
329
 
328
330
 
331
+ - **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
332
+
329
333
  - **log.loglevel.file** - off, error, info, or debug. Output to plugin-logfile e.g. `logs\plugin-saphana.log`
330
334
 
331
335
  - **log.loglevel.console** - off, error, info, or debug. Output to stdout and errors to stderr.
@@ -347,7 +351,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
347
351
 
348
352
  - **auth.bearerOAuth** - Array of one or more Client Credentials OAuth configuration objects. **`client_id`** and **`client_secret`** are mandatory. client_secret value will become encrypted when gateway is started. OAuth token request url is **/oauth/token** e.g. http://localhost:8880/oauth/token
349
353
 
350
- - **certificate** - If not using SSL/TLS certificate, set "key", "cert" and "ca" to **null**. When using SSL/TLS, "key" and "cert" have to be defined with the filename corresponding to the primary-key and public-certificate. Both files must be located in the `<package-root>\config\certs` directory e.g:
354
+ - **certificate** - If not using TLS certificate, set "key", "cert" and "ca" to **null**. When using TLS, "key" and "cert" have to be defined with the filename corresponding to the primary-key and public-certificate. Both files must be located in the `<package-root>\config\certs` directory e.g:
351
355
 
352
356
  "certificate": {
353
357
  "key": "key.pem",
@@ -1139,6 +1143,44 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1139
1143
 
1140
1144
  ## Change log
1141
1145
 
1146
+ ### v4.1.5
1147
+ [Added]
1148
+
1149
+ Announcing some SCIM Gateway related news:
1150
+
1151
+ - [SCIM Stream](https://elshaug.xyz/docs/scim-stream) is the modern way of user provisioning letting clients subscribe to messages instead of traditional IGA top-down provisioning. SCIM Stream includes **SCIM Stream Gateway**, the next generation SCIM Gateway that supports message subscription and automated provisioning
1152
+
1153
+
1154
+ ### v4.1.4
1155
+ [Fixed]
1156
+
1157
+ - TypeConvert logic for multivalue attribute `addresses` did not correctly catch duplicate entries
1158
+ - PUT (Replace User) configuration `scim.usePutSoftsync=true` will also prevent removing any existing roles that are not included in body.roles ref. v4.1.3
1159
+
1160
+ ### v4.1.3
1161
+ [Fixed]
1162
+
1163
+ - createUser response did not include the id that was returned by plugin
1164
+
1165
+ [Added]
1166
+
1167
+ - PUT (Replace User) now includes group handling. Using configuration `scim.usePutSoftsync=true` will prevent removing any existing groups that are not included in body.groups
1168
+
1169
+ Example:
1170
+
1171
+ PUT /Users/bjensen
1172
+ {
1173
+ ...
1174
+ "groups": [
1175
+ {"value":"Employees","display":"Employees"},
1176
+ {"value":"Admins","display":"Admins"}
1177
+ ],
1178
+ ...
1179
+ }
1180
+
1181
+
1182
+
1183
+
1142
1184
  ### v4.1.2
1143
1185
  [Added]
1144
1186
 
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "2.0",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "1.1",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "2.0",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "2.0",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "2.0",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "2.0",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "2.0",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "2.0",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -5,7 +5,8 @@
5
5
  "scim": {
6
6
  "version": "2.0",
7
7
  "customSchema": null,
8
- "skipTypeConvert": false
8
+ "skipTypeConvert": false,
9
+ "usePutSoftSync": false
9
10
  },
10
11
  "log": {
11
12
  "loglevel": {
@@ -272,7 +272,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
272
272
  } else { // add
273
273
  if (!userObj[key]) userObj[key] = []
274
274
  let exists
275
- if (el.value) exists = userObj[key].find(e => e.value && e.value === el.value)
275
+ if (el.value) exists = userObj[key].find(e => e.value && e.value === el.value && e.type === el.type) // allowing same value on different type (type not mandatory)
276
276
  if (!exists) userObj[key].push(el)
277
277
  }
278
278
  })
@@ -355,7 +355,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
355
355
  } else { // add
356
356
  if (!userObj[key]) userObj[key] = []
357
357
  let exists
358
- if (el.value) exists = userObj[key].find(e => e.value && e.value === el.value)
358
+ if (el.value) exists = userObj[key].find(e => e.value && e.value === el.value && e.type === el.type) // allowing same value on different type (type not mandatory)
359
359
  if (!exists) userObj[key].push(el)
360
360
  }
361
361
  })
@@ -276,8 +276,8 @@ const ScimGateway = function () {
276
276
  }
277
277
  }
278
278
 
279
- this.testmodeusers = scimDef.TestmodeUsers.Resources // exported and used by plugin-loki
280
- this.testmodegroups = scimDef.TestmodeGroups.Resources // exported and used by plugin-loki
279
+ this.testmodeusers = scimDef.TestmodeUsers.Resources // exposed and used by plugin-loki
280
+ this.testmodegroups = scimDef.TestmodeGroups.Resources // exposed and used by plugin-loki
281
281
 
282
282
  // multiValueTypes array contains attributes that will be used by "type converted objects" logic
283
283
  // groups, roles, and members are excluded
@@ -1083,33 +1083,36 @@ const ScimGateway = function () {
1083
1083
  return
1084
1084
  }
1085
1085
  logger.debug(`${gwName}[${pluginName}] calling "${handle.createMethod}" and awaiting result`)
1086
+ delete jsonBody.id // in case included in request
1086
1087
  try {
1087
1088
  const res = await this[handle.createMethod](ctx.params.baseEntity, scimdata)
1088
- for (const key in res) { // merge any result e.g: data = {'id': 'xxxx'}
1089
+ for (const key in res) { // merge any result e.g: {'id': 'xxxx'}
1089
1090
  jsonBody[key] = res[key]
1090
1091
  }
1091
1092
 
1092
- let obj
1093
- try { // retrieve id
1094
- if (handle.createMethod === 'createUser') {
1095
- if (jsonBody.userName) {
1096
- jsonBody.id = jsonBody.userName
1097
- obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'userName', operator: 'eq', value: jsonBody.userName }, ['id', 'userName'])
1098
- } else if (jsonBody.externalId) {
1099
- jsonBody.id = jsonBody.externalId
1100
- obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, ['id', 'externalId'])
1101
- }
1102
- } else if (handle.createMethod === 'createGroup') {
1103
- if (jsonBody.externalId) {
1104
- jsonBody.id = jsonBody.externalId
1105
- obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, ['id', 'externalId'])
1106
- } else if (jsonBody.displayName) {
1107
- jsonBody.id = jsonBody.displayName
1108
- obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'displayName', operator: 'eq', value: jsonBody.displayName }, ['id', 'displayName'])
1093
+ if (!jsonBody.id) { // retrieve id
1094
+ let obj
1095
+ try {
1096
+ if (handle.createMethod === 'createUser') {
1097
+ if (jsonBody.userName) {
1098
+ jsonBody.id = jsonBody.userName
1099
+ obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'userName', operator: 'eq', value: jsonBody.userName }, ['id', 'userName'])
1100
+ } else if (jsonBody.externalId) {
1101
+ jsonBody.id = jsonBody.externalId
1102
+ obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, ['id', 'externalId'])
1103
+ }
1104
+ } else if (handle.createMethod === 'createGroup') {
1105
+ if (jsonBody.externalId) {
1106
+ jsonBody.id = jsonBody.externalId
1107
+ obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, ['id', 'externalId'])
1108
+ } else if (jsonBody.displayName) {
1109
+ jsonBody.id = jsonBody.displayName
1110
+ obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'displayName', operator: 'eq', value: jsonBody.displayName }, ['id', 'displayName'])
1111
+ }
1109
1112
  }
1110
- }
1111
- } catch (err) { }
1112
- if (obj && obj.id) jsonBody.id = obj.id
1113
+ } catch (err) { }
1114
+ if (obj && obj.id) jsonBody.id = obj.id
1115
+ }
1113
1116
 
1114
1117
  const location = `${ctx.origin}${ctx.path}/${jsonBody.id}`
1115
1118
  if (!jsonBody.meta) jsonBody.meta = {}
@@ -1206,7 +1209,7 @@ const ScimGateway = function () {
1206
1209
  } else {
1207
1210
  logger.debug(`${gwName}[${pluginName}] [Modify ${handle.description}] id=${id}`)
1208
1211
  let scimdata, err
1209
- if (jsonBody.Operations) [scimdata, err] = convertedScim20(jsonBody) // v2.0
1212
+ if (jsonBody.Operations) [scimdata, err] = ScimGateway.prototype.convertedScim20(jsonBody) // v2.0
1210
1213
  else [scimdata, err] = ScimGateway.prototype.convertedScim(jsonBody) // v1.1
1211
1214
  logger.debug(`${gwName}[${pluginName}] convertedBody=${JSON.stringify(scimdata)}`)
1212
1215
  if (err) {
@@ -1268,7 +1271,6 @@ const ScimGateway = function () {
1268
1271
  err = jsonErr(config.scim.version, pluginName, ctx.status, err)
1269
1272
  ctx.body = err
1270
1273
  } else {
1271
- logger.debug(`${gwName}[${pluginName}] [Modify ${handle.description}] id=${id}`)
1272
1274
  logger.debug(`${gwName}[${pluginName}] PUT ${ctx.originalUrl} body=${strBody}`)
1273
1275
  try {
1274
1276
  // get current object
@@ -1286,6 +1288,18 @@ const ScimGateway = function () {
1286
1288
  delete clearedObj.password
1287
1289
  delete clearedObj.meta
1288
1290
 
1291
+ // usePutSoftSync=true prevents removing existing roles (only add roles)
1292
+ if (config.scim.usePutSoftSync) {
1293
+ if (clearedObj.roles && Array.isArray(clearedObj.roles)) {
1294
+ for (let i = 0; i < clearedObj.roles.length; i++) {
1295
+ if (clearedObj.roles[i].operation && clearedObj.roles[i].operation === 'delete') {
1296
+ clearedObj.roles.splice(i, 1) // delete
1297
+ i -= 1
1298
+ }
1299
+ }
1300
+ }
1301
+ }
1302
+
1289
1303
  // merge cleared object with the new
1290
1304
  const newObj = utils.extendObj(clearedObj, jsonBody)
1291
1305
  delete newObj.id
@@ -1302,6 +1316,86 @@ const ScimGateway = function () {
1302
1316
  logger.debug(`${gwName}[${pluginName}] calling "${handle.modifyMethod}" and awaiting result`)
1303
1317
  await this[handle.modifyMethod](ctx.params.baseEntity, id, scimdata)
1304
1318
 
1319
+ // add/remove groups
1320
+ if (jsonBody.groups && Array.isArray(jsonBody.groups)) { // only if groups included, { "groups": [] } will remove all existing
1321
+ if (typeof this[handler.groups.getMethod] !== 'function' || typeof this[handler.groups.modifyMethod] !== 'function') {
1322
+ 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')
1323
+ }
1324
+ let currentGroups
1325
+ if (currentObj.groups && Array.isArray(currentObj.groups)) currentGroups = currentObj.groups
1326
+ else { // try to get current groups the standard way
1327
+ let res
1328
+ try {
1329
+ res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }, ['members.value', 'id', 'displayName']) // await scimgateway.getUserGroups(baseEntity, userObj.id, 'members.value,displayName')
1330
+ } catch (err) {} // method may be implemented but throwing error like groups not supported/implemented
1331
+ currentGroups = []
1332
+ if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
1333
+ for (let i = 0; i < res.Resources.length; i++) {
1334
+ if (!res.Resources[i].id) continue
1335
+ const el = {}
1336
+ el.value = res.Resources[i].id
1337
+ if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
1338
+ if (el.value) currentGroups.push(el) // { "value": "Admins", "display": "Admins"}
1339
+ }
1340
+ }
1341
+ }
1342
+ const addGrps = []
1343
+ const removeGrps = []
1344
+ // add
1345
+ for (let i = 0; i < jsonBody.groups.length; i++) {
1346
+ let found = false
1347
+ for (let j = 0; j < currentGroups.length; j++) {
1348
+ if (jsonBody.groups[i].value === currentGroups[j].value) {
1349
+ found = true
1350
+ break
1351
+ }
1352
+ }
1353
+ if (!found && jsonBody.groups[i].value) addGrps.push(jsonBody.groups[i].value)
1354
+ }
1355
+ // remove
1356
+ for (let i = 0; i < currentGroups.length; i++) {
1357
+ let found = false
1358
+ for (let j = 0; j < jsonBody.groups.length; j++) {
1359
+ if (currentGroups[i].value === jsonBody.groups[j].value) {
1360
+ found = true
1361
+ break
1362
+ }
1363
+ }
1364
+ if (!found && currentGroups[i].value) removeGrps.push(currentGroups[i].value)
1365
+ }
1366
+
1367
+ const addGroups = async (grp) => {
1368
+ const obj = { members: [{ value: id }] }
1369
+ return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj)
1370
+ }
1371
+
1372
+ const removeGroups = async (grp) => {
1373
+ const obj = { members: [{ operation: 'delete', value: id }] }
1374
+ return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj)
1375
+ }
1376
+
1377
+ let errRemove
1378
+ if (!config.scim.usePutSoftSync) { // default will remove any existing groups not included, usePutSoftSync=true prevents removing existing groups (only add groups)
1379
+ await Promise.all(removeGrps.map((grp) => removeGroups(grp)))
1380
+ .then()
1381
+ .catch((err) => {
1382
+ errRemove = err
1383
+ })
1384
+ }
1385
+
1386
+ let errAdd
1387
+ await Promise.all(addGrps.map((grp) => addGroups(grp)))
1388
+ .then()
1389
+ .catch((err) => {
1390
+ errAdd = err
1391
+ })
1392
+
1393
+ let errMsg = ''
1394
+ if (errRemove) errMsg = `removeGroups error: ${errRemove.message}`
1395
+ if (errAdd) errMsg += `${errMsg ? ' ' : ''}addGroups error: ${errAdd.message}`
1396
+ if (errMsg) throw new Error(errMsg)
1397
+ }
1398
+
1305
1399
  // get updated object
1306
1400
  logger.debug(`${gwName}[${pluginName}] calling "${handle.getMethod}" and awaiting result`)
1307
1401
  res = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'id', operator: 'eq', value: id }, [])
@@ -1313,7 +1407,7 @@ const ScimGateway = function () {
1313
1407
  else throw Error(`put using method ${handle.getMethod} got unexpected response: ${JSON.stringify(res)}`)
1314
1408
 
1315
1409
  // include groups
1316
- if (handle.getMethod === handler.users.getMethod) {
1410
+ if (handle.getMethod === handler.users.getMethod && typeof this[handler.groups.getMethod] === 'function') {
1317
1411
  logger.debug(`${gwName}[${pluginName}] calling "${handler.groups.getMethod}" and awaiting result`)
1318
1412
  const res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: id }, ['members.value', 'id', 'displayName'])
1319
1413
  let grps = []
@@ -1346,7 +1440,7 @@ const ScimGateway = function () {
1346
1440
  ctx.body = e
1347
1441
  }
1348
1442
  }
1349
- }) // put
1443
+ })
1350
1444
 
1351
1445
  // ==========================================
1352
1446
  // API POST (no SCIM)
@@ -1719,13 +1813,27 @@ const ScimGateway = function () {
1719
1813
  }
1720
1814
  })
1721
1815
  } else if (multiValueTypes.includes(key)) { // "type converted object" // groups, roles, member and scim.excludeTypeConvert are not included
1816
+ const tmpAddr = []
1722
1817
  scimdata[key].forEach(function (element, index) {
1723
1818
  if (!element.type) element.type = 'undefined' // "none-type"
1724
1819
  if (element.operation && element.operation === 'delete') { // add as delete if same type not included as none delete
1725
1820
  const arr = scimdata[key].filter(obj => obj.type && obj.type === element.type && !obj.operation)
1726
1821
  if (arr.length < 1) {
1727
1822
  if (!newMulti[key]) newMulti[key] = {}
1728
- if (newMulti[key][element.type]) err = new Error(`'type converted object' ${key} - includes more than one element having same type, or type is blank on more than one element - note, setting configuration scim.skipTypeConvert=true will disable this logic/check`)
1823
+ if (newMulti[key][element.type]) {
1824
+ if (['addresses'].includes(key)) { // not checking type, but the others have to be unique
1825
+ for (const i in element) {
1826
+ if (i !== 'type') {
1827
+ if (tmpAddr.includes(i)) {
1828
+ err = new Error(`'type converted object' ${key} - includes more than one element having same ${i}, or ${i} is blank on more than one element - note, setting configuration scim.skipTypeConvert=true will disable this logic/check`)
1829
+ }
1830
+ tmpAddr.push(i)
1831
+ }
1832
+ }
1833
+ } else {
1834
+ err = new Error(`'type converted object' ${key} - includes more than one element having same type, or type is blank on more than one element - note, setting configuration scim.skipTypeConvert=true will disable this logic/check`)
1835
+ }
1836
+ }
1729
1837
  newMulti[key][element.type] = {}
1730
1838
  for (const i in element) {
1731
1839
  newMulti[key][element.type][i] = element[i]
@@ -1734,7 +1842,20 @@ const ScimGateway = function () {
1734
1842
  }
1735
1843
  } else {
1736
1844
  if (!newMulti[key]) newMulti[key] = {}
1737
- if (newMulti[key][element.type]) err = new Error(`'type converted object' ${key} - includes more than one element having same type, or type is blank on more than one element - note, setting configuration scim.skipTypeConvert=true will disable this logic/check`)
1845
+ if (newMulti[key][element.type]) {
1846
+ if (['addresses'].includes(key)) { // not checking type, but the others have to be unique
1847
+ for (const i in element) {
1848
+ if (i !== 'type') {
1849
+ if (tmpAddr.includes(i)) {
1850
+ err = new Error(`'type converted object' ${key} - includes more than one element having same ${i}, or ${i} is blank on more than one element - note, setting configuration scim.skipTypeConvert=true will disable this logic/check`)
1851
+ }
1852
+ tmpAddr.push(i)
1853
+ }
1854
+ }
1855
+ } else {
1856
+ err = new Error(`'type converted object' ${key} - includes more than one element having same type, or type is blank on more than one element - note, setting configuration scim.skipTypeConvert=true will disable this logic/check`)
1857
+ }
1858
+ }
1738
1859
  newMulti[key][element.type] = {}
1739
1860
  for (const i in element) {
1740
1861
  newMulti[key][element.type][i] = element[i]
@@ -1782,7 +1903,7 @@ const ScimGateway = function () {
1782
1903
  // exported methods
1783
1904
  //
1784
1905
  ScimGateway.prototype.endpointMap = endpointMap
1785
- ScimGateway.prototype.countries = endpointMap
1906
+ ScimGateway.prototype.countries = countries
1786
1907
 
1787
1908
  ScimGateway.prototype.getPassword = (pwEntity, configFile) => {
1788
1909
  return utils.getPassword(pwEntity, configFile) // utils.getPassword('scimgateway.password', './config/plugin-testmode.json');
@@ -2406,7 +2527,7 @@ const notValidAttributes = (obj, validScimAttr) => {
2406
2527
  // "type converted object" and blank deleted values
2407
2528
  // {"name":{"givenName":"Rocky",formatted:""},"emails":{"work":{"value":"user@company.com","type":"work"}}}
2408
2529
  //
2409
- const convertedScim20 = (obj) => {
2530
+ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
2410
2531
  let scimdata = {}
2411
2532
  if (!obj.Operations || !Array.isArray(obj.Operations)) return scimdata
2412
2533
  const o = utils.copyObj(obj)
@@ -2593,7 +2714,7 @@ const convertedScim20 = (obj) => {
2593
2714
  // clearObjectValues returns a new object having values set to blank
2594
2715
  // array values are kept, but includes {"operation" : "delete"} - scim 1.1 formatted
2595
2716
  // boolean values e.g. {"active" : true} are kept "as is"
2596
- // parent only used for internal recursive logic
2717
+ // parent used for internal recursive logic
2597
2718
  const clearObjectValues = (o, parent) => {
2598
2719
  if (!o) return {}
2599
2720
  let v, key
@@ -2601,18 +2722,14 @@ const clearObjectValues = (o, parent) => {
2601
2722
  for (key in o) {
2602
2723
  v = o[key]
2603
2724
  if (typeof v === 'object' && v !== null) {
2604
- const objProp = Object.getPrototypeOf(v) // e.g. HttpsProxyAgent {}
2725
+ const objProp = Object.getPrototypeOf(v)
2605
2726
  if (objProp !== null && objProp !== Object.getPrototypeOf({}) && objProp !== Object.getPrototypeOf([])) {
2606
- output[key] = Object.assign(Object.create(v), v) // e.g. { HttpsProxyAgent {...} }
2727
+ output[key] = Object.assign(Object.create(v), v)
2607
2728
  } else {
2608
2729
  output[key] = clearObjectValues(v, key)
2609
2730
  }
2610
- } else if (key === 'type') {
2611
- output[key] = v
2612
- output.operation = 'delete'
2613
- if (output.value) output.value = ''
2614
2731
  } else {
2615
- if (parent && !isNaN(parent) && key === 'value') { // array
2732
+ if (parent && !isNaN(parent)) { // array
2616
2733
  output.operation = 'delete'
2617
2734
  output[key] = v
2618
2735
  } else {
@@ -2629,7 +2746,7 @@ const clearObjectValues = (o, parent) => {
2629
2746
  //
2630
2747
  const jsonErr = (scimVersion, pluginName, htmlErrCode, err, scimType) => {
2631
2748
  let errJson = {}
2632
- let msg = `ScimGateway[${pluginName}] `
2749
+ let msg = `scimgateway[${pluginName}] `
2633
2750
  err.constructor === Error ? msg += err.message : msg += err
2634
2751
 
2635
2752
  if (scimVersion !== '2.0' && scimVersion !== 2) { // v1.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.1.2",
3
+ "version": "4.1.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",
@@ -13,7 +13,6 @@ const options = {
13
13
  }
14
14
 
15
15
  describe('plugin-loki tests', () => {
16
-
17
16
  it('getUsers all test (1)', function (done) {
18
17
  server_8880.get('/Users' +
19
18
  '?startIndex=1&count=100')
@@ -362,7 +361,7 @@ describe('plugin-loki tests', () => {
362
361
  */
363
362
 
364
363
  it('modifyUser test', (done) => {
365
- var user = {
364
+ let user = {
366
365
  Operations: [
367
366
  {
368
367
  op: 'replace',
@@ -490,7 +489,11 @@ describe('plugin-loki tests', () => {
490
489
  }],
491
490
  'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': {
492
491
  employeeNumber: '1111'
493
- }
492
+ },
493
+ groups: [
494
+ { value: 'Employees', display: 'Employees' },
495
+ { value: 'Admins', display: 'Admins' }
496
+ ]
494
497
  }
495
498
 
496
499
  server_8880.put('/Users/jgilber')
@@ -517,6 +520,9 @@ describe('plugin-loki tests', () => {
517
520
  expect(user['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'].manager).to.equal(undefined) // deleted
518
521
  expect(user['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'].test1).to.equal(undefined) // deleted
519
522
  expect(user['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'].test2).to.equal(undefined) // deleted
523
+ expect(user.groups.length).to.equal(2)
524
+ expect(user.groups[0].value).to.equal('Admins')
525
+ expect(user.groups[1].value).to.equal('Employees')
520
526
  done()
521
527
  })
522
528
  })