scimgateway 6.1.7 → 6.1.9

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
@@ -1303,6 +1303,18 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1303
1303
 
1304
1304
  ## Change log
1305
1305
 
1306
+ ### v6.1.9
1307
+
1308
+ [Improved]
1309
+
1310
+ - Some improvements to createUser/createGroup regarding the response object, which should contain the newly generated ID
1311
+
1312
+ ### v6.1.8
1313
+
1314
+ [Fixed]
1315
+
1316
+ - Incorrect masking of secrets in the final info log message for requests
1317
+
1306
1318
  ### v6.1.7
1307
1319
 
1308
1320
  [Fixed]
package/lib/logger.ts CHANGED
@@ -297,7 +297,13 @@ export class Logger {
297
297
  private async log(level: 'debug' | 'info' | 'warn' | 'error', message: string, obj?: Record<string, any>) {
298
298
  const time = new Date().toISOString()
299
299
  message = this.maskSecret(message)
300
- if (typeof obj === 'object' && !Array.isArray(obj)) obj = JSON.parse(this.maskSecret(JSON.stringify(obj)))
300
+
301
+ if (typeof obj === 'object' && !Array.isArray(obj)) {
302
+ obj = JSON.parse(JSON.stringify(obj, (_k, v) => {
303
+ if (typeof v === 'string') return this.maskSecret(v)
304
+ return v
305
+ }))
306
+ }
301
307
  const msgObj: Record<string, any> = {
302
308
  time,
303
309
  level,
@@ -57,8 +57,6 @@
57
57
  // Members members members
58
58
  // =====================================================================================================================
59
59
 
60
- import querystring from 'querystring'
61
-
62
60
  // start - mandatory plugin initialization
63
61
  import { ScimGateway, HelperRest } from 'scimgateway'
64
62
  const scimgateway = new ScimGateway()
@@ -143,6 +141,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
143
141
  let options: Record<string, any> = {}
144
142
  let isExpandManager = true
145
143
 
144
+ if (Object.hasOwn(getObj, 'value')) getObj.value = encodeURIComponent(getObj.value)
146
145
  if (!Object.hasOwn(getObj, 'count')) getObj.count = 200
147
146
  if (getObj.count > 500) getObj.count = 500 // Entra ID max 999
148
147
 
@@ -266,7 +265,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
266
265
  const id = res?.body?.id || userObj.userName
267
266
  await scimgateway.modifyUser(baseEntity, id, addonObj, ctx) // manager, proxyAddresses, servicePlan
268
267
  }
269
- return null
268
+ return res?.body
270
269
  } catch (err: any) {
271
270
  const newErr = new Error(`${action} error: ${err.message}`)
272
271
  if (err.message.includes('userPrincipalName already exists')) newErr.name += '#409' // customErrCode
@@ -402,6 +401,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
402
401
  totalResults: null,
403
402
  }
404
403
 
404
+ if (Object.hasOwn(getObj, 'value')) getObj.value = encodeURIComponent(getObj.value)
405
405
  if (attributes.length === 0) attributes = groupAttributes
406
406
  let includeMembers = false
407
407
  if (attributes.includes('members.value') || attributes.includes('members')) {
@@ -525,7 +525,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
525
525
  scimgateway.logDebug(baseEntity, `handling ${action} groupObj=${JSON.stringify(groupObj)} passThrough=${ctx ? 'true' : 'false'}`)
526
526
 
527
527
  const body: any = { displayName: groupObj.displayName }
528
- body.mailNickName = groupObj.displayName
528
+ body.mailNickName = groupObj.displayName?.replace(/[^a-zA-Z0-9]/g, '')
529
529
  body.mailEnabled = false
530
530
  body.securityEnabled = true
531
531
  const method = 'POST'
@@ -536,8 +536,8 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
536
536
  if (res && res.Resources && res.Resources.length > 0) {
537
537
  throw new Error(`group ${groupObj.displayName} already exist`)
538
538
  }
539
- await helper.doRequest(baseEntity, method, path, body, ctx)
540
- return null
539
+ const response = await helper.doRequest(baseEntity, method, path, body, ctx)
540
+ return response?.body
541
541
  } catch (err: any) {
542
542
  const newErr = new Error(`${action} error: ${err.message}`)
543
543
  if (err.message.includes('already exist')) newErr.name += '#409' // customErrCode
@@ -743,7 +743,7 @@ const getUser = async (baseEntity: string, uid: string, attributes: string[], ct
743
743
 
744
744
  const userPromise = (async () => {
745
745
  const method = 'GET'
746
- const path = `/users/${querystring.escape(uid)}?$expand=manager($select=userPrincipalName)`
746
+ const path = `/users/${uid}?$expand=manager($select=userPrincipalName)`
747
747
  const body = null
748
748
  const response = await helper.doRequest(baseEntity, method, path, body, ctx)
749
749
  const userObj = response.body
@@ -761,7 +761,7 @@ const getUser = async (baseEntity: string, uid: string, attributes: string[], ct
761
761
  const licensePromise = (async () => {
762
762
  if (!attributes.includes('servicePlans.value')) return null // licenses not requested
763
763
  const method = 'GET'
764
- const path = `/users/${querystring.escape(uid)}/licenseDetails`
764
+ const path = `/users/${uid}/licenseDetails`
765
765
  const body = null
766
766
  const retObj: Record<string, any> = { servicePlan: [] }
767
767
  try {
@@ -548,7 +548,7 @@ export class ScimGateway {
548
548
  status: ctx.response.status,
549
549
  method: ctx.request.method,
550
550
  url: ctx.request.url,
551
- requestBody: ctx.request.body,
551
+ requestBody: JSON.stringify(ctx.request.body),
552
552
  responseBody: outbound,
553
553
  }
554
554
  let msg = utils.statusText(logEvent.status)
@@ -1707,33 +1707,45 @@ export class ScimGateway {
1707
1707
  }
1708
1708
  }
1709
1709
  logger.debug(`${gwName} calling ${handle.createMethod}`, { baseEntity: ctx?.routeObj?.baseEntity })
1710
- const res = await (this as any)[handle.createMethod](baseEntity, scimdata, ctx.passThrough)
1711
- for (const key in res) { // merge any result e.g: {'id': 'xxxx'}
1712
- jsonBody[key] = res[key]
1713
- }
1710
+ const response = await (this as any)[handle.createMethod](baseEntity, scimdata, ctx.passThrough)
1714
1711
 
1715
- if (!jsonBody.id) { // retrieve all attributes including id
1716
- let res: any
1717
- let obj: any
1718
- try {
1719
- if (handle.createMethod === 'createUser') {
1720
- const attributes: string[] = []
1721
- if (jsonBody.userName) obj = { attribute: 'userName', operator: 'eq', value: jsonBody.userName }
1722
- else if (jsonBody.externalId) obj = { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }
1723
- res = await (this as any)[handle.getMethod](baseEntity, obj, attributes, ctx.passThrough)
1724
- } else if (handle.createMethod === 'createGroup') {
1725
- const attributes: string[] = []
1726
- if (jsonBody.externalId) obj = { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }
1727
- else if (jsonBody.displayName) obj = { attribute: 'displayName', operator: 'eq', value: jsonBody.displayName }
1712
+ // lookup user/group created, id should be included in response
1713
+ let res: any
1714
+ let obj: any
1715
+ try {
1716
+ if (handle.createMethod === 'createUser') {
1717
+ const attributes: string[] = []
1718
+ if (response?.id) obj = { attribute: 'id', operator: 'eq', value: response.id }
1719
+ else if (jsonBody.userName) obj = { attribute: 'userName', operator: 'eq', value: jsonBody.userName }
1720
+ else if (jsonBody.externalId) obj = { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }
1721
+ res = await (this as any)[handle.getMethod](baseEntity, obj, attributes, ctx.passThrough)
1722
+ } else if (handle.createMethod === 'createGroup') {
1723
+ const attributes: string[] = []
1724
+ if (response?.id) obj = { attribute: 'id', operator: 'eq', value: response.id }
1725
+ else if (jsonBody.displayName) obj = { attribute: 'displayName', operator: 'eq', value: jsonBody.displayName }
1726
+ else if (jsonBody.externalId) obj = { attribute: 'externalId', operator: 'eq', value: jsonBody.externalId }
1727
+ if (response?.id && response['@odata.context']?.includes('graph.microsoft.com')) {
1728
+ // Entra ID may experience some latency before a newly created group can be looked up
1729
+ let counter = 0
1730
+ const maxCounter = 20
1731
+ while (true) {
1732
+ counter++
1733
+ if (counter > maxCounter) break
1734
+ res = await (this as any)[handle.getMethod](baseEntity, obj, attributes, ctx.passThrough)
1735
+ if (res?.Resources && Array.isArray(res.Resources) && res.Resources.length === 1) break
1736
+ await new Promise(resolve => setTimeout(resolve, 1000))
1737
+ }
1738
+ } else {
1728
1739
  res = await (this as any)[handle.getMethod](baseEntity, obj, attributes, ctx.passThrough)
1729
1740
  }
1730
- } catch (err: any) {
1731
- logger.warn(`${gwName} ${handle.createMethod} succeeded, but corresponding ${handle.getMethod} ${obj?.value} failed with error: ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
1732
- }
1733
- if (res?.Resources && Array.isArray(res.Resources) && res.Resources.length === 1) {
1734
- if (res.Resources[0]?.id) jsonBody = res.Resources[0] // id found, using returned object
1735
1741
  }
1742
+ } catch (err: any) {
1743
+ logger.warn(`${gwName} ${handle.createMethod} succeeded, but corresponding ${handle.getMethod} ${obj?.value} failed with error: ${err.message}`, { baseEntity: ctx?.routeObj?.baseEntity })
1744
+ }
1745
+ if (res?.Resources && Array.isArray(res.Resources) && res.Resources.length === 1) {
1746
+ jsonBody = res.Resources[0]
1736
1747
  }
1748
+ delete jsonBody.password
1737
1749
 
1738
1750
  const eTag = utils.getEtag(jsonBody)
1739
1751
  if (addGrps.length > 0 && handle.createMethod === 'createUser') { // add group membership
@@ -2090,7 +2102,7 @@ export class ScimGateway {
2090
2102
  }
2091
2103
  }
2092
2104
 
2093
- const activeExists = Object.prototype.hasOwnProperty.call(obj, 'active')
2105
+ const activeExists = Object.hasOwn(obj, 'active')
2094
2106
  let objGroups: any
2095
2107
  if (obj.groups) {
2096
2108
  if (!this.config.scimgateway.scim.groupMemberOfUser) {
package/lib/utils-scim.ts CHANGED
@@ -329,7 +329,7 @@ export function convertedScim20(obj: any, multiValueTypes: string[]): any {
329
329
  scimdata.meta.attributes.push(`${key}:${_k}`)
330
330
  delete value[_k]
331
331
  } else if (typeof value[_k] === 'object') { // manager.value
332
- if (Object.prototype.hasOwnProperty.call(value[_k], 'value') && (value[_k].value === undefined || value[_k].value === null)) {
332
+ if (Object.hasOwn(value[_k], 'value') && (value[_k].value === undefined || value[_k].value === null)) {
333
333
  scimdata.meta.attributes.push(`${key}:${_k}.value`)
334
334
  delete value[_k].value
335
335
  }
package/lib/utils.ts CHANGED
@@ -170,7 +170,7 @@ export const JSONStringify = function (object: any) {
170
170
  }
171
171
 
172
172
  const objProp = function (obj: Record<string, any>, prop: string, val: any) { // return obj value based on json dot notation formatted prop
173
- if (Object.prototype.hasOwnProperty.call(obj, prop)) return obj[prop]
173
+ if (Object.hasOwn(obj, prop)) return obj[prop]
174
174
  const props = prop.split('.') // scimgateway.auth.basic[0].password
175
175
  const final = props.pop() as string
176
176
  for (let i = 0; i < props.length; i++) {
@@ -210,7 +210,7 @@ const _extendObj = (obj: Record<any, any>, src: Record<any, any>) => {
210
210
  for (let i = 0; i < src[key].length; i++) {
211
211
  const val = src[key][i]
212
212
  if (typeof val === 'object') {
213
- if (Object.prototype.hasOwnProperty.call(val, 'type')) {
213
+ if (Object.hasOwn(val, 'type')) {
214
214
  if (!obj[key]) obj[key] = [val]
215
215
  else {
216
216
  for (let j = 0; j < obj[key].length; j++) {
@@ -223,7 +223,7 @@ const _extendObj = (obj: Record<any, any>, src: Record<any, any>) => {
223
223
  }
224
224
  obj[key].push(val)
225
225
  }
226
- } else if (Object.prototype.hasOwnProperty.call(val, 'value')) {
226
+ } else if (Object.hasOwn(val, 'value')) {
227
227
  if (!obj[key]) obj[key] = [val]
228
228
  else {
229
229
  for (let j = 0; j < obj[key].length; j++) {
@@ -258,7 +258,7 @@ export const extendObjClear = (obj: Record<string, any>, src: Record<string, any
258
258
  Object.keys(src).forEach((key) => {
259
259
  if (src[key] === null) return
260
260
  if (typeof src[key] !== 'object') { // last key
261
- if (Object.prototype.hasOwnProperty.call(obj, key)) return
261
+ if (Object.hasOwn(obj, key)) return
262
262
  if (isSoftSync) obj[key] = src[key]
263
263
  else {
264
264
  switch (typeof src[key]) {
@@ -288,7 +288,7 @@ export const extendObjClear = (obj: Record<string, any>, src: Record<string, any
288
288
  if (typeof val !== 'object') {
289
289
  if (!obj[key].includes(val)) obj[key].push(val) // e.g. ["value1", "value2"]
290
290
  } else {
291
- if (Object.prototype.hasOwnProperty.call(val, 'type') && key !== 'members' && key !== 'groups') {
291
+ if (Object.hasOwn(val, 'type') && key !== 'members' && key !== 'groups') {
292
292
  if (obj[key].length < 1) {
293
293
  const v: any = copyObj(val)
294
294
  if (!isSoftSync) v.operation = 'delete'
@@ -301,7 +301,7 @@ export const extendObjClear = (obj: Record<string, any>, src: Record<string, any
301
301
  found = true
302
302
  for (const kv in val) {
303
303
  if (kv === 'type' || kv === 'value' || isSoftSync) continue // don't clear type/value
304
- if (Object.prototype.hasOwnProperty.call(el, kv)) continue
304
+ if (Object.hasOwn(el, kv)) continue
305
305
  switch (typeof val[kv]) {
306
306
  case 'string':
307
307
  el[kv] = ''
@@ -324,7 +324,7 @@ export const extendObjClear = (obj: Record<string, any>, src: Record<string, any
324
324
  obj[key].push(v)
325
325
  }
326
326
  }
327
- } else if (Object.prototype.hasOwnProperty.call(val, 'value')) { // no type
327
+ } else if (Object.hasOwn(val, 'value')) { // no type
328
328
  if (obj[key].length < 1) {
329
329
  const v: any = copyObj(val)
330
330
  if (!isSoftSync) v.operation = 'delete'
@@ -370,7 +370,7 @@ export const deltaObj = (obj: Record<string, any>, src: Record<string, any>) =>
370
370
  const el = arr[i]
371
371
  if (el.operation) continue // keep operation
372
372
  if (el.type) {
373
- if (Object.prototype.hasOwnProperty.call(el, 'value')) {
373
+ if (Object.hasOwn(el, 'value')) {
374
374
  const a = src[key].filter(o => o.type === el.type && o.value === el.value)
375
375
  if (a.length === 1) {
376
376
  arr.splice(i, 1)
@@ -388,7 +388,7 @@ export const deltaObj = (obj: Record<string, any>, src: Record<string, any>) =>
388
388
  i -= 1
389
389
  }
390
390
  }
391
- } else if (Object.prototype.hasOwnProperty.call(el, 'value')) {
391
+ } else if (Object.hasOwn(el, 'value')) {
392
392
  const a = src[key].filter(o => o.value === el.value)
393
393
  if (a.length === 1) {
394
394
  arr.splice(i, 1)
@@ -441,9 +441,9 @@ export const stripObj = (obj: Record<string, any>, attributes?: string, excluded
441
441
  const ret: Record<string, any> = {}
442
442
  for (let i = 0; i < arrAttr.length; i++) {
443
443
  const attr = arrAttr[i].split('.') // title / name.familyName / emails.value
444
- if (Object.prototype.hasOwnProperty.call(obj, attr[0])) {
444
+ if (Object.hasOwn(obj, attr[0])) {
445
445
  if (attr.length === 1) ret[attr[0]] = obj[attr[0]]
446
- else if (Object.prototype.hasOwnProperty.call(obj[attr[0]], attr[1])) { // name.familyName
446
+ else if (Object.hasOwn(obj[attr[0]], attr[1])) { // name.familyName
447
447
  if (!ret[attr[0]]) ret[attr[0]] = {}
448
448
  ret[attr[0]][attr[1]] = obj[attr[0]][attr[1]]
449
449
  } else if (Array.isArray(obj[attr[0]])) { // emails.value / phoneNumbers.type
@@ -452,7 +452,7 @@ export const stripObj = (obj: Record<string, any>, attributes?: string, excluded
452
452
  for (let j = 0; j < arr.length; j++) {
453
453
  if (typeof arr[j] !== 'object') {
454
454
  ret[attr[0]].push(arr[j])
455
- } else if (Object.prototype.hasOwnProperty.call(arr[j], attr[1])) {
455
+ } else if (Object.hasOwn(arr[j], attr[1])) {
456
456
  if (ret[attr[0]].length !== arr.length) { // initiate
457
457
  for (let i = 0; i < arr.length; i++) ret[attr[0]].push({}) // need arrCheckEmpty
458
458
  }
@@ -481,14 +481,14 @@ export const stripObj = (obj: Record<string, any>, attributes?: string, excluded
481
481
  const ret: any = copyObj(obj)
482
482
  for (let i = 0; i < arrAttr.length; i++) {
483
483
  const attr = arrAttr[i].split('.') // title / name.familyName / emails.value
484
- if (Object.prototype.hasOwnProperty.call(ret, attr[0])) {
484
+ if (Object.hasOwn(ret, attr[0])) {
485
485
  if (attr.length === 1) delete ret[attr[0]]
486
- else if (Object.prototype.hasOwnProperty.call(ret[attr[0]], attr[1])) delete ret[attr[0]][attr[1]] // name.familyName
486
+ else if (Object.hasOwn(ret[attr[0]], attr[1])) delete ret[attr[0]][attr[1]] // name.familyName
487
487
  else if (Array.isArray(ret[attr[0]])) { // emails.value / phoneNumbers.type
488
488
  const arr = ret[attr[0]]
489
489
  for (let j = 0; j < arr.length; j++) {
490
- if (Object.prototype.hasOwnProperty.call(arr[j], attr[1])) {
491
- const index = arr.findIndex((el: Record<string, any>) => ((Object.prototype.hasOwnProperty.call(el, attr[1]))))
490
+ if (Object.hasOwn(arr[j], attr[1])) {
491
+ const index = arr.findIndex((el: Record<string, any>) => ((Object.hasOwn(el, attr[1]))))
492
492
  if (index > -1) {
493
493
  delete arr[index][attr[1]]
494
494
  try {
@@ -516,7 +516,7 @@ export const sortByKey = (key: string, order: string = 'ascending') => {
516
516
  const val: any = [undefined, undefined]
517
517
  const arrIter = [a, b]
518
518
  const levels = key.split('.')
519
- if (!Object.prototype.hasOwnProperty.call(a, levels[0]) || !Object.prototype.hasOwnProperty.call(b, levels[0])) return 0
519
+ if (!Object.hasOwn(a, levels[0]) || !Object.hasOwn(b, levels[0])) return 0
520
520
  arrIter.forEach((el, index) => {
521
521
  let parent = el
522
522
  for (let i = 0; i < levels.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "6.1.7",
3
+ "version": "6.1.9",
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)",