scimgateway 4.2.1 → 4.2.2
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 +6 -0
- package/lib/plugin-loki.js +50 -34
- package/lib/plugin-mongodb.js +63 -45
- package/lib/scimgateway.js +117 -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,12 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1165
1165
|
|
|
1166
1166
|
## Change log
|
|
1167
1167
|
|
|
1168
|
+
### v4.2.2
|
|
1169
|
+
|
|
1170
|
+
[Fixed]
|
|
1171
|
+
|
|
1172
|
+
- some minor SCIM protocol complient adjustments for beeing fully SCIM API complient with [https://scimvalidator.microsoft.com](https://scimvalidator.microsoft.com)
|
|
1173
|
+
|
|
1168
1174
|
### v4.2.1
|
|
1169
1175
|
|
|
1170
1176
|
[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,16 +267,26 @@ 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)
|
|
270
|
+
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups)
|
|
269
271
|
attrObj[key].forEach(el => {
|
|
270
272
|
if (el.operation === 'delete') {
|
|
271
|
-
userObj[key] = userObj[key].filter(e =>
|
|
272
|
-
|
|
273
|
+
userObj[key] = userObj[key].filter(e => {
|
|
274
|
+
if (Object.prototype.hasOwnProperty.call(el, 'value') && el.value !== e.value) return true
|
|
275
|
+
if (Object.prototype.hasOwnProperty.call(el, 'type') && el.type !== e.type) return true
|
|
276
|
+
if (Object.prototype.hasOwnProperty.call(el, 'display') && el.display !== e.display) return true
|
|
277
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary') && el.primary !== e.primary) return true
|
|
278
|
+
return false
|
|
279
|
+
})
|
|
273
280
|
} else { // add
|
|
274
281
|
if (!userObj[key]) userObj[key] = []
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
283
|
+
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
284
|
+
// delete any existing primary before adding
|
|
285
|
+
const index = userObj[key].findIndex(e => e.primary === el.primary)
|
|
286
|
+
if (index >= 0) userObj[key].splice(index, 1)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
userObj[key].push(el)
|
|
278
290
|
}
|
|
279
291
|
})
|
|
280
292
|
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
@@ -389,7 +401,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
389
401
|
if (getObj.operator === '$eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
|
|
390
402
|
// mandatory - unique filtering - single unique group to be returned - correspond to getGroup() in versions < 4.x.x
|
|
391
403
|
const queryObj = {}
|
|
392
|
-
queryObj[getObj.attribute] = getObj.value
|
|
404
|
+
if (getObj.attribute === 'id') queryObj[getObj.attribute] = getObj.value
|
|
405
|
+
else queryObj[getObj.attribute] = { $regex: [getObj.value, 'i'] } // case insensitive
|
|
393
406
|
groupsArr = groups.find(queryObj)
|
|
394
407
|
} else if (getObj.operator === '$eq' && getObj.attribute === 'members.value') {
|
|
395
408
|
// mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
|
|
@@ -477,13 +490,6 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
477
490
|
const action = 'modifyGroup'
|
|
478
491
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
479
492
|
|
|
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
493
|
const res = groups.find({ id: id })
|
|
488
494
|
if (res.length === 0) throw new Error(`${action} error: group id=${id} - group does not exist`)
|
|
489
495
|
if (res.length > 1) throw new Error(`${action} error: group id=${id} - group is not unique, more than one have been found`)
|
|
@@ -492,25 +498,35 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
492
498
|
if (!groupObj.members) groupObj.members = []
|
|
493
499
|
const usersNotExist = []
|
|
494
500
|
|
|
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
|
-
}
|
|
501
|
+
if (attrObj.members) {
|
|
502
|
+
if (!Array.isArray(attrObj.members)) {
|
|
503
|
+
throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
|
|
512
504
|
}
|
|
513
|
-
|
|
505
|
+
await attrObj.members.forEach(async el => {
|
|
506
|
+
if (el.operation && el.operation === 'delete') { // delete member from group
|
|
507
|
+
if (!el.value) groupObj.members = [] // members=[{"operation":"delete"}] => no value, delete all members
|
|
508
|
+
else groupObj.members = groupObj.members.filter(element => element.value !== el.value)
|
|
509
|
+
} else { // Add member to group
|
|
510
|
+
if (el.value) {
|
|
511
|
+
const getObj = { attribute: 'id', operator: 'eq', value: el.value }
|
|
512
|
+
const usrs = await scimgateway.getUsers(baseEntity, getObj, 'id,displayName', ctx) // check if user exist
|
|
513
|
+
if (usrs && usrs.Resources && usrs.Resources.length === 1 && usrs.Resources[0].id === el.value) {
|
|
514
|
+
const newMember = {
|
|
515
|
+
display: usrs.Resources[0].displayName || el.value,
|
|
516
|
+
value: el.value
|
|
517
|
+
}
|
|
518
|
+
const exists = groupObj.members.some(e => (e.value === el.value))
|
|
519
|
+
if (!exists) groupObj.members.push(newMember)
|
|
520
|
+
} else usersNotExist.push(el.value)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
delete attrObj.members
|
|
527
|
+
for (const key in attrObj) { // displayName/externalId
|
|
528
|
+
groupObj[key] = attrObj[key]
|
|
529
|
+
}
|
|
514
530
|
|
|
515
531
|
groups.update(groupObj)
|
|
516
532
|
|
|
@@ -532,7 +548,7 @@ const stripLoki = (obj) => { // remove loki meta data and insert scim
|
|
|
532
548
|
delete retObj.meta.updated
|
|
533
549
|
}
|
|
534
550
|
if (retObj.meta.revision !== undefined) {
|
|
535
|
-
retObj.meta.version = retObj.meta.revision
|
|
551
|
+
retObj.meta.version = `W/"${retObj.meta.revision}"`
|
|
536
552
|
delete retObj.meta.revision
|
|
537
553
|
}
|
|
538
554
|
}
|
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,16 +353,26 @@ 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)
|
|
356
|
+
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups)
|
|
352
357
|
attrObj[key].forEach(el => {
|
|
353
358
|
if (el.operation === 'delete') {
|
|
354
|
-
userObj[key] = userObj[key].filter(e =>
|
|
355
|
-
|
|
359
|
+
userObj[key] = userObj[key].filter(e => {
|
|
360
|
+
if (Object.prototype.hasOwnProperty.call(el, 'value') && el.value !== e.value) return true
|
|
361
|
+
if (Object.prototype.hasOwnProperty.call(el, 'type') && el.type !== e.type) return true
|
|
362
|
+
if (Object.prototype.hasOwnProperty.call(el, 'display') && el.display !== e.display) return true
|
|
363
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary') && el.primary !== e.primary) return true
|
|
364
|
+
return false
|
|
365
|
+
})
|
|
356
366
|
} else { // add
|
|
357
367
|
if (!userObj[key]) userObj[key] = []
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
|
|
369
|
+
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
|
|
370
|
+
// delete any existing primary before adding
|
|
371
|
+
const index = userObj[key].findIndex(e => e.primary === el.primary)
|
|
372
|
+
if (index >= 0) userObj[key].splice(index, 1)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
userObj[key].push(el)
|
|
361
376
|
}
|
|
362
377
|
})
|
|
363
378
|
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
|
|
@@ -493,7 +508,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
493
508
|
if (getObj.operator === '$eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
|
|
494
509
|
// mandatory - unique filtering - single unique group to be returned - correspond to getGroup() in versions < 4.x.x
|
|
495
510
|
findObj = {}
|
|
496
|
-
findObj[getObj.attribute] = getObj.value
|
|
511
|
+
if (getObj.attribute === 'id') findObj[getObj.attribute] = getObj.value
|
|
512
|
+
else findObj[getObj.attribute] = new RegExp(`^${getObj.value}$`, 'i') // case insensitive
|
|
497
513
|
} else if (getObj.operator === '$eq' && getObj.attribute === 'members.value') {
|
|
498
514
|
// mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
|
|
499
515
|
// Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
|
|
@@ -518,7 +534,6 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
518
534
|
// mandatory if-else logic - end
|
|
519
535
|
|
|
520
536
|
if (!findObj) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
|
521
|
-
|
|
522
537
|
if (!getObj.startIndex) getObj.startIndex = 1
|
|
523
538
|
if (!getObj.count) getObj.count = 200
|
|
524
539
|
|
|
@@ -530,10 +545,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
530
545
|
try {
|
|
531
546
|
const projection = attributes.length > 0 ? getProjectionFromAttributes(attributes) : { _id: 0 }
|
|
532
547
|
const groups = config.entity[baseEntity].collection.groups
|
|
533
|
-
|
|
534
548
|
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
|
-
|
|
549
|
+
const totalResults = await groups.countDocuments(findObj, { projection: projection })
|
|
537
550
|
const arr = groupsArr.map((obj) => {
|
|
538
551
|
return decodeDotDate(obj)
|
|
539
552
|
})
|
|
@@ -611,13 +624,6 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
611
624
|
const action = 'modifyGroup'
|
|
612
625
|
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
613
626
|
|
|
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
627
|
const users = config.entity[baseEntity].collection.users
|
|
622
628
|
const groups = config.entity[baseEntity].collection.groups
|
|
623
629
|
let res
|
|
@@ -632,40 +638,52 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
632
638
|
}
|
|
633
639
|
|
|
634
640
|
let groupObj = decodeDotDate(res[0])
|
|
641
|
+
if (!groupObj.members) groupObj.members = []
|
|
635
642
|
const usersNotExist = []
|
|
636
643
|
|
|
637
|
-
|
|
638
|
-
if (
|
|
644
|
+
if (attrObj.members) {
|
|
645
|
+
if (!Array.isArray(attrObj.members)) {
|
|
646
|
+
throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
|
|
647
|
+
}
|
|
648
|
+
for (const el of attrObj.members) {
|
|
649
|
+
if (el.operation && el.operation === 'delete') {
|
|
639
650
|
// delete member from group
|
|
640
|
-
|
|
651
|
+
if (!el.value) {
|
|
641
652
|
// 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}`)
|
|
653
|
+
await groups.updateOne({ id: groupObj.id }, { $set: { members: [] } })
|
|
654
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} deleted all members`)
|
|
655
|
+
isModified = true
|
|
656
|
+
} else {
|
|
657
|
+
await groups.updateMany({ id: groupObj.id }, { $pull: { members: { value: el.value } } })
|
|
658
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} deleted from group: ${el.value}`)
|
|
659
|
+
isModified = true
|
|
657
660
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
661
|
+
} else { // Add member to group
|
|
662
|
+
if (el.value) {
|
|
663
|
+
let usrs = []
|
|
664
|
+
try {
|
|
665
|
+
usrs = await users.find({ id: el.value }, { projection: { _id: 0 } }).toArray() // check if user exist
|
|
666
|
+
} catch (err) {
|
|
667
|
+
throw new Error(`${action} error: failed to find group id=${id} - ${err.message}`)
|
|
663
668
|
}
|
|
664
|
-
|
|
669
|
+
if (usrs.length === 1 && usrs[0].id === el.value) {
|
|
670
|
+
if (!groupObj.members.some((element) => element.value === el.value)) {
|
|
671
|
+
await groups.updateMany({ id: groupObj.id }, { $push: { members: { display: usrs[0].displayName || el.value, value: el.value } } })
|
|
672
|
+
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} added member to group: ${el.value}`)
|
|
673
|
+
isModified = true
|
|
674
|
+
}
|
|
675
|
+
} else usersNotExist.push(el.value)
|
|
676
|
+
}
|
|
665
677
|
}
|
|
666
678
|
}
|
|
667
679
|
}
|
|
668
680
|
|
|
681
|
+
delete attrObj.members
|
|
682
|
+
if (Object.keys(attrObj).length > 0) { // displayName/externalId
|
|
683
|
+
await groups.updateOne({ id: groupObj.id }, { $set: attrObj })
|
|
684
|
+
isModified = true
|
|
685
|
+
}
|
|
686
|
+
|
|
669
687
|
if (!groupObj.meta) {
|
|
670
688
|
const now = Date.now()
|
|
671
689
|
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,44 @@ const addSchemas = (data, type, isScimv2, location) => {
|
|
|
2602
2604
|
return data
|
|
2603
2605
|
}
|
|
2604
2606
|
|
|
2607
|
+
// addPrimaryAttrs cheks for primary attributes 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
|
+
if (!obj || typeof obj !== 'object') return obj
|
|
2617
|
+
let arrObj
|
|
2618
|
+
if (!Array.isArray(obj)) arrObj = [obj]
|
|
2619
|
+
else {
|
|
2620
|
+
if (obj.length < 1) return obj
|
|
2621
|
+
arrObj = obj
|
|
2622
|
+
}
|
|
2623
|
+
let arrRet = []
|
|
2624
|
+
arrRet = arrObj.map(obj => {
|
|
2625
|
+
const o = utils.copyObj(obj)
|
|
2626
|
+
for (const key in o) {
|
|
2627
|
+
if (Array.isArray(o[key]) && key !== 'groups' && key !== 'members') {
|
|
2628
|
+
// check for primary attribute
|
|
2629
|
+
const index = o[key].findIndex(el => (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')))
|
|
2630
|
+
if (index >= 0) {
|
|
2631
|
+
const prim = o[key][index]
|
|
2632
|
+
for (const k in prim) {
|
|
2633
|
+
const primKey = `${key}[primary eq ${typeof prim.primary === 'string' ? `"${prim.primary}"` : prim.primary}].${k}` // roles[primary eq true].value / roles[primary eq "True"].value``
|
|
2634
|
+
o[primKey] = prim[k] // { roles[primary eq true].value : "some-value" }
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return o
|
|
2640
|
+
})
|
|
2641
|
+
if (!Array.isArray(obj)) return arrRet[0]
|
|
2642
|
+
return arrRet
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2605
2645
|
//
|
|
2606
2646
|
// Check and return none supported attributes
|
|
2607
2647
|
//
|
|
@@ -2634,6 +2674,8 @@ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
|
2634
2674
|
let scimdata = {}
|
|
2635
2675
|
if (!obj.Operations || !Array.isArray(obj.Operations)) return scimdata
|
|
2636
2676
|
const o = utils.copyObj(obj)
|
|
2677
|
+
const arrPrimaryDone = []
|
|
2678
|
+
const primaryOrgType = {}
|
|
2637
2679
|
|
|
2638
2680
|
for (let i = 0; i < o.Operations.length; i++) {
|
|
2639
2681
|
const element = o.Operations[i]
|
|
@@ -2641,33 +2683,43 @@ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
|
2641
2683
|
let typeElement = null
|
|
2642
2684
|
let path = null
|
|
2643
2685
|
let pathRoot = null
|
|
2644
|
-
let rePattern =
|
|
2686
|
+
let rePattern = /^.*\[(.*) eq (.*)\].*$/
|
|
2645
2687
|
let arrMatches = null
|
|
2688
|
+
let primaryValue = null
|
|
2646
2689
|
|
|
2647
2690
|
if (element.op) element.op = element.op.toLowerCase()
|
|
2648
2691
|
|
|
2649
2692
|
if (element.path) {
|
|
2650
2693
|
arrMatches = element.path.match(rePattern)
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
}
|
|
2694
|
+
|
|
2695
|
+
if (Array.isArray(arrMatches) && arrMatches.length === 3) { // [type eq "work"]
|
|
2696
|
+
if (arrMatches[1] === 'primary') {
|
|
2697
|
+
type = 'primary'
|
|
2698
|
+
primaryValue = arrMatches[2].replace(/"/g, '') // True
|
|
2699
|
+
} else type = arrMatches[2].replace(/"/g, '') // work
|
|
2657
2700
|
}
|
|
2658
2701
|
|
|
2659
|
-
rePattern = /^(.*)\[type eq .*\]\.(.*)$/ // "path":"addresses[type eq \"work\"].streetAddress"
|
|
2702
|
+
rePattern = /^(.*)\[(type|primary) eq .*\]\.(.*)$/ // "path":"addresses[type eq \"work\"].streetAddress" - "path":"roles[primary eq \"True\"].streetAddress"
|
|
2660
2703
|
arrMatches = element.path.match(rePattern)
|
|
2661
2704
|
if (Array.isArray(arrMatches)) {
|
|
2662
2705
|
if (arrMatches.length === 2) {
|
|
2663
2706
|
if (type) path = `${arrMatches[1]}.${type}`
|
|
2664
2707
|
else path = arrMatches[1]
|
|
2665
2708
|
pathRoot = arrMatches[1]
|
|
2666
|
-
} else if (arrMatches.length ===
|
|
2709
|
+
} else if (arrMatches.length === 4) {
|
|
2667
2710
|
if (type) {
|
|
2668
|
-
path = `${arrMatches[1]}.${type}.${arrMatches[
|
|
2669
|
-
typeElement = arrMatches[
|
|
2670
|
-
|
|
2711
|
+
path = `${arrMatches[1]}.${type}.${arrMatches[3]}`
|
|
2712
|
+
typeElement = arrMatches[3] // streetAddress
|
|
2713
|
+
|
|
2714
|
+
if (type === 'primary' && !arrPrimaryDone.includes(arrMatches[1])) { // make sure primary is included
|
|
2715
|
+
const pObj = utils.copyObj(element)
|
|
2716
|
+
pObj.path = pObj.path.substring(0, pObj.path.lastIndexOf('.')) + '.primary'
|
|
2717
|
+
pObj.value = primaryValue
|
|
2718
|
+
o.Operations.push(pObj)
|
|
2719
|
+
arrPrimaryDone.push(arrMatches[1])
|
|
2720
|
+
primaryOrgType[arrMatches[1]] = 'primary'
|
|
2721
|
+
}
|
|
2722
|
+
} else path = `${arrMatches[1]}.${arrMatches[3]}` // NA
|
|
2671
2723
|
pathRoot = arrMatches[1]
|
|
2672
2724
|
}
|
|
2673
2725
|
} else {
|
|
@@ -2735,7 +2787,9 @@ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
|
2735
2787
|
el[typeElement] = element.value
|
|
2736
2788
|
scimdata[pathRoot].push(el)
|
|
2737
2789
|
} else {
|
|
2738
|
-
|
|
2790
|
+
if (type === 'primary' && typeElement === 'type') { // type=primary, don't change but store and correct to original type later
|
|
2791
|
+
primaryOrgType[pathRoot] = element.value
|
|
2792
|
+
} else scimdata[pathRoot][index][typeElement] = element.value
|
|
2739
2793
|
if (element.op && element.op === 'remove') scimdata[pathRoot][index].operation = 'delete'
|
|
2740
2794
|
}
|
|
2741
2795
|
}
|
|
@@ -2809,6 +2863,16 @@ ScimGateway.prototype.convertedScim20 = function convertedScim20 (obj) {
|
|
|
2809
2863
|
}
|
|
2810
2864
|
}
|
|
2811
2865
|
|
|
2866
|
+
for (const key in primaryOrgType) { // revert back to original type when included
|
|
2867
|
+
if (scimdata[key]) {
|
|
2868
|
+
const index = scimdata[key].findIndex(el => el.type === 'primary')
|
|
2869
|
+
if (index >= 0) {
|
|
2870
|
+
if (primaryOrgType[key] === 'primary') delete scimdata[key][index].type // temp have not been changed - remove
|
|
2871
|
+
else scimdata[key][index].type = primaryOrgType[key]
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2812
2876
|
// scimdata now SCIM 1.1 formatted, using convertedScim to get "type converted Object" and blank deleted values
|
|
2813
2877
|
return ScimGateway.prototype.convertedScim(scimdata)
|
|
2814
2878
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scimgateway",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.2",
|
|
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