scimgateway 4.2.1 → 4.2.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 +12 -0
- package/lib/plugin-loki.js +63 -38
- package/lib/plugin-mongodb.js +76 -49
- package/lib/scimgateway.js +103 -53
- package/package.json +1 -1
- package/test/lib/plugin-loki.js +1 -1
- package/test/lib/plugin-scim.js +1 -1
package/README.md
CHANGED
|
@@ -1165,6 +1165,18 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1165
1165
|
|
|
1166
1166
|
## Change log
|
|
1167
1167
|
|
|
1168
|
+
### v4.2.3
|
|
1169
|
+
|
|
1170
|
+
[Fixed]
|
|
1171
|
+
|
|
1172
|
+
- plugin-loki and plugin-mongodb, for multi-value attributes like emails,phoneNumbers,... that includes primary attribute, only one is allowed having primary value set to true in the multi-value set.
|
|
1173
|
+
|
|
1174
|
+
### v4.2.2
|
|
1175
|
+
|
|
1176
|
+
[Fixed]
|
|
1177
|
+
|
|
1178
|
+
- some minor SCIM protocol complient adjustments for beeing fully SCIM API complient with [https://scimvalidator.microsoft.com](https://scimvalidator.microsoft.com)
|
|
1179
|
+
|
|
1168
1180
|
### v4.2.1
|
|
1169
1181
|
|
|
1170
1182
|
[Fixed]
|
package/lib/plugin-loki.js
CHANGED
|
@@ -147,7 +147,9 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
147
147
|
if (getObj.operator === '$eq' && ['id', 'userName', 'externalId'].includes(getObj.attribute)) {
|
|
148
148
|
// mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
|
|
149
149
|
const queryObj = {}
|
|
150
|
-
queryObj[getObj.attribute] = getObj.value
|
|
150
|
+
if (getObj.attribute === 'id') queryObj[getObj.attribute] = getObj.value
|
|
151
|
+
else queryObj[getObj.attribute] = { $regex: [getObj.value, 'i'] } // case insensitive
|
|
152
|
+
// new RegExp(`^${getObj.value}$`, 'i')
|
|
151
153
|
usersArr = users.find(queryObj)
|
|
152
154
|
} else if (getObj.operator === '$eq' && getObj.attribute === 'group.value') {
|
|
153
155
|
// optional - only used when groups are member of users, not default behavior - correspond to getGroupUsers() in versions < 4.x.x
|
|
@@ -265,17 +267,27 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
265
267
|
const userObj = res[0]
|
|
266
268
|
|
|
267
269
|
for (const key in attrObj) {
|
|
268
|
-
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g groups)
|
|
269
|
-
attrObj[key].
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
270
|
+
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
|
|
271
|
+
const delArr = attrObj[key].filter(el => el.operation === 'delete')
|
|
272
|
+
const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
|
|
273
|
+
if (!userObj[key]) userObj[key] = []
|
|
274
|
+
// delete
|
|
275
|
+
userObj[key] = userObj[key].filter(el => {
|
|
276
|
+
if (delArr.findIndex(e => e.value === el.value) >= 0) return false
|
|
277
|
+
return true
|
|
278
|
+
})
|
|
279
|
+
// add
|
|
280
|
+
addArr.forEach(el => {
|
|
281
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
282
|
+
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
283
|
+
const index = userObj[key].findIndex(e => e.primary === el.primary)
|
|
284
|
+
if (index >= 0) {
|
|
285
|
+
if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
|
|
286
|
+
else userObj[key][index].primary = undefined // remove primary attribute, only one primary
|
|
287
|
+
}
|
|
288
|
+
}
|
|
278
289
|
}
|
|
290
|
+
userObj[key].push(el)
|
|
279
291
|
})
|
|
280
292
|
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
281
293
|
if (!attrObj[key]) delete userObj[key] // blank or null
|
|
@@ -288,6 +300,15 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
288
300
|
if (userObj[key].length < 1) delete userObj[key]
|
|
289
301
|
} else { // modify/create multivalue
|
|
290
302
|
if (!userObj[key]) userObj[key] = []
|
|
303
|
+
if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
|
|
304
|
+
const primVal = attrObj[key][el].primary
|
|
305
|
+
if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
|
|
306
|
+
const index = userObj[key].findIndex(e => e.primary === primVal)
|
|
307
|
+
if (index >= 0) {
|
|
308
|
+
userObj[key][index].primary = undefined
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
291
312
|
const found = userObj[key].find((e, i) => {
|
|
292
313
|
if (e.type === el || (!e.type && el === 'undefined')) {
|
|
293
314
|
for (const k in attrObj[key][el]) {
|
|
@@ -389,7 +410,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
389
410
|
if (getObj.operator === '$eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
|
|
390
411
|
// mandatory - unique filtering - single unique group to be returned - correspond to getGroup() in versions < 4.x.x
|
|
391
412
|
const queryObj = {}
|
|
392
|
-
queryObj[getObj.attribute] = getObj.value
|
|
413
|
+
if (getObj.attribute === 'id') queryObj[getObj.attribute] = getObj.value
|
|
414
|
+
else queryObj[getObj.attribute] = { $regex: [getObj.value, 'i'] } // case insensitive
|
|
393
415
|
groupsArr = groups.find(queryObj)
|
|
394
416
|
} else if (getObj.operator === '$eq' && getObj.attribute === 'members.value') {
|
|
395
417
|
// mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
|
|
@@ -477,13 +499,6 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
477
499
|
const action = 'modifyGroup'
|
|
478
500
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
479
501
|
|
|
480
|
-
if (!attrObj.members) {
|
|
481
|
-
throw new Error(`${action} error: only supports modification of members`)
|
|
482
|
-
}
|
|
483
|
-
if (!Array.isArray(attrObj.members)) {
|
|
484
|
-
throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
|
|
485
|
-
}
|
|
486
|
-
|
|
487
502
|
const res = groups.find({ id: id })
|
|
488
503
|
if (res.length === 0) throw new Error(`${action} error: group id=${id} - group does not exist`)
|
|
489
504
|
if (res.length > 1) throw new Error(`${action} error: group id=${id} - group is not unique, more than one have been found`)
|
|
@@ -492,25 +507,35 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
492
507
|
if (!groupObj.members) groupObj.members = []
|
|
493
508
|
const usersNotExist = []
|
|
494
509
|
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
else groupObj.members = groupObj.members.filter(element => element.value !== el.value)
|
|
499
|
-
} else { // Add member to group
|
|
500
|
-
if (el.value) {
|
|
501
|
-
const getObj = { attribute: 'id', operator: 'eq', value: el.value }
|
|
502
|
-
const usrs = await scimgateway.getUsers(baseEntity, getObj, 'id,displayName', ctx) // check if user exist
|
|
503
|
-
if (usrs && usrs.Resources && usrs.Resources.length === 1 && usrs.Resources[0].id === el.value) {
|
|
504
|
-
const newMember = {
|
|
505
|
-
display: usrs.Resources[0].displayName || el.value,
|
|
506
|
-
value: el.value
|
|
507
|
-
}
|
|
508
|
-
const exists = groupObj.members.some(e => (e.value === el.value))
|
|
509
|
-
if (!exists) groupObj.members.push(newMember)
|
|
510
|
-
} else usersNotExist.push(el.value)
|
|
511
|
-
}
|
|
510
|
+
if (attrObj.members) {
|
|
511
|
+
if (!Array.isArray(attrObj.members)) {
|
|
512
|
+
throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
|
|
512
513
|
}
|
|
513
|
-
|
|
514
|
+
await attrObj.members.forEach(async el => {
|
|
515
|
+
if (el.operation && el.operation === 'delete') { // delete member from group
|
|
516
|
+
if (!el.value) groupObj.members = [] // members=[{"operation":"delete"}] => no value, delete all members
|
|
517
|
+
else groupObj.members = groupObj.members.filter(element => element.value !== el.value)
|
|
518
|
+
} else { // Add member to group
|
|
519
|
+
if (el.value) {
|
|
520
|
+
const getObj = { attribute: 'id', operator: 'eq', value: el.value }
|
|
521
|
+
const usrs = await scimgateway.getUsers(baseEntity, getObj, 'id,displayName', ctx) // check if user exist
|
|
522
|
+
if (usrs && usrs.Resources && usrs.Resources.length === 1 && usrs.Resources[0].id === el.value) {
|
|
523
|
+
const newMember = {
|
|
524
|
+
display: usrs.Resources[0].displayName || el.value,
|
|
525
|
+
value: el.value
|
|
526
|
+
}
|
|
527
|
+
const exists = groupObj.members.some(e => (e.value === el.value))
|
|
528
|
+
if (!exists) groupObj.members.push(newMember)
|
|
529
|
+
} else usersNotExist.push(el.value)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
delete attrObj.members
|
|
536
|
+
for (const key in attrObj) { // displayName/externalId
|
|
537
|
+
groupObj[key] = attrObj[key]
|
|
538
|
+
}
|
|
514
539
|
|
|
515
540
|
groups.update(groupObj)
|
|
516
541
|
|
|
@@ -532,7 +557,7 @@ const stripLoki = (obj) => { // remove loki meta data and insert scim
|
|
|
532
557
|
delete retObj.meta.updated
|
|
533
558
|
}
|
|
534
559
|
if (retObj.meta.revision !== undefined) {
|
|
535
|
-
retObj.meta.version = retObj.meta.revision
|
|
560
|
+
retObj.meta.version = `W/"${retObj.meta.revision}"`
|
|
536
561
|
delete retObj.meta.revision
|
|
537
562
|
}
|
|
538
563
|
}
|
package/lib/plugin-mongodb.js
CHANGED
|
@@ -194,7 +194,8 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
194
194
|
if (getObj.operator === '$eq' && ['id', 'userName', 'externalId'].includes(getObj.attribute)) {
|
|
195
195
|
// mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
|
|
196
196
|
findObj = {}
|
|
197
|
-
findObj[getObj.attribute] = getObj.value
|
|
197
|
+
if (getObj.attribute === 'id') findObj[getObj.attribute] = getObj.value
|
|
198
|
+
else findObj[getObj.attribute] = new RegExp(`^${getObj.value}$`, 'i') // case insensitive
|
|
198
199
|
} else if (getObj.operator === '$eq' && getObj.attribute === 'group.value') {
|
|
199
200
|
// optional - only used when groups are member of users, not default behavior - correspond to getGroupUsers() in versions < 4.x.x
|
|
200
201
|
findObj = { groups: { value: getObj.value } }
|
|
@@ -230,9 +231,13 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
230
231
|
try {
|
|
231
232
|
const projection = attributes.length > 0 ? getProjectionFromAttributes(attributes) : { _id: 0 }
|
|
232
233
|
const usersArr = await users.find(findObj, { projection: projection }).sort({ _id: 1 }).skip(getObj.startIndex - 1).limit(getObj.count).toArray()
|
|
233
|
-
const totalResults = await users.
|
|
234
|
+
const totalResults = await users.countDocuments(findObj, { projection: projection })
|
|
234
235
|
const arr = usersArr.map((obj) => {
|
|
235
|
-
|
|
236
|
+
const o = decodeDotDate(obj)
|
|
237
|
+
if (o.meta && o.meta.version !== undefined) {
|
|
238
|
+
o.meta.version = `W/"${o.meta.version}"`
|
|
239
|
+
}
|
|
240
|
+
return o
|
|
236
241
|
}) // virtual attribute groups automatically handled by scimgateway
|
|
237
242
|
Array.prototype.push.apply(ret.Resources, arr)
|
|
238
243
|
ret.totalResults = totalResults
|
|
@@ -348,17 +353,27 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
348
353
|
let userObj = decodeDotDate(res[0])
|
|
349
354
|
|
|
350
355
|
for (const key in attrObj) {
|
|
351
|
-
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g groups)
|
|
352
|
-
attrObj[key].
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
356
|
+
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
|
|
357
|
+
const delArr = attrObj[key].filter(el => el.operation === 'delete')
|
|
358
|
+
const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
|
|
359
|
+
if (!userObj[key]) userObj[key] = []
|
|
360
|
+
// delete
|
|
361
|
+
userObj[key] = userObj[key].filter(el => {
|
|
362
|
+
if (delArr.findIndex(e => e.value === el.value) >= 0) return false
|
|
363
|
+
return true
|
|
364
|
+
})
|
|
365
|
+
// add
|
|
366
|
+
addArr.forEach(el => {
|
|
367
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
368
|
+
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
369
|
+
const index = userObj[key].findIndex(e => e.primary === el.primary)
|
|
370
|
+
if (index >= 0) {
|
|
371
|
+
if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
|
|
372
|
+
else userObj[key][index].primary = undefined // remove primary attribute, only one primary
|
|
373
|
+
}
|
|
374
|
+
}
|
|
361
375
|
}
|
|
376
|
+
userObj[key].push(el)
|
|
362
377
|
})
|
|
363
378
|
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
364
379
|
if (!attrObj[key]) delete userObj[key] // blank or null
|
|
@@ -371,6 +386,15 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
371
386
|
if (userObj[key].length < 1) delete userObj[key]
|
|
372
387
|
} else { // modify/create multivalue
|
|
373
388
|
if (!userObj[key]) userObj[key] = []
|
|
389
|
+
if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
|
|
390
|
+
const primVal = attrObj[key][el].primary
|
|
391
|
+
if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
|
|
392
|
+
const index = userObj[key].findIndex(e => e.primary === primVal)
|
|
393
|
+
if (index >= 0) {
|
|
394
|
+
userObj[key][index].primary = undefined
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
374
398
|
const found = userObj[key].find((e, i) => {
|
|
375
399
|
if (e.type === el || (!e.type && el === 'undefined')) {
|
|
376
400
|
for (const k in attrObj[key][el]) {
|
|
@@ -493,7 +517,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
493
517
|
if (getObj.operator === '$eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
|
|
494
518
|
// mandatory - unique filtering - single unique group to be returned - correspond to getGroup() in versions < 4.x.x
|
|
495
519
|
findObj = {}
|
|
496
|
-
findObj[getObj.attribute] = getObj.value
|
|
520
|
+
if (getObj.attribute === 'id') findObj[getObj.attribute] = getObj.value
|
|
521
|
+
else findObj[getObj.attribute] = new RegExp(`^${getObj.value}$`, 'i') // case insensitive
|
|
497
522
|
} else if (getObj.operator === '$eq' && getObj.attribute === 'members.value') {
|
|
498
523
|
// mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
|
|
499
524
|
// Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
|
|
@@ -518,7 +543,6 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
518
543
|
// mandatory if-else logic - end
|
|
519
544
|
|
|
520
545
|
if (!findObj) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
|
521
|
-
|
|
522
546
|
if (!getObj.startIndex) getObj.startIndex = 1
|
|
523
547
|
if (!getObj.count) getObj.count = 200
|
|
524
548
|
|
|
@@ -530,10 +554,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
530
554
|
try {
|
|
531
555
|
const projection = attributes.length > 0 ? getProjectionFromAttributes(attributes) : { _id: 0 }
|
|
532
556
|
const groups = config.entity[baseEntity].collection.groups
|
|
533
|
-
|
|
534
557
|
const groupsArr = await groups.find(findObj, { projection: projection }).sort({ _id: 1 }).skip(getObj.startIndex - 1).limit(getObj.count).toArray()
|
|
535
|
-
const totalResults = await groups.
|
|
536
|
-
|
|
558
|
+
const totalResults = await groups.countDocuments(findObj, { projection: projection })
|
|
537
559
|
const arr = groupsArr.map((obj) => {
|
|
538
560
|
return decodeDotDate(obj)
|
|
539
561
|
})
|
|
@@ -611,13 +633,6 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
611
633
|
const action = 'modifyGroup'
|
|
612
634
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
613
635
|
|
|
614
|
-
if (!attrObj.members) {
|
|
615
|
-
throw new Error(`${action} error: only supports modification of members`)
|
|
616
|
-
}
|
|
617
|
-
if (!Array.isArray(attrObj.members)) {
|
|
618
|
-
throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
|
|
619
|
-
}
|
|
620
|
-
|
|
621
636
|
const users = config.entity[baseEntity].collection.users
|
|
622
637
|
const groups = config.entity[baseEntity].collection.groups
|
|
623
638
|
let res
|
|
@@ -632,40 +647,52 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
632
647
|
}
|
|
633
648
|
|
|
634
649
|
let groupObj = decodeDotDate(res[0])
|
|
650
|
+
if (!groupObj.members) groupObj.members = []
|
|
635
651
|
const usersNotExist = []
|
|
636
652
|
|
|
637
|
-
|
|
638
|
-
if (
|
|
653
|
+
if (attrObj.members) {
|
|
654
|
+
if (!Array.isArray(attrObj.members)) {
|
|
655
|
+
throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
|
|
656
|
+
}
|
|
657
|
+
for (const el of attrObj.members) {
|
|
658
|
+
if (el.operation && el.operation === 'delete') {
|
|
639
659
|
// delete member from group
|
|
640
|
-
|
|
660
|
+
if (!el.value) {
|
|
641
661
|
// members=[{"operation":"delete"}] => no value, delete all members
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
} else { // Add member to group
|
|
651
|
-
if (el.value) {
|
|
652
|
-
let usrs = []
|
|
653
|
-
try {
|
|
654
|
-
usrs = await users.find({ id: el.value }, { projection: { _id: 0 } }).toArray() // check if user exist
|
|
655
|
-
} catch (err) {
|
|
656
|
-
throw new Error(`${action} error: failed to find group id=${id} - ${err.message}`)
|
|
662
|
+
await groups.updateOne({ id: groupObj.id }, { $set: { members: [] } })
|
|
663
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} deleted all members`)
|
|
664
|
+
isModified = true
|
|
665
|
+
} else {
|
|
666
|
+
await groups.updateMany({ id: groupObj.id }, { $pull: { members: { value: el.value } } })
|
|
667
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} deleted from group: ${el.value}`)
|
|
668
|
+
isModified = true
|
|
657
669
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
670
|
+
} else { // Add member to group
|
|
671
|
+
if (el.value) {
|
|
672
|
+
let usrs = []
|
|
673
|
+
try {
|
|
674
|
+
usrs = await users.find({ id: el.value }, { projection: { _id: 0 } }).toArray() // check if user exist
|
|
675
|
+
} catch (err) {
|
|
676
|
+
throw new Error(`${action} error: failed to find group id=${id} - ${err.message}`)
|
|
663
677
|
}
|
|
664
|
-
|
|
678
|
+
if (usrs.length === 1 && usrs[0].id === el.value) {
|
|
679
|
+
if (!groupObj.members.some((element) => element.value === el.value)) {
|
|
680
|
+
await groups.updateMany({ id: groupObj.id }, { $push: { members: { display: usrs[0].displayName || el.value, value: el.value } } })
|
|
681
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} added member to group: ${el.value}`)
|
|
682
|
+
isModified = true
|
|
683
|
+
}
|
|
684
|
+
} else usersNotExist.push(el.value)
|
|
685
|
+
}
|
|
665
686
|
}
|
|
666
687
|
}
|
|
667
688
|
}
|
|
668
689
|
|
|
690
|
+
delete attrObj.members
|
|
691
|
+
if (Object.keys(attrObj).length > 0) { // displayName/externalId
|
|
692
|
+
await groups.updateOne({ id: groupObj.id }, { $set: attrObj })
|
|
693
|
+
isModified = true
|
|
694
|
+
}
|
|
695
|
+
|
|
669
696
|
if (!groupObj.meta) {
|
|
670
697
|
const now = Date.now()
|
|
671
698
|
groupObj.meta = {
|
package/lib/scimgateway.js
CHANGED
|
@@ -322,6 +322,7 @@ const ScimGateway = function () {
|
|
|
322
322
|
} else logger.info(`${gwName}[${pluginName}] ${ellapsed} ${ctx.request.ipcli} ${userName} ${ctx.request.method} ${ctx.request.href} Inbound = ${JSON.stringify(ctx.request.body)} Outbound = ${JSON.stringify(res)}${(config.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
|
|
323
323
|
requestCounter += 1 // logged on exit (not win process termination)
|
|
324
324
|
}
|
|
325
|
+
if (ctx.response.body && typeof ctx.response.body === 'object') ctx.set('Content-Type', 'application/scim+json; charset=utf-8')
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
// start auth methods - used by auth
|
|
@@ -523,10 +524,7 @@ const ScimGateway = function () {
|
|
|
523
524
|
authPassThrough(baseEntity, ctx.request.method, authType, authToken, ctx)])
|
|
524
525
|
.catch((err) => { throw (err) })
|
|
525
526
|
for (const i in arrResolve) {
|
|
526
|
-
if (arrResolve[i])
|
|
527
|
-
ctx.set('Content-Type', 'application/scim+json; charset=utf-8') // IE don't support JSON content to be shown in browser, pre IE/Edge versions did not support 'application/scim+json'
|
|
528
|
-
return next() // auth OK - continue with routes
|
|
529
|
-
}
|
|
527
|
+
if (arrResolve[i]) return next() // auth OK - continue with routes
|
|
530
528
|
}
|
|
531
529
|
// all false - invalid auth method or missing pluging config
|
|
532
530
|
let err
|
|
@@ -824,7 +822,7 @@ const ScimGateway = function () {
|
|
|
824
822
|
}
|
|
825
823
|
|
|
826
824
|
// check for user attribute groups and include if needed
|
|
827
|
-
|
|
825
|
+
let userObj = scimdata.Resources[0]
|
|
828
826
|
if (handle.getMethod === handler.users.getMethod && Object.keys(userObj).length > 0 && !userObj.groups && !config.scim.groupMemberOfUser) { // groupMemberOfUser can be set to true for skipping
|
|
829
827
|
let arrAttr = []
|
|
830
828
|
if (ctx.query.attributes) arrAttr = ctx.query.attributes.split(',')
|
|
@@ -849,6 +847,7 @@ const ScimGateway = function () {
|
|
|
849
847
|
}
|
|
850
848
|
}
|
|
851
849
|
const location = ctx.origin + ctx.path
|
|
850
|
+
userObj = addPrimaryAttrs(userObj)
|
|
852
851
|
scimdata = utils.stripObj(userObj, ctx.query.attributes, ctx.query.excludedAttributes)
|
|
853
852
|
scimdata = addSchemas(scimdata, handle.description, isScimv2)
|
|
854
853
|
if (scimdata.meta) scimdata.meta.location = location
|
|
@@ -1056,8 +1055,10 @@ const ScimGateway = function () {
|
|
|
1056
1055
|
|
|
1057
1056
|
let location = ctx.origin + ctx.path
|
|
1058
1057
|
if (ctx.query.attributes || (ctx.query.excludedAttributes && ctx.query.excludedAttributes.includes('meta'))) location = null
|
|
1059
|
-
|
|
1060
|
-
|
|
1058
|
+
for (let i = 0; i < scimdata.Resources.length; i++) {
|
|
1059
|
+
scimdata.Resources[i] = addPrimaryAttrs(scimdata.Resources[i])
|
|
1060
|
+
scimdata.Resources[i] = utils.stripObj(scimdata.Resources[i], ctx.query.attributes, ctx.query.excludedAttributes)
|
|
1061
|
+
}
|
|
1061
1062
|
scimdata = addResources(scimdata, ctx.query.startIndex, ctx.query.sortBy, ctx.query.sortOrder)
|
|
1062
1063
|
scimdata = addSchemas(scimdata, handle.description, isScimv2, location)
|
|
1063
1064
|
|
|
@@ -1134,34 +1135,35 @@ const ScimGateway = function () {
|
|
|
1134
1135
|
jsonBody[key] = res[key]
|
|
1135
1136
|
}
|
|
1136
1137
|
|
|
1137
|
-
if (!jsonBody.id) { // retrieve id
|
|
1138
|
+
if (!jsonBody.id) { // retrieve all attributes including id
|
|
1138
1139
|
let obj
|
|
1139
1140
|
try {
|
|
1140
1141
|
if (handle.createMethod === 'createUser') {
|
|
1141
1142
|
if (jsonBody.userName) {
|
|
1142
1143
|
jsonBody.id = jsonBody.userName
|
|
1143
|
-
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'userName', operator: 'eq', value: jsonBody.userName }, [
|
|
1144
|
+
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'userName', operator: 'eq', value: jsonBody.userName }, [], ctx.ctxCopy)
|
|
1144
1145
|
} else if (jsonBody.externalId) {
|
|
1145
1146
|
jsonBody.id = jsonBody.externalId
|
|
1146
|
-
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, [
|
|
1147
|
+
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, [], ctx.ctxCopy)
|
|
1147
1148
|
}
|
|
1148
1149
|
} else if (handle.createMethod === 'createGroup') {
|
|
1149
1150
|
if (jsonBody.externalId) {
|
|
1150
1151
|
jsonBody.id = jsonBody.externalId
|
|
1151
|
-
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, [
|
|
1152
|
+
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }, [], ctx.ctxCopy)
|
|
1152
1153
|
} else if (jsonBody.displayName) {
|
|
1153
1154
|
jsonBody.id = jsonBody.displayName
|
|
1154
|
-
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'displayName', operator: 'eq', value: jsonBody.displayName }, [
|
|
1155
|
+
obj = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'displayName', operator: 'eq', value: jsonBody.displayName }, [], ctx.ctxCopy)
|
|
1155
1156
|
}
|
|
1156
1157
|
}
|
|
1157
1158
|
} catch (err) { }
|
|
1158
|
-
if (obj && obj.id) jsonBody
|
|
1159
|
+
if (obj && obj.id) jsonBody = obj
|
|
1159
1160
|
}
|
|
1160
1161
|
|
|
1161
1162
|
const location = `${ctx.origin}${ctx.path}/${jsonBody.id}`
|
|
1162
1163
|
if (!jsonBody.meta) jsonBody.meta = {}
|
|
1163
1164
|
jsonBody.meta.location = location
|
|
1164
1165
|
delete jsonBody.password
|
|
1166
|
+
jsonBody = addPrimaryAttrs(jsonBody)
|
|
1165
1167
|
ctx.set('Location', location)
|
|
1166
1168
|
ctx.status = 201
|
|
1167
1169
|
ctx.body = jsonBody
|
|
@@ -1265,30 +1267,32 @@ const ScimGateway = function () {
|
|
|
1265
1267
|
logger.debug(`${gwName}[${pluginName}] calling "${handle.modifyMethod}" and awaiting result`)
|
|
1266
1268
|
try {
|
|
1267
1269
|
await this[handle.modifyMethod](ctx.params.baseEntity, id, scimdata, ctx.ctxCopy)
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
if (
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1270
|
+
// include full object in response
|
|
1271
|
+
if (handle.getMethod !== handler.users.getMethod && handle.getMethod !== handler.groups.getMethod) { // getUsers or getGroups not implemented
|
|
1272
|
+
ctx.status = 204
|
|
1273
|
+
return
|
|
1274
|
+
}
|
|
1275
|
+
logger.debug(`${gwName}[${pluginName}] calling "${handle.getMethod}" and awaiting result`)
|
|
1276
|
+
const res = await this[handle.getMethod](ctx.params.baseEntity, { attribute: 'id', operator: 'eq', value: id }, ctx.query.attributes ? ctx.query.attributes.split(',').map(item => item.trim()) : [], ctx.ctxCopy)
|
|
1277
|
+
scimdata = {
|
|
1278
|
+
Resources: [],
|
|
1279
|
+
totalResults: null
|
|
1280
|
+
}
|
|
1281
|
+
if (res) {
|
|
1282
|
+
if (res.Resources && Array.isArray(res.Resources)) {
|
|
1283
|
+
scimdata.Resources = res.Resources
|
|
1284
|
+
scimdata.totalResults = res.totalResults
|
|
1285
|
+
} else if (Array.isArray(res)) scimdata.Resources = res
|
|
1286
|
+
else if (typeof (res) === 'object' && Object.keys(res).length > 0) scimdata.Resources[0] = res
|
|
1287
|
+
}
|
|
1288
|
+
if (scimdata.Resources.length !== 1) throw new Error(`using ${handle.getMethod} to retrive user ${id} after ${handle.modifyMethod} but response did not include user object`)
|
|
1289
|
+
const location = ctx.origin + ctx.path
|
|
1290
|
+
ctx.set('Location', location)
|
|
1291
|
+
scimdata.Resources[0] = addPrimaryAttrs(scimdata.Resources[0])
|
|
1292
|
+
scimdata = utils.stripObj(scimdata.Resources[0], ctx.query.attributes, ctx.query.excludedAttributes)
|
|
1293
|
+
scimdata = addSchemas(scimdata, handle.description, isScimv2)
|
|
1294
|
+
ctx.status = 200
|
|
1295
|
+
ctx.body = scimdata
|
|
1292
1296
|
} catch (err) {
|
|
1293
1297
|
ctx.status = 500
|
|
1294
1298
|
const e = jsonErr(config.scim.version, pluginName, ctx.status, err)
|
|
@@ -1903,9 +1907,7 @@ const ScimGateway = function () {
|
|
|
1903
1907
|
if (Array.isArray(scimdata[key]) && (scimdata[key].length > 0)) {
|
|
1904
1908
|
if (key === 'groups' || key === 'members' || key === 'roles') {
|
|
1905
1909
|
scimdata[key].forEach(function (element, index) {
|
|
1906
|
-
if (element.value)
|
|
1907
|
-
scimdata[key][index].value = decodeURIComponent(element.value)
|
|
1908
|
-
}
|
|
1910
|
+
if (element.value) scimdata[key][index].value = decodeURIComponent(element.value)
|
|
1909
1911
|
})
|
|
1910
1912
|
} else if (multiValueTypes.includes(key)) { // "type converted object" // groups, roles, member and scim.excludeTypeConvert are not included
|
|
1911
1913
|
const tmpAddr = []
|
|
@@ -2602,6 +2604,30 @@ const addSchemas = (data, type, isScimv2, location) => {
|
|
|
2602
2604
|
return data
|
|
2603
2605
|
}
|
|
2604
2606
|
|
|
2607
|
+
// addPrimaryAttrs cheks for primary attributes (only for roles) and add them as standalone attributes
|
|
2608
|
+
// some IdP's may check for these e.g. Azure
|
|
2609
|
+
// e.g. {roles: [{value: "val1", primary: "True"}]}
|
|
2610
|
+
// gives:
|
|
2611
|
+
// { roles: [{value: "val1", primary: "True"}],
|
|
2612
|
+
// roles[primary eq "True"].value: "val1",
|
|
2613
|
+
// roles[primary eq "True"].primary: "True"}]
|
|
2614
|
+
// }
|
|
2615
|
+
const addPrimaryAttrs = (obj) => {
|
|
2616
|
+
const key = 'roles'
|
|
2617
|
+
if (!obj || typeof obj !== 'object') return obj
|
|
2618
|
+
if (!obj[key] || !Array.isArray(obj[key])) return obj
|
|
2619
|
+
const o = utils.copyObj(obj)
|
|
2620
|
+
const index = o[key].findIndex(el => (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')))
|
|
2621
|
+
if (index >= 0) {
|
|
2622
|
+
const prim = o[key][index]
|
|
2623
|
+
for (const k in prim) {
|
|
2624
|
+
const primKey = `${key}[primary eq ${typeof prim.primary === 'string' ? `"${prim.primary}"` : prim.primary}].${k}` // roles[primary eq true].value / roles[primary eq "True"].value``
|
|
2625
|
+
o[primKey] = prim[k] // { roles[primary eq true].value : "some-value" }
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
return o
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2605
2631
|
//
|
|
2606
2632
|
// Check and return none supported attributes
|
|
2607
2633
|
//
|
|
@@ -2634,6 +2660,8 @@ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
|
2634
2660
|
let scimdata = {}
|
|
2635
2661
|
if (!obj.Operations || !Array.isArray(obj.Operations)) return scimdata
|
|
2636
2662
|
const o = utils.copyObj(obj)
|
|
2663
|
+
const arrPrimaryDone = []
|
|
2664
|
+
const primaryOrgType = {}
|
|
2637
2665
|
|
|
2638
2666
|
for (let i = 0; i < o.Operations.length; i++) {
|
|
2639
2667
|
const element = o.Operations[i]
|
|
@@ -2641,33 +2669,43 @@ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
|
2641
2669
|
let typeElement = null
|
|
2642
2670
|
let path = null
|
|
2643
2671
|
let pathRoot = null
|
|
2644
|
-
let rePattern =
|
|
2672
|
+
let rePattern = /^.*\[(.*) eq (.*)\].*$/
|
|
2645
2673
|
let arrMatches = null
|
|
2674
|
+
let primaryValue = null
|
|
2646
2675
|
|
|
2647
2676
|
if (element.op) element.op = element.op.toLowerCase()
|
|
2648
2677
|
|
|
2649
2678
|
if (element.path) {
|
|
2650
2679
|
arrMatches = element.path.match(rePattern)
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
}
|
|
2680
|
+
|
|
2681
|
+
if (Array.isArray(arrMatches) && arrMatches.length === 3) { // [type eq "work"]
|
|
2682
|
+
if (arrMatches[1] === 'primary') {
|
|
2683
|
+
type = 'primary'
|
|
2684
|
+
primaryValue = arrMatches[2].replace(/"/g, '') // True
|
|
2685
|
+
} else type = arrMatches[2].replace(/"/g, '') // work
|
|
2657
2686
|
}
|
|
2658
2687
|
|
|
2659
|
-
rePattern = /^(.*)\[type eq .*\]\.(.*)$/ // "path":"addresses[type eq \"work\"].streetAddress"
|
|
2688
|
+
rePattern = /^(.*)\[(type|primary) eq .*\]\.(.*)$/ // "path":"addresses[type eq \"work\"].streetAddress" - "path":"roles[primary eq \"True\"].streetAddress"
|
|
2660
2689
|
arrMatches = element.path.match(rePattern)
|
|
2661
2690
|
if (Array.isArray(arrMatches)) {
|
|
2662
2691
|
if (arrMatches.length === 2) {
|
|
2663
2692
|
if (type) path = `${arrMatches[1]}.${type}`
|
|
2664
2693
|
else path = arrMatches[1]
|
|
2665
2694
|
pathRoot = arrMatches[1]
|
|
2666
|
-
} else if (arrMatches.length ===
|
|
2695
|
+
} else if (arrMatches.length === 4) {
|
|
2667
2696
|
if (type) {
|
|
2668
|
-
path = `${arrMatches[1]}.${type}.${arrMatches[
|
|
2669
|
-
typeElement = arrMatches[
|
|
2670
|
-
|
|
2697
|
+
path = `${arrMatches[1]}.${type}.${arrMatches[3]}`
|
|
2698
|
+
typeElement = arrMatches[3] // streetAddress
|
|
2699
|
+
|
|
2700
|
+
if (type === 'primary' && !arrPrimaryDone.includes(arrMatches[1])) { // make sure primary is included
|
|
2701
|
+
const pObj = utils.copyObj(element)
|
|
2702
|
+
pObj.path = pObj.path.substring(0, pObj.path.lastIndexOf('.')) + '.primary'
|
|
2703
|
+
pObj.value = primaryValue
|
|
2704
|
+
o.Operations.push(pObj)
|
|
2705
|
+
arrPrimaryDone.push(arrMatches[1])
|
|
2706
|
+
primaryOrgType[arrMatches[1]] = 'primary'
|
|
2707
|
+
}
|
|
2708
|
+
} else path = `${arrMatches[1]}.${arrMatches[3]}` // NA
|
|
2671
2709
|
pathRoot = arrMatches[1]
|
|
2672
2710
|
}
|
|
2673
2711
|
} else {
|
|
@@ -2735,7 +2773,9 @@ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
|
2735
2773
|
el[typeElement] = element.value
|
|
2736
2774
|
scimdata[pathRoot].push(el)
|
|
2737
2775
|
} else {
|
|
2738
|
-
|
|
2776
|
+
if (type === 'primary' && typeElement === 'type') { // type=primary, don't change but store and correct to original type later
|
|
2777
|
+
primaryOrgType[pathRoot] = element.value
|
|
2778
|
+
} else scimdata[pathRoot][index][typeElement] = element.value
|
|
2739
2779
|
if (element.op && element.op === 'remove') scimdata[pathRoot][index].operation = 'delete'
|
|
2740
2780
|
}
|
|
2741
2781
|
}
|
|
@@ -2809,6 +2849,16 @@ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
|
2809
2849
|
}
|
|
2810
2850
|
}
|
|
2811
2851
|
|
|
2852
|
+
for (const key in primaryOrgType) { // revert back to original type when included
|
|
2853
|
+
if (scimdata[key]) {
|
|
2854
|
+
const index = scimdata[key].findIndex(el => el.type === 'primary')
|
|
2855
|
+
if (index >= 0) {
|
|
2856
|
+
if (primaryOrgType[key] === 'primary') delete scimdata[key][index].type // temp have not been changed - remove
|
|
2857
|
+
else scimdata[key][index].type = primaryOrgType[key]
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2812
2862
|
// scimdata now SCIM 1.1 formatted, using convertedScim to get "type converted Object" and blank deleted values
|
|
2813
2863
|
return ScimGateway.prototype.convertedScim(scimdata)
|
|
2814
2864
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scimgateway",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.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
package/test/lib/plugin-scim.js
CHANGED