scimgateway 4.1.1 → 4.1.4
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 +54 -3
- 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/plugin-loki.js +1 -1
- package/lib/plugin-mongodb.js +1 -1
- package/lib/scimgateway.js +170 -47
- package/package.json +1 -1
- package/test/lib/plugin-loki.js +9 -3
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Latest news:
|
|
|
31
31
|
|
|
32
32
|
## Overview
|
|
33
33
|
|
|
34
|
-
With SCIM Gateway we
|
|
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
|
|
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,54 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1139
1142
|
|
|
1140
1143
|
## Change log
|
|
1141
1144
|
|
|
1145
|
+
### v4.1.4
|
|
1146
|
+
[Fixed]
|
|
1147
|
+
|
|
1148
|
+
- TypeConvert logic for multivalue attribute `addresses` did not correctly catch duplicate entries
|
|
1149
|
+
- 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
|
|
1150
|
+
|
|
1151
|
+
### v4.1.3
|
|
1152
|
+
[Fixed]
|
|
1153
|
+
|
|
1154
|
+
- createUser response did not include the id that was returned by plugin
|
|
1155
|
+
|
|
1156
|
+
[Added]
|
|
1157
|
+
|
|
1158
|
+
- 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
|
|
1159
|
+
|
|
1160
|
+
Example:
|
|
1161
|
+
|
|
1162
|
+
PUT /Users/bjensen
|
|
1163
|
+
{
|
|
1164
|
+
...
|
|
1165
|
+
"groups": [
|
|
1166
|
+
{"value":"Employees","display":"Employees"},
|
|
1167
|
+
{"value":"Admins","display":"Admins"}
|
|
1168
|
+
],
|
|
1169
|
+
...
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
### v4.1.2
|
|
1176
|
+
[Added]
|
|
1177
|
+
|
|
1178
|
+
- endpointMapper supporting one to many mappings using a comma separated list of attributes in the `mapTo`
|
|
1179
|
+
|
|
1180
|
+
Configuration example:
|
|
1181
|
+
|
|
1182
|
+
"map": {
|
|
1183
|
+
"user": {
|
|
1184
|
+
"PersonnelNumber": {
|
|
1185
|
+
"mapTo": "id,userName",
|
|
1186
|
+
"type": "string"
|
|
1187
|
+
},
|
|
1188
|
+
...
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
|
|
1142
1193
|
### v4.1.1
|
|
1143
1194
|
[Added]
|
|
1144
1195
|
|
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/plugin-loki.js
CHANGED
|
@@ -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
|
})
|
package/lib/plugin-mongodb.js
CHANGED
|
@@ -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
|
})
|
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 = {}
|
|
@@ -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
|
-
})
|
|
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])
|
|
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])
|
|
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 =
|
|
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');
|
|
@@ -1994,7 +2115,7 @@ ScimGateway.prototype.endpointMapper = function endpointMapper (direction, parse
|
|
|
1994
2115
|
}
|
|
1995
2116
|
}
|
|
1996
2117
|
for (const key2 in mapObj) {
|
|
1997
|
-
if (mapObj[key2].mapTo.
|
|
2118
|
+
if (mapObj[key2].mapTo.split(',').map(item => item.trim().toLowerCase()).includes(key.toLowerCase())) {
|
|
1998
2119
|
found = true
|
|
1999
2120
|
if (mapObj[key2].type === 'array' && arrIndex && arrIndex >= 0) {
|
|
2000
2121
|
dotNewObj[`${key2}.${arrIndex}`] = dotObj[keyOrg] // servicePlan.0.value => servicePlan.0 and groups[0].value => memberOf.0
|
|
@@ -2007,16 +2128,19 @@ ScimGateway.prototype.endpointMapper = function endpointMapper (direction, parse
|
|
|
2007
2128
|
}
|
|
2008
2129
|
} else { // string (get)
|
|
2009
2130
|
const resArr = []
|
|
2010
|
-
let strArr
|
|
2011
|
-
if (Array.isArray(str))
|
|
2012
|
-
|
|
2131
|
+
let strArr = []
|
|
2132
|
+
if (Array.isArray(str)) {
|
|
2133
|
+
for (let i = 0; i < str.length; i++) {
|
|
2134
|
+
strArr = strArr.concat(str[i].split(',').map(item => item.trim())) // supports "id,userName" e.g. {"mapTo": "id,userName"}
|
|
2135
|
+
}
|
|
2136
|
+
} else strArr = str.split(',').map(item => item.trim())
|
|
2013
2137
|
for (let i = 0; i < strArr.length; i++) {
|
|
2014
2138
|
const attr = strArr[i]
|
|
2015
2139
|
let found = false
|
|
2016
2140
|
for (const key in mapObj) {
|
|
2017
|
-
if (mapObj[key].mapTo
|
|
2141
|
+
if (mapObj[key].mapTo && mapObj[key].mapTo.split(',').map(item => item.trim()).includes(attr)) { // supports { "mapTo": "userName,id" }
|
|
2018
2142
|
found = true
|
|
2019
|
-
resArr.push(key)
|
|
2143
|
+
if (!resArr.includes(key)) resArr.push(key)
|
|
2020
2144
|
break
|
|
2021
2145
|
} else if (attr === 'roles' && mapObj[key].mapTo === 'roles.value') { // allow get using attribute roles - convert to correct roles.value
|
|
2022
2146
|
found = true
|
|
@@ -2098,7 +2222,10 @@ ScimGateway.prototype.endpointMapper = function endpointMapper (direction, parse
|
|
|
2098
2222
|
mapTo = mapTo.replace('.', '##') // only first occurence
|
|
2099
2223
|
noneCore = true
|
|
2100
2224
|
}
|
|
2101
|
-
|
|
2225
|
+
const arrMapTo = mapTo.split(',').map(item => item.trim()) // supports {"mapTo": "id,userName"}
|
|
2226
|
+
for (let i = 0; i < arrMapTo.length; i++) {
|
|
2227
|
+
dotNewObj[arrMapTo[i]] = dotObj[key] // {"active": {"mapTo": "accountEnabled"} => str.replace("accountEnabled", "active")
|
|
2228
|
+
}
|
|
2102
2229
|
}
|
|
2103
2230
|
let mapTo = mapObj[key].mapTo
|
|
2104
2231
|
if (mapTo.startsWith('urn:')) {
|
|
@@ -2400,7 +2527,7 @@ const notValidAttributes = (obj, validScimAttr) => {
|
|
|
2400
2527
|
// "type converted object" and blank deleted values
|
|
2401
2528
|
// {"name":{"givenName":"Rocky",formatted:""},"emails":{"work":{"value":"user@company.com","type":"work"}}}
|
|
2402
2529
|
//
|
|
2403
|
-
|
|
2530
|
+
ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
2404
2531
|
let scimdata = {}
|
|
2405
2532
|
if (!obj.Operations || !Array.isArray(obj.Operations)) return scimdata
|
|
2406
2533
|
const o = utils.copyObj(obj)
|
|
@@ -2587,7 +2714,7 @@ const convertedScim20 = (obj) => {
|
|
|
2587
2714
|
// clearObjectValues returns a new object having values set to blank
|
|
2588
2715
|
// array values are kept, but includes {"operation" : "delete"} - scim 1.1 formatted
|
|
2589
2716
|
// boolean values e.g. {"active" : true} are kept "as is"
|
|
2590
|
-
// parent
|
|
2717
|
+
// parent used for internal recursive logic
|
|
2591
2718
|
const clearObjectValues = (o, parent) => {
|
|
2592
2719
|
if (!o) return {}
|
|
2593
2720
|
let v, key
|
|
@@ -2595,18 +2722,14 @@ const clearObjectValues = (o, parent) => {
|
|
|
2595
2722
|
for (key in o) {
|
|
2596
2723
|
v = o[key]
|
|
2597
2724
|
if (typeof v === 'object' && v !== null) {
|
|
2598
|
-
const objProp = Object.getPrototypeOf(v)
|
|
2725
|
+
const objProp = Object.getPrototypeOf(v)
|
|
2599
2726
|
if (objProp !== null && objProp !== Object.getPrototypeOf({}) && objProp !== Object.getPrototypeOf([])) {
|
|
2600
|
-
output[key] = Object.assign(Object.create(v), v)
|
|
2727
|
+
output[key] = Object.assign(Object.create(v), v)
|
|
2601
2728
|
} else {
|
|
2602
2729
|
output[key] = clearObjectValues(v, key)
|
|
2603
2730
|
}
|
|
2604
|
-
} else if (key === 'type') {
|
|
2605
|
-
output[key] = v
|
|
2606
|
-
output.operation = 'delete'
|
|
2607
|
-
if (output.value) output.value = ''
|
|
2608
2731
|
} else {
|
|
2609
|
-
if (parent && !isNaN(parent)
|
|
2732
|
+
if (parent && !isNaN(parent)) { // array
|
|
2610
2733
|
output.operation = 'delete'
|
|
2611
2734
|
output[key] = v
|
|
2612
2735
|
} else {
|
|
@@ -2623,7 +2746,7 @@ const clearObjectValues = (o, parent) => {
|
|
|
2623
2746
|
//
|
|
2624
2747
|
const jsonErr = (scimVersion, pluginName, htmlErrCode, err, scimType) => {
|
|
2625
2748
|
let errJson = {}
|
|
2626
|
-
let msg = `
|
|
2749
|
+
let msg = `scimgateway[${pluginName}] `
|
|
2627
2750
|
err.constructor === Error ? msg += err.message : msg += err
|
|
2628
2751
|
|
|
2629
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.
|
|
3
|
+
"version": "4.1.4",
|
|
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
|
})
|