scimgateway 4.1.0 → 4.1.3

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
@@ -31,7 +31,7 @@ Latest news:
31
31
 
32
32
  ## Overview
33
33
 
34
- With SCIM Gateway we could do user management by using REST based [SCIM](http://www.simplecloud.info/) 1.1 or 2.0 protocol. Gateway will translate incoming SCIM requests and expose CRUD functionality (create, read, update and delete user/group) towards destinations using endpoint specific protocols. In other words, none SCIM-endpoints will become SCIM-endpoints. Gateway do not require SCIM to be used, it's also an API Gateway that could be used for other things than user provisioning.
34
+ With SCIM Gateway we can manage users and groups by using REST based [SCIM](http://www.simplecloud.info/) 1.1 or 2.0 protocol. Gateway translates incoming SCIM requests and expose CRUD functionality (create, read, update and delete user/group) towards destinations using endpoint specific protocols. In other words, none SCIM-endpoints will become SCIM-endpoints. Gateway do not require SCIM to be used, it's also an API Gateway that could be used for other things than user provisioning.
35
35
 
36
36
  SCIM Gateway is a standalone product, however this document shows how the gateway could be used by products like Symatec/Broadcom/CA Identity Manager.
37
37
 
@@ -208,7 +208,8 @@ Below shows an example of config\plugin-saphana.json
208
208
  "scim": {
209
209
  "version": "2.0",
210
210
  "customSchema": null,
211
- "skipTypeConvert" : false
211
+ "skipTypeConvert" : false,
212
+ "usePutSoftSync" : false
212
213
  },
213
214
  "log": {
214
215
  "loglevel": {
@@ -326,6 +327,8 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
326
327
  ]
327
328
 
328
329
 
330
+ - **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
331
+
329
332
  - **log.loglevel.file** - off, error, info, or debug. Output to plugin-logfile e.g. `logs\plugin-saphana.log`
330
333
 
331
334
  - **log.loglevel.console** - off, error, info, or debug. Output to stdout and errors to stderr.
@@ -347,7 +350,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
347
350
 
348
351
  - **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
352
 
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:
353
+ - **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
354
 
352
355
  "certificate": {
353
356
  "key": "key.pem",
@@ -1139,28 +1142,84 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1139
1142
 
1140
1143
  ## Change log
1141
1144
 
1145
+ ### v4.1.3
1146
+ [Fixed]
1147
+
1148
+ - createUser response did not include the id that was returned by plugin
1149
+
1150
+ [Added]
1151
+
1152
+ - 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
1153
+
1154
+ Example:
1155
+
1156
+ PUT /Users/bjensen
1157
+ {
1158
+ ...
1159
+ "groups": [
1160
+ {"value":"Employees","display":"Employees"},
1161
+ {"value":"Admins","display":"Admins"}
1162
+ ],
1163
+ ...
1164
+ }
1165
+
1166
+
1167
+
1168
+
1169
+ ### v4.1.2
1170
+ [Added]
1171
+
1172
+ - endpointMapper supporting one to many mappings using a comma separated list of attributes in the `mapTo`
1173
+
1174
+ Configuration example:
1175
+
1176
+ "map": {
1177
+ "user": {
1178
+ "PersonnelNumber": {
1179
+ "mapTo": "id,userName",
1180
+ "type": "string"
1181
+ },
1182
+ ...
1183
+ }
1184
+ }
1185
+
1186
+
1187
+ ### v4.1.1
1188
+ [Added]
1189
+
1190
+ - plugin-ldap support userFilter/groupFilter configuration for restricting scope
1191
+
1192
+ Configuration example:
1193
+
1194
+ {
1195
+ ...
1196
+ "userFilter": "(memberOf=CN=grp1,OU=Groups,DC=test,DC=com)(!(memberOf=CN=Domain Admins,CN=Users,DC=test,DC=com))",
1197
+ "groupFilter": "(!(cn=grp2))",
1198
+ ...
1199
+ }
1200
+
1142
1201
  ### v4.1.0
1143
1202
  [Added]
1144
1203
 
1145
1204
  - Supporting OAuth Client Credentials authentication
1146
1205
 
1147
- Configuration example:
1206
+ Configuration example:
1148
1207
 
1149
- "bearerOAuth": [
1150
- {
1151
- "client_id": "my_client_id",
1152
- "client_secret": "my_client_secret",
1153
- "readOnly": false,
1154
- "baseEntities": []
1155
- }
1156
- ]
1208
+ "bearerOAuth": [
1209
+ {
1210
+ "client_id": "my_client_id",
1211
+ "client_secret": "my_client_secret",
1212
+ "readOnly": false,
1213
+ "baseEntities": []
1214
+ }
1215
+ ]
1157
1216
 
1158
1217
 
1159
- In example above, client using SCIM Gateway must have OAuth configuration:
1218
+ In example above, client using SCIM Gateway must have OAuth configuration:
1160
1219
 
1161
- client_id = my_client_id
1162
- client_secret = my_client_secret
1163
- token request url = http(s)://<host>:<port>/oauth/token
1220
+ client_id = my_client_id
1221
+ client_secret = my_client_secret
1222
+ token request url = http(s)://<host>:<port>/oauth/token
1164
1223
 
1165
1224
 
1166
1225
  ### v4.0.1
@@ -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": {
@@ -94,6 +95,8 @@
94
95
  "ldap": {
95
96
  "userBase": "CN=Users,DC=test,DC=com",
96
97
  "groupBase": "OU=Groups,DC=test,DC=com",
98
+ "userFilter": null,
99
+ "groupFilter": null,
97
100
  "userNamingAttr": "CN",
98
101
  "groupNamingAttr": "CN",
99
102
  "userObjectClasses": [
@@ -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": {
@@ -23,6 +23,13 @@
23
23
  // "type": "string"
24
24
  // }
25
25
  //
26
+ // Additional user/group filtering for restricting scope may be configured in endpoint.entity.xxx.ldap e.g:
27
+ // {
28
+ // ...
29
+ // "userFilter": "(memberOf=CN=grp1,OU=Groups,DC=test,DC=com)(!(memberOf=CN=Domain Admins,CN=Users,DC=test,DC=com))",
30
+ // "groupFilter": "(!(cn=grp2))",
31
+ // ...
32
+ // }
26
33
  //
27
34
  // Attributes according to map definition in the configuration file plugin-ldap.json:
28
35
  //
@@ -190,6 +197,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes) => {
190
197
  scope: scope,
191
198
  attributes: attrs
192
199
  }
200
+ if (config.entity[baseEntity].ldap.userFilter) ldapOptions.filter += config.entity[baseEntity].ldap.userFilter
193
201
  }
194
202
  }
195
203
  } else if (getObj.operator === 'eq' && getObj.attribute === 'group.value') {
@@ -209,6 +217,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes) => {
209
217
  scope: scope,
210
218
  attributes: attrs
211
219
  }
220
+ if (config.entity[baseEntity].ldap.userFilter) ldapOptions.filter += config.entity[baseEntity].ldap.userFilter
212
221
  }
213
222
  // end mandatory if-else logic
214
223
 
@@ -558,6 +567,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
558
567
  scope: scope,
559
568
  attributes: attrs
560
569
  }
