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 +29 -2
- package/config/plugin-api.json +2 -1
- package/config/plugin-azure-ad.json +2 -1
- package/config/plugin-forwardinc.json +2 -1
- package/config/plugin-ldap.json +2 -1
- package/config/plugin-loki.json +2 -1
- package/config/plugin-mongodb.json +2 -1
- package/config/plugin-mssql.json +2 -1
- package/config/plugin-saphana.json +2 -1
- package/config/plugin-scim.json +2 -1
- package/lib/scimgateway.js +116 -28
- package/lib/utils.js +17 -0
- package/package.json +1 -1
- package/test/lib/plugin-loki.js +9 -3
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
|
|
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
|
|
package/config/plugin-api.json
CHANGED
package/config/plugin-ldap.json
CHANGED
package/config/plugin-loki.json
CHANGED
package/config/plugin-mssql.json
CHANGED
package/config/plugin-scim.json
CHANGED
package/lib/scimgateway.js
CHANGED
|
@@ -276,8 +276,8 @@ const ScimGateway = function () {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
this.testmodeusers = scimDef.TestmodeUsers.Resources //
|
|
280
|
-
this.testmodegroups = scimDef.TestmodeGroups.Resources //
|
|
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:
|
|
1089
|
+
for (const key in res) { // merge any result e.g: {'id': 'xxxx'}
|
|
1089
1090
|
jsonBody[key] = res[key]
|
|
1090
1091
|
}
|
|
1091
1092
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
if (
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
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}] [
|
|
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
|
-
}
|
|
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 =
|
|
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 = `
|
|
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.
|
|
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",
|
package/test/lib/plugin-loki.js
CHANGED
|
@@ -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
|
-
|
|
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
|
})
|