scimgateway 5.3.2 → 5.3.4

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
@@ -1405,6 +1405,36 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1405
1405
 
1406
1406
  ## Change log
1407
1407
 
1408
+ ### v5.3.4
1409
+
1410
+ [Fixed]
1411
+
1412
+ - PATCH operations (modifyUser/modifyGroup) that includes `null` values, will now be converted to empty string `""`
1413
+
1414
+ {
1415
+ "schemas": [
1416
+ "urn:ietf:params:scim:api:messages:2.0:PatchOp"
1417
+ ],
1418
+ "Operations": [{
1419
+ "op": "replace",
1420
+ "value": {
1421
+ "name": {
1422
+ "formatted": "Smith, John",
1423
+ "honorificPrefix": null
1424
+ }
1425
+ }}
1426
+ ]
1427
+ }
1428
+
1429
+ In the example above, following will be sent to plugin:
1430
+ { "name": { "formatted": "Smith, John", "honorificPrefix": "" } }
1431
+
1432
+ ### v5.3.3
1433
+
1434
+ [Fixed]
1435
+
1436
+ - helper-rest, SamlBearer token-request now includes `new_token=true` to avoid retrieving an existing token that is about to expire
1437
+
1408
1438
  ### v5.3.2
1409
1439
 
1410
1440
  [Improved]
@@ -142,6 +142,7 @@ export class HelperRest {
142
142
  grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
143
143
  client_id: clientId,
144
144
  company_id: companyId,
145
+ new_token: true,
145
146
  assertion: await samlAssertion.run(context, cert, key, issuer, lifetime, clientId, nameId, userIdentifierFormat, tokenEndpoint, audience, delay),
146
147
  }
147
148
  break
@@ -265,11 +265,8 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
265
265
  if (parsedAttrObj instanceof Error) throw (parsedAttrObj) // error object
266
266
 
267
267
  const objManager: Record<string, any> = {}
