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 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]
@@ -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,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 => e.value !== el.value)
272
- if (userObj[key].length < 1) delete userObj[key]
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
- 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)
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 // { displayName: 'Employees' } / { externalId: 'Employees' } / { id: 'Employees' }
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
- 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
- }
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
  }
@@ -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,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 => e.value !== el.value)
355
- if (userObj[key].length < 1) delete userObj[key]
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
- 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)
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.find(findObj, { projection: projection }).count()
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
- for (const el of attrObj.members) {
638
- if (el.operation && el.operation === 'delete') {
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
- if (!el.value) {
651
+ if (!el.value) {
641
652
  // 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}`)
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
- 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
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
- } else usersNotExist.push(el.value)
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 = {
@@ -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,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 = /^.*(\[type eq .*\]).*$/
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
- 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
- }
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 === 3) {
2709
+ } else if (arrMatches.length === 4) {
2667
2710
  if (type) {
2668
- path = `${arrMatches[1]}.${type}.${arrMatches[2]}`
2669
- typeElement = arrMatches[2] // streetAddress
2670
- } else path = `${arrMatches[1]}.${arrMatches[2]}` // NA
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
- scimdata[pathRoot][index][typeElement] = element.value
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.1",
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",
@@ -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
  })