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 +49 -7
- 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 +157 -40
- package/package.json +1 -1
- package/test/lib/plugin-loki.js +9 -3
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
|
|
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
|

|
|
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).
|
|
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
|
|
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
|
|
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
|
|
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');
|
|
@@ -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
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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 = `
|
|
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.
|
|
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",
|
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
|
})
|