268
- if (parsedAttrObj.manager) { // new manager
269
- objManager.manager = JSON.parse(JSON.stringify(parsedAttrObj.manager))
270
- delete parsedAttrObj.manager
271
- } else if (parsedAttrObj.manager === null) { // delete manager
272
- objManager.manager = null
268
+ if (Object.prototype.hasOwnProperty.call(parsedAttrObj, 'manager')) {
269
+ objManager.manager = parsedAttrObj.manager
273
270
  delete parsedAttrObj.manager
274
271
  }
275
272
 
@@ -309,6 +306,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
309
306
  const manager = () => {
310
307
  return new Promise((resolve, reject) => {
311
308
  (async () => {
309
+ if (!Object.prototype.hasOwnProperty.call(objManager, 'manager')) return resolve(null)
312
310
  let method: string | null = null
313
311
  let path: string | null = null
314
312
  let body: Record<string, any> | null = null
@@ -317,11 +315,11 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
317
315
  method = 'PUT'
318
316
  path = `/users/${id}/manager/$ref`
319
317
  body = { '@odata.id': `${graphUrl}/users/${objManager.manager}` }
320
- } else if (objManager.manager === null) { // delete manager
318
+ } else { // delete manager (null/undefined/'')
321
319
  method = 'DELETE'
322
320
  path = `/users/${id}/manager/$ref`
323
321
  body = null
324
- } else return resolve(null)
322
+ }
325
323
  try {
326
324
  await helper.doRequest(baseEntity, method, path, body, ctx)
327
325
  resolve(null)
@@ -380,31 +380,24 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
380
380
  }
381
381
  }
382
382
  } else {
383
- // None multi value attribute
384
- if (typeof (attrObj[key]) !== 'object' || attrObj[key] === null) {
385
- if (attrObj[key] === '' || attrObj[key] === null) delete userObj[key]
386
- else userObj[key] = attrObj[key]
387
- } else {
383
+ // None multi value attribute, blank will be deleted
384
+ if (typeof (attrObj[key]) === 'object' && attrObj[key] !== null) {
388
385
  // name.familyName=Bianchi
389
386
  if (!userObj[key]) userObj[key] = {} // e.g name object does not exist
390
- for (const sub in attrObj[key]) { // attributes to be cleard located in meta.attributes eg: {"meta":{"attributes":["name.familyName","profileUrl","title"]}
391
- if (sub === 'attributes' && Array.isArray(attrObj[key][sub])) {
392
- attrObj[key][sub].forEach((element) => {
393
- const arrSub = element.split('.')
394
- if (arrSub.length === 2) userObj[arrSub[0]][arrSub[1]] = '' // e.g. name.familyName
395
- else userObj[element] = ''
396
- })
397
- } else {
398
- if (Object.prototype.hasOwnProperty.call(attrObj[key][sub], 'value')
399
- && attrObj[key][sub].value === '') delete userObj[key][sub] // object having blank value attribute e.g. {"manager": {"value": "",...}}
400
- else if (attrObj[key][sub] === '') delete userObj[key][sub]
401
- else {
402
- if (!userObj[key]) userObj[key] = {} // may have been deleted by length check below
403
- userObj[key][sub] = attrObj[key][sub]
404
- }
405
- if (Object.keys(userObj[key]).length < 1) delete userObj[key]
387
+ for (const sub in attrObj[key]) {
388
+ if (!userObj[key]) userObj[key] = {}
389
+ if (Object.prototype.hasOwnProperty.call(attrObj[key][sub], 'value')
390
+ && attrObj[key][sub].value === '') delete userObj[key][sub] // object having blank value attribute e.g. {"manager": {"value": "",...}}
391
+ else if (attrObj[key][sub] === '') delete userObj[key][sub]
392
+ else {
393
+ if (!userObj[key]) userObj[key] = {} // may have been deleted by length check below
394
+ userObj[key][sub] = attrObj[key][sub]
406
395
  }
396
+ if (Object.keys(userObj[key]).length < 1) delete userObj[key]
407
397
  }
398
+ } else {
399
+ if (attrObj[key] === '') delete userObj[key]
400
+ else userObj[key] = attrObj[key]
408
401
  }
409
402
  }
410
403
  }
@@ -581,7 +574,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
581
574
  if (!Array.isArray(attrObj.members)) {
582
575
  throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
583
576
  }
584
- await attrObj.members.forEach(async (el) => {
577
+ for (const el of attrObj.members) {
585
578
  if (el.operation && el.operation === 'delete') { // delete member from group
586
579
  if (!el.value) groupObj.members = [] // members=[{"operation":"delete"}] => no value, delete all members
587
580
  else {
@@ -601,7 +594,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
601
594
  } else usersNotExist.push(el.value)
602
595
  }
603
596
  }
604
- })
597
+ }
605
598
  }
606
599
 
607
600
  delete attrObj.members
@@ -439,16 +439,12 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
439
439
  if (attrObj[key] === '' || attrObj[key] === null) delete userObj[key]
440
440
  else userObj[key] = attrObj[key]
441
441
  } else {
442
+ // None multi value attribute, blank will be deleted
443
+ if (typeof (attrObj[key]) === 'object' && attrObj[key] !== null) {
442
444
  // name.familyName=Bianchi
443
- if (!userObj[key]) userObj[key] = {} // e.g name object does not exist
444
- for (const sub in attrObj[key]) { // attributes to be cleard located in meta.attributes eg: {"meta":{"attributes":["name.familyName","profileUrl","title"]}
445
- if (sub === 'attributes' && Array.isArray(attrObj[key][sub])) {
446
- attrObj[key][sub].forEach((element) => {
447
- const arrSub = element.split('.')
448
- if (arrSub.length === 2) userObj[arrSub[0]][arrSub[1]] = '' // e.g. name.familyName
449
- else userObj[element] = ''
450
- })
451
- } else {
445
+ if (!userObj[key]) userObj[key] = {} // e.g name object does not exist
446
+ for (const sub in attrObj[key]) {
447
+ if (!userObj[key]) userObj[key] = {}
452
448
  if (Object.prototype.hasOwnProperty.call(attrObj[key][sub], 'value')
453
449
  && attrObj[key][sub].value === '') delete userObj[key][sub] // object having blank value attribute e.g. {"manager": {"value": "",...}}
454
450
  else if (attrObj[key][sub] === '') delete userObj[key][sub]
@@ -458,6 +454,9 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
458
454
  }
459
455
  if (Object.keys(userObj[key]).length < 1) delete userObj[key]
460
456
  }
457
+ } else {
458
+ if (attrObj[key] === '') delete userObj[key]
459
+ else userObj[key] = attrObj[key]
461
460
  }
462
461
  }
463
462
  }
@@ -436,7 +436,7 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
436
436
  try {
437
437
  const serviceClient = await getServiceClient(baseEntity, action, ctx)
438
438
 
439
- attrObj.members.forEach(async function (el) {
439
+ for (const el of attrObj.members) {
440
440
  if (el.operation && el.operation === 'delete') { // delete member from group
441
441
  const soapRequest = {
442
442
  groupID: id,
@@ -451,7 +451,6 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
451
451
  if (!result.return) {
452
452
  throw new Error(`${config[action].service}-removeUserFromGroup : Got empty response on soap request: ${soapRequest}`)
453
453
  }
454
- return null
455
454
  } else { // add member to group
456
455
  const soapRequest = {
457
456
  groupID: id,
@@ -466,9 +465,9 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
466
465
  if (!result.return) {
467
466
  throw new Error(`assignUserToGroup : Got empty response on soap request: ${soapRequest}`)
468
467
  }
469
- return null
470
468
  }
471
- })
469
+ }
470
+ return null
472
471
  } catch (err: any) {
473
472
  throw new Error(`${action} error: ${err.message}`)
474
473
  }
package/lib/utils-scim.ts CHANGED
@@ -87,37 +87,55 @@ export function convertedScim(obj: any, multiValueTypes: string[]): any {
87
87
  })
88
88
  delete scimdata[key]
89
89
  }
90
- }
91
- }
92
- if (scimdata.active && typeof scimdata.active === 'string') {
93
- const lcase = scimdata.active.toLowerCase()
94
- if (lcase === 'true') scimdata.active = true
95
- else if (lcase === 'false') scimdata.active = false
96
- }
97
- if (scimdata.meta) { // cleared attributes e.g { meta: { attributes: [ 'name.givenName', 'title' ] } }
98
- if (Array.isArray(scimdata.meta.attributes)) {
99
- scimdata.meta.attributes.forEach((el: string) => {
100
- let rootKey = ''
101
- let subKey = ''
102
- if (el.startsWith('urn:')) { // can't use dot.str on key having dot e.g. urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department
103
- const i = el.lastIndexOf(':')
104
- subKey = el.substring(i + 1)
105
- if (subKey === 'User' || subKey === 'Group') rootKey = el
106
- else rootKey = el.substring(0, i)
107
- }
108
- if (rootKey) {
109
- if (!scimdata[rootKey]) scimdata[rootKey] = {}
110
- dot.str(subKey, '', scimdata[rootKey])
111
- } else {
112
- dot.str(el, '', scimdata)
90
+ } else if (key === 'active' && typeof scimdata[key] === 'string') {
91
+ const lcase = scimdata.active.toLowerCase()
92
+ if (lcase === 'true') scimdata.active = true
93
+ else if (lcase === 'false') scimdata.active = false
94
+ } else if (key === 'meta') { // cleared attributes e.g { meta: { attributes: [ 'name.givenName', 'title' ] } }
95
+ if (Array.isArray(scimdata.meta.attributes)) {
96
+ scimdata.meta.attributes.forEach((el: string) => {
97
+ let rootKey = ''
98
+ let subKey = ''
99
+ if (el.startsWith('urn:')) { // can't use dot.str on key having dot e.g. urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department
100
+ const i = el.lastIndexOf(':')
101
+ subKey = el.substring(i + 1)
102
+ if (subKey === 'User' || subKey === 'Group') rootKey = el
103
+ else rootKey = el.substring(0, i)
104
+ }
105
+ if (rootKey) {
106
+ if (!scimdata[rootKey]) scimdata[rootKey] = {}
107
+ dot.str(subKey, '', scimdata[rootKey])
108
+ } else {
109
+ dot.str(el, '', scimdata)
110
+ }
111
+ })
112
+ }
113
+ delete scimdata.meta
114
+ } else { // replace any undefined/null with empty string
115
+ if (typeof scimdata[key] === 'object' && scimdata[key] !== null) {
116
+ for (const k in scimdata[key]) {
117
+ if (typeof scimdata[key][k] === 'object' && scimdata[key][k] !== null) {
118
+ for (const _k in scimdata[key][k]) {
119
+ if (scimdata[key][k][_k] === undefined || scimdata[key][k][_k] === null) {
120
+ scimdata[key][k][_k] = ''
121
+ }
122
+ }
123
+ } else {
124
+ if (scimdata[key][k] === undefined || scimdata[key][k] === null) {
125
+ scimdata[key][k] = ''
126
+ }
127
+ }
113
128
  }
114
- })
129
+ } else if (scimdata[key] === undefined || scimdata[key] === null) {
130
+ scimdata[key] = ''
131
+ }
115
132
  }
116
- delete scimdata.meta
117
133
  }
134
+
118
135
  for (const key in newMulti) {
119
136
  dot.copy(key, key, newMulti, scimdata)
120
137
  }
138
+
121
139
  return [scimdata, err]
122
140
  }
