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 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]
@@ -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 // { userName: 'bjensen } / { externalId: 'bjensen } / { id: 'bjensen }
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].forEach(el => {
270
- if (el.operation === 'delete') {
271
- userObj[key] = userObj[key].filter(e => e.value !== el.value)
272
- if (userObj[key].length < 1) delete userObj[key]
273
- } else { // add
274
- if (!userObj[key]) userObj[key] = []
275
- let exists
276
- 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)
277
- if (!exists) userObj[key].push(el)
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 // { displayName: 'Employees' } / { externalId: 'Employees' } / { id: 'Employees' }
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
- await attrObj.members.forEach(async el => {
496
- if (el.operation && el.operation === 'delete') { // delete member from group
497
- if (!el.value) groupObj.members = [] // members=[{"operation":"delete"}] => no value, delete all members
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
  }
@@ -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.find(findObj, { projection: projection }).sort({ _id: 1 }).count()
234
+ const totalResults = await users.countDocuments(findObj, { projection: projection })
234
235
  const arr = usersArr.map((obj) => {
235
- return decodeDotDate(obj)
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].forEach(el => {
353
- if (el.operation === 'delete') {
354
- userObj[key] = userObj[key].filter(e => e.value !== el.value)
355
- if (userObj[key].length < 1) delete userObj[key]
356
- } else { // add
357
- if (!userObj[key]) userObj[key] = []
358
- let exists
359
- 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)
360
- if (!exists) userObj[key].push(el)
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.find(findObj, { projection: projection }).count()
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
- for (const el of attrObj.members) {
638
- if (el.operation && el.operation === 'delete') {
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
- if (!el.value) {
660
+ if (!el.value) {
641
661
  // members=[{"operation":"delete"}] => no value, delete all members
642
- await groups.updateOne({ id: groupObj.id }, { $set: { members: [] } })
643
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} deleted all members`)
644
- isModified = true
645
- } else {
646
- await groups.updateMany({ id: groupObj.id }, { $pull: { members: { value: el.value } } })
647
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} deleted from group: ${el.value}`)
648
- isModified = true
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
- if (usrs.length === 1 && usrs[0].id === el.value) {
659
- if (!groupObj.members.some((element) => element.value === el.value)) {
660
- await groups.updateMany({ id: groupObj.id }, { $push: { members: { display: usrs[0].displayName || el.value, value: el.value } } })
661
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} added member to group: ${el.value}`)
662
- isModified = true
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
- } else usersNotExist.push(el.value)
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 = {
@@ -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
- const userObj = scimdata.Resources[0]
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
- scimdata.Resources = utils.stripObj(scimdata.Resources, ctx.query.attributes, ctx.query.excludedAttributes)
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 }, ['id', 'userName'], ctx.ctxCopy)
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 }, ['id', 'externalId'], ctx.ctxCopy)
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 }, ['id', 'externalId'], ctx.ctxCopy)
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 }, ['id', 'displayName'], ctx.ctxCopy)
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.id = obj.id
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
- if (ctx.query.attributes || ctx.query.excludedAttributes) {
1269
- logger.debug(`${gwName}[${pluginName}] calling "${handle.getMethod}" and awaiting result`)
1270
-
1271
- 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)
1272
- let scimdata = {
1273
- Resources: [],
1274
- totalResults: null
1275
- }
1276
- if (res) {
1277
- if (res.Resources && Array.isArray(res.Resources)) {
1278
- scimdata.Resources = res.Resources
1279
- scimdata.totalResults = res.totalResults
1280
- } else if (Array.isArray(res)) scimdata.Resources = res
1281
- else if (typeof (res) === 'object' && Object.keys(res).length > 0) scimdata.Resources[0] = res
1282
- }
1283
-
1284
- 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`)
1285
- const location = ctx.origin + ctx.path
1286
- ctx.set('Location', location)
1287
- scimdata = utils.stripObj(scimdata.Resources[0], ctx.query.attributes, ctx.query.excludedAttributes)
1288
- scimdata = addSchemas(scimdata, handle.description, isScimv2)
1289
- ctx.status = 200
1290
- ctx.body = scimdata
1291
- } else ctx.status = 204
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 = /^.*(\[type eq .*\]).*$/
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
- if (Array.isArray(arrMatches) && arrMatches.length === 2) { // [type eq "work"]
2652
- rePattern = /^\[type eq (.*)\]$/
2653
- arrMatches = arrMatches[1].match(rePattern)
2654
- if (Array.isArray(arrMatches) && arrMatches.length === 2) { // "work"
2655
- type = arrMatches[1].replace(/"/g, '') // work
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 === 3) {
2695
+ } else if (arrMatches.length === 4) {
2667
2696
  if (type) {
2668
- path = `${arrMatches[1]}.${type}.${arrMatches[2]}`
2669
- typeElement = arrMatches[2] // streetAddress
2670
- } else path = `${arrMatches[1]}.${arrMatches[2]}` // NA
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
- scimdata[pathRoot][index][typeElement] = element.value
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.1",
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",
@@ -431,7 +431,7 @@ describe('plugin-loki tests', () => {
431
431
  .send(user)
432
432
  .end(function (err, res) {
433
433
  expect(err).to.equal(null)
434
- expect(res.statusCode).to.equal(204)
434
+ expect(res.statusCode).to.equal(200)
435
435
  done()
436
436
  })
437
437
  })
@@ -368,7 +368,7 @@ describe('plugin-scim tests', () => {
368
368
  .send(user)
369
369
  .end(function (err, res) {
370
370
  expect(err).to.equal(null)
371
- expect(res.statusCode).to.equal(204)
371
+ expect(res.statusCode).to.equal(200)
372
372
  done()
373
373
  })
374
374
  })