570
+ if (config.entity[baseEntity].ldap.groupFilter) ldapOptions.filter += config.entity[baseEntity].ldap.groupFilter
561
571
  }
562
572
  }
563
573
  } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
@@ -578,6 +588,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
578
588
  scope: scope,
579
589
  attributes: attrs
580
590
  }
591
+ if (config.entity[baseEntity].ldap.groupFilter) ldapOptions.filter += config.entity[baseEntity].ldap.groupFilter
581
592
  }
582
593
  // mandatory if-else logic - end
583
594
 
@@ -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 = {}
@@ -1256,6 +1259,10 @@ const ScimGateway = function () {
1256
1259
  // ==========================================
1257
1260
  router.put([`/(|scim/)(!${undefined}|Users|Groups|servicePlans)/:id`,
1258
1261
  `/:baseEntity/(|scim/)(!${undefined}|Users|Groups|servicePlans)/:id`], async (ctx) => {
1262
+ await replaceUsrGrp(ctx)
1263
+ })
1264
+
1265
+ const replaceUsrGrp = async (ctx, isExtCaller) => {
1259
1266
  let u = ctx.originalUrl.substr(0, ctx.originalUrl.lastIndexOf('/'))
1260
1267
  u = u.substr(u.lastIndexOf('/') + 1) // u = Users, Groups
1261
1268
  const handle = handler[u]
@@ -1268,7 +1275,7 @@ const ScimGateway = function () {
1268
1275
  err = jsonErr(config.scim.version, pluginName, ctx.status, err)
1269
1276
  ctx.body = err
1270
1277
  } else {
1271
- logger.debug(`${gwName}[${pluginName}] [Modify ${handle.description}] id=${id}`)
1278
+ if (!isExtCaller) logger.debug(`${gwName}[${pluginName}] [Replace ${handle.description}] id=${id}`)
1272
1279
  logger.debug(`${gwName}[${pluginName}] PUT ${ctx.originalUrl} body=${strBody}`)
1273
1280
  try {
1274
1281
  // get current object
@@ -1302,6 +1309,86 @@ const ScimGateway = function () {
1302
1309
  logger.debug(`${gwName}[${pluginName}] calling "${handle.modifyMethod}" and awaiting result`)
1303
1310
  await this[handle.modifyMethod](ctx.params.baseEntity, id, scimdata)
1304
1311
 
1312
+ // add/remove groups
1313
+ if (jsonBody.groups && Array.isArray(jsonBody.groups)) { // only if groups included, { "groups": [] } will remove all existing
1314
+ if (typeof this[handler.groups.getMethod] !== 'function' || typeof this[handler.groups.modifyMethod] !== 'function') {
1315
+ 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')
1316
+ }
1317
+ let currentGroups
1318
+ if (currentObj.groups && Array.isArray(currentObj.groups)) currentGroups = currentObj.groups
1319
+ else { // try to get current groups the standard way
1320
+ let res
1321
+ try {
1322
+ 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')
1323
+ } catch (err) {} // method may be implemented but throwing error like groups not supported/implemented
1324
+ currentGroups = []
1325
+ if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
1326
+ for (let i = 0; i < res.Resources.length; i++) {
1327
+ if (!res.Resources[i].id) continue
1328
+ const el = {}
1329
+ el.value = res.Resources[i].id
1330
+ if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
1331
+ if (el.value) currentGroups.push(el) // { "value": "Admins", "display": "Admins"}
1332
+ }
1333
+ }
1334
+ }
1335
+ const addGrps = []
1336
+ const removeGrps = []
1337
+ // add
1338
+ for (let i = 0; i < jsonBody.groups.length; i++) {
1339
+ let found = false
1340
+ for (let j = 0; j < currentGroups.length; j++) {
1341
+ if (jsonBody.groups[i].value === currentGroups[j].value) {
1342
+ found = true
1343
+ break
1344
+ }
1345
+ }
1346
+ if (!found && jsonBody.groups[i].value) addGrps.push(jsonBody.groups[i].value)
1347
+ }
1348
+ // remove
1349
+ for (let i = 0; i < currentGroups.length; i++) {
1350
+ let found = false
1351
+ for (let j = 0; j < jsonBody.groups.length; j++) {
1352
+ if (currentGroups[i].value === jsonBody.groups[j].value) {
1353
+ found = true
1354
+ break
1355
+ }
1356
+ }
1357
+ if (!found && currentGroups[i].value) removeGrps.push(currentGroups[i].value)
1358
+ }
1359
+
1360
+ const addGroups = async (grp) => {
1361
+ const obj = { members: [{ value: id }] }
1362
+ return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj)
1363
+ }
1364
+
1365
+ const removeGroups = async (grp) => {
1366
+ const obj = { members: [{ operation: 'delete', value: id }] }
1367
+ return await this[handler.groups.modifyMethod](ctx.params.baseEntity, grp, obj)
1368
+ }
1369
+
1370
+ let errRemove
1371
+ if (!config.scim.usePutSoftSync) { // default will remove any existing groups not included, usePutSoftSync=true prevents removing groups (only add groups)
1372
+ await Promise.all(removeGrps.map((grp) => removeGroups(grp)))
1373
+ .then()
1374
+ .catch((err) => {
1375
+ errRemove = err
1376
+ })
1377
+ }
1378
+
1379
+ let errAdd
1380
+ await Promise.all(addGrps.map((grp) => addGroups(grp)))
1381
+ .then()
1382
+ .catch((err) => {
1383
+ errAdd = err
1384
+ })
1385
+
1386
+ let errMsg = ''
1387
+ if (errRemove) errMsg = `removeGroups error: ${errRemove.message}`
1388
+ if (errAdd) errMsg += `${errMsg ? ' ' : ''}addGroups error: ${errAdd.message}`
1389
+ if (errMsg) throw new Error(errMsg)
1390
+ }
1391
+
1305
1392
  // get updated object
1306
1393
  logger.debug(`${gwName}[${pluginName}] calling "${handle.getMethod}" and awaiting result`)
1307
1394
  res = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'id', operator: 'eq', value: id }, [])
@@ -1313,7 +1400,7 @@ const ScimGateway = function () {
1313
1400
  else throw Error(`put using method ${handle.getMethod} got unexpected response: ${JSON.stringify(res)}`)
1314
1401
 
1315
1402
  // include groups
1316
- if (handle.getMethod === handler.users.getMethod) {
1403
+ if (handle.getMethod === handler.users.getMethod && typeof this[handler.groups.getMethod] === 'function') {
1317
1404
  logger.debug(`${gwName}[${pluginName}] calling "${handler.groups.getMethod}" and awaiting result`)
1318
1405
  const res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: id }, ['members.value', 'id', 'displayName'])
1319
1406
  let grps = []
@@ -1346,7 +1433,8 @@ const ScimGateway = function () {
1346
1433
  ctx.body = e
1347
1434
  }
1348
1435
  }
1349
- }) // put
1436
+ }
1437
+ this.replaceUsrGrp = replaceUsrGrp // exposed
1350
1438
 