123
141
 
@@ -134,8 +152,8 @@ export function convertedScim(obj: any, multiValueTypes: string[]): any {
134
152
  * {"name":{"givenName":"Rocky",formatted:""},"emails":{"work":{"value":"user@company.com","type":"work"}}}
135
153
  */
136
154
  export function convertedScim20(obj: any, multiValueTypes: string[]): any {
137
- let scimdata: { [key: string]: any } = {}
138
- if (!obj.Operations || !Array.isArray(obj.Operations)) return scimdata
155
+ if (!obj.Operations || !Array.isArray(obj.Operations)) return {}
156
+ let scimdata: { [key: string]: any } = { meta: { attributes: [] } } // meta is used for deleted attributes
139
157
  const o: any = utils.copyObj(obj)
140
158
  const arrPrimaryDone: any = []
141
159
  const primaryOrgType: any = {}
@@ -289,10 +307,9 @@ export function convertedScim20(obj: any, multiValueTypes: string[]): any {
289
307
  })
290
308
  } else {
291
309
  let value = element.value[key]
292
- if (element.op && element.op === 'remove') {
293
- if (!scimdata.meta) scimdata.meta = {}
294
- if (!scimdata.meta.attributes) scimdata.meta.attributes = []
310
+ if (element?.op === 'remove' || value === undefined || value === null) {
295
311
  scimdata.meta.attributes.push(key)
312
+ continue
296
313
  }
297
314
  if (key.startsWith('urn:')) { // can't use dot.str on key having dot e.g. urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department
298
315
  const i = key.lastIndexOf(':')
@@ -301,6 +318,17 @@ export function convertedScim20(obj: any, multiValueTypes: string[]): any {
301
318
  if (k === 'User' || k === 'Group') rootKey = key
302
319
  else rootKey = key.substring(0, i) // urn:ietf:params:scim:schemas:extension:enterprise:2.0:User
303
320
  if (k === 'User' || k === 'Group') { // value is object
321
+ for (const _k in value) {
322
+ if (value[_k] === undefined || value[_k] === null) {
323
+ scimdata.meta.attributes.push(`${key}:${_k}`)
324
+ delete value[_k]
325
+ } else if (typeof value[_k] === 'object') { // manager.value
326
+ if (Object.prototype.hasOwnProperty.call(value[_k], 'value') && (value[_k].value === undefined || value[_k].value === null)) {
327
+ scimdata.meta.attributes.push(`${key}:${_k}.value`)
328
+ delete value[_k].value
329
+ }
330
+ }
331
+ }
304
332
  const o: Record<string, any> = {}
305
333
  o[rootKey] = value
306
334
  scimdata = utils.extendObj(scimdata, o)
@@ -316,14 +344,10 @@ export function convertedScim20(obj: any, multiValueTypes: string[]): any {
316
344
  } else {
317
345
  if (typeof value === 'object') {
318
346
  for (const k in element.value[key]) {
319
- if (element.op && element.op === 'remove') {
320
- if (!scimdata.meta) scimdata.meta = {}
321
- if (!scimdata.meta.attributes) scimdata.meta.attributes = []
347
+ value = element.value[key][k]
348
+ if ((element.op && element.op === 'remove') || value === null || value === undefined) {
322
349
  scimdata.meta.attributes.push(`${key}.${k}`)
323
- } else {
324
- value = element.value[key][k]
325
- dot.str(`${key}.${k}`, value, scimdata)
326
- }
350
+ } else dot.str(`${key}.${k}`, value, scimdata)
327
351
  }
328
352
  } else dot.str(key, value, scimdata)
329
353
  }
package/lib/utils.ts CHANGED
@@ -201,7 +201,8 @@ export const copyObj = (o: any): any => { // deep copy/clone faster than JSON.pa
201
201
 
202
202
  const _extendObj = (obj: Record<any, any>, src: Record<any, any>) => {
203
203
  Object.keys(src).forEach((key) => {
204
- if (typeof src[key] === 'object' && src[key] != null) {
204
+ if (typeof src[key] === 'object' && src[key] !== null) {
205
+ if (Object.keys(src[key]).length === 0) return
205
206
  if (typeof obj[key] === 'undefined') obj[key] = src[key]
206
207
  else if (Array.isArray(src[key])) {
207
208
  if (!Array.isArray(obj[key])) obj[key] = src[key]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.3.2",
3
+ "version": "5.3.4",
4
4
  "type": "module",
5
5
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
6
6
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
package/tsconfig.json CHANGED
@@ -3,7 +3,7 @@
3
3
  // Enable latest features
4
4
  "lib": ["ESNext", "DOM"],
5
5
  "target": "ESNext",
6
- "module": "ESNext",
6
+ "module": "Preserve",
7
7
  "moduleDetection": "force",
8
8
  "jsx": "react-jsx",
9
9
  "allowJs": true,