scimgateway 4.1.2 → 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
@@ -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,6 +1142,30 @@ 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
+
1142
1169
  ### v4.1.2
1143
1170
  [Added]
1144
1171
 
@@ -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": {
@@ -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');
@@ -2629,7 +2717,7 @@ const clearObjectValues = (o, parent) => {
2629
2717
  //
2630
2718
  const jsonErr = (scimVersion, pluginName, htmlErrCode, err, scimType) => {
2631
2719
  let errJson = {}
2632
- let msg = `ScimGateway[${pluginName}] `
2720
+ let msg = `scimgateway[${pluginName}] `
2633
2721
  err.constructor === Error ? msg += err.message : msg += err
2634
2722
 
2635
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.2",
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
  })