1351
1439
  // ==========================================
1352
1440
  // API POST (no SCIM)
@@ -1782,7 +1870,7 @@ const ScimGateway = function () {
1782
1870
  // exported methods
1783
1871
  //
1784
1872
  ScimGateway.prototype.endpointMap = endpointMap
1785
- ScimGateway.prototype.countries = endpointMap
1873
+ ScimGateway.prototype.countries = countries
1786
1874
 
1787
1875
  ScimGateway.prototype.getPassword = (pwEntity, configFile) => {
1788
1876
  return utils.getPassword(pwEntity, configFile) // utils.getPassword('scimgateway.password', './config/plugin-testmode.json');
@@ -1994,7 +2082,7 @@ ScimGateway.prototype.endpointMapper = function endpointMapper (direction, parse
1994
2082
  }
1995
2083
  }
1996
2084
  for (const key2 in mapObj) {
1997
- if (mapObj[key2].mapTo.toLowerCase() === key.toLowerCase()) {
2085
+ if (mapObj[key2].mapTo.split(',').map(item => item.trim().toLowerCase()).includes(key.toLowerCase())) {
1998
2086
  found = true
1999
2087
  if (mapObj[key2].type === 'array' && arrIndex && arrIndex >= 0) {
2000
2088
  dotNewObj[`${key2}.${arrIndex}`] = dotObj[keyOrg] // servicePlan.0.value => servicePlan.0 and groups[0].value => memberOf.0
@@ -2007,16 +2095,19 @@ ScimGateway.prototype.endpointMapper = function endpointMapper (direction, parse
2007
2095
  }
2008
2096
  } else { // string (get)
2009
2097
  const resArr = []
2010
- let strArr
2011
- if (Array.isArray(str)) strArr = str
2012
- else strArr = str.split(',').map(item => item.trim())
2098
+ let strArr = []
2099
+ if (Array.isArray(str)) {
2100
+ for (let i = 0; i < str.length; i++) {
2101
+ strArr = strArr.concat(str[i].split(',').map(item => item.trim())) // supports "id,userName" e.g. {"mapTo": "id,userName"}
2102
+ }
2103
+ } else strArr = str.split(',').map(item => item.trim())
2013
2104
  for (let i = 0; i < strArr.length; i++) {
2014
2105
  const attr = strArr[i]
2015
2106
  let found = false
2016
2107
  for (const key in mapObj) {
2017
- if (mapObj[key].mapTo === attr) {
2108
+ if (mapObj[key].mapTo && mapObj[key].mapTo.split(',').map(item => item.trim()).includes(attr)) { // supports { "mapTo": "userName,id" }
2018
2109
  found = true
2019
- resArr.push(key)
2110
+ if (!resArr.includes(key)) resArr.push(key)
2020
2111
  break
2021
2112
  } else if (attr === 'roles' && mapObj[key].mapTo === 'roles.value') { // allow get using attribute roles - convert to correct roles.value
2022
2113
  found = true
@@ -2098,7 +2189,10 @@ ScimGateway.prototype.endpointMapper = function endpointMapper (direction, parse
2098
2189
  mapTo = mapTo.replace('.', '##') // only first occurence
2099
2190
  noneCore = true
2100
2191
  }
2101
- dotNewObj[mapTo] = dotObj[key] // {"active": {"mapTo": "accountEnabled"} => str.replace("accountEnabled", "active")
2192
+ const arrMapTo = mapTo.split(',').map(item => item.trim()) // supports {"mapTo": "id,userName"}
2193
+ for (let i = 0; i < arrMapTo.length; i++) {
2194
+ dotNewObj[arrMapTo[i]] = dotObj[key] // {"active": {"mapTo": "accountEnabled"} => str.replace("accountEnabled", "active")
2195
+ }
2102
2196
  }
2103
2197
  let mapTo = mapObj[key].mapTo
2104
2198
  if (mapTo.startsWith('urn:')) {
@@ -2623,7 +2717,7 @@ const clearObjectValues = (o, parent) => {
2623
2717
  //
2624
2718
  const jsonErr = (scimVersion, pluginName, htmlErrCode, err, scimType) => {
2625
2719
  let errJson = {}
2626
- let msg = `ScimGateway[${pluginName}] `
2720
+ let msg = `scimgateway[${pluginName}] `
2627
2721
  err.constructor === Error ? msg += err.message : msg += err
2628
2722
 
2629
2723
  if (scimVersion !== '2.0' && scimVersion !== 2) { // v1.1
package/lib/utils.js CHANGED
@@ -373,3 +373,20 @@ module.exports.getEncrypted = function (pw, seed) {
373
373
  }
374
374
  return undefined
375
375
  }
376
+
377
+ // aadUnExtUpn convert Azure AD guest UPN to origin target UPN
378
+ // john.doe_company.com#EXT#@company.onmicrosoft.com => john.doe@company.com
379
+ module.exports.aadUnExtUpn = function (upn) {
380
+ const arr = upn.split('#EXT#')
381
+ if (arr.length === 1) return arr[0]
382
+ const extArr = arr[0].split('_')
383
+ if (extArr.length === 1) return extArr[0]
384
+ else {
385
+ upn = extArr[0]
386
+ for (let i = 1; i < extArr.length - 1; i++) {
387
+ upn += `_${extArr[i]}`
388
+ }
389
+ upn += `@${extArr[extArr.length - 1]}`
390
+ }
391
+ return upn
392
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.1.0",
3
+ "version": "4.1.3",
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
  })