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 +12 -0
- package/lib/logger.ts +7 -1
- package/lib/plugin-entra-id.ts +8 -8
- package/lib/scimgateway.ts +36 -24
- package/lib/utils-scim.ts +1 -1
- package/lib/utils.ts +17 -17
- package/package.json +1 -1
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
|
-
|
|
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,
|
package/lib/plugin-entra-id.ts
CHANGED
|
@@ -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
|
|
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
|
|
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/${
|
|
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/${
|
|
764
|
+
const path = `/users/${uid}/licenseDetails`
|
|
765
765
|
const body = null
|
|
766
766
|
const retObj: Record<string, any> = { servicePlan: [] }
|
|
767
767
|
try {
|
package/lib/scimgateway.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
444
|
+
if (Object.hasOwn(obj, attr[0])) {
|
|
445
445
|
if (attr.length === 1) ret[attr[0]] = obj[attr[0]]
|
|
446
|
-
else if (Object.
|
|
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.
|
|
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.
|
|
484
|
+
if (Object.hasOwn(ret, attr[0])) {
|
|
485
485
|
if (attr.length === 1) delete ret[attr[0]]
|
|
486
|
-
else if (Object.
|
|
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.
|
|
491
|
-
const index = arr.findIndex((el: Record<string, any>) => ((Object.
|
|
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.
|
|
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