scimgateway 6.1.8 → 6.1.10
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 +13 -0
- package/lib/helper-rest.ts +7 -7
- package/lib/plugin-entra-id.ts +76 -11
- package/lib/postinstall.ts +4 -2
- package/lib/scimgateway.ts +67 -41
- package/lib/utils-scim.ts +4 -4
- package/lib/utils.ts +19 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1303,6 +1303,19 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1303
1303
|
|
|
1304
1304
|
## Change log
|
|
1305
1305
|
|
|
1306
|
+
### v6.1.10
|
|
1307
|
+
|
|
1308
|
+
[Fixed]
|
|
1309
|
+
|
|
1310
|
+
- plugin-entra-id: user group membership now includes nested (transitive) groups (`direct` and `indirect`)
|
|
1311
|
+
- Docker example files `config/docker/.dockerignore` and `docker-compose-mssql.yml` were missing
|
|
1312
|
+
|
|
1313
|
+
### v6.1.9
|
|
1314
|
+
|
|
1315
|
+
[Improved]
|
|
1316
|
+
|
|
1317
|
+
- Some improvements to createUser/createGroup regarding the response object, which should contain the newly generated ID
|
|
1318
|
+
|
|
1306
1319
|
### v6.1.8
|
|
1307
1320
|
|
|
1308
1321
|
[Fixed]
|
package/lib/helper-rest.ts
CHANGED
|
@@ -34,8 +34,8 @@ export class HelperRest {
|
|
|
34
34
|
this.scimgateway = scimgateway
|
|
35
35
|
this.idleTimeout = (scimgateway as any)?.config?.scimgateway.idleTimeout || 120
|
|
36
36
|
this.idleTimeout = this.idleTimeout - 1
|
|
37
|
-
if (optionalEntities && optionalEntities.entity) this.config_entity =
|
|
38
|
-
else this.config_entity =
|
|
37
|
+
if (optionalEntities && optionalEntities.entity) this.config_entity = structuredClone(optionalEntities.entity) ?? {}
|
|
38
|
+
else this.config_entity = structuredClone(scimgateway.getConfig())?.entity ?? {}
|
|
39
39
|
|
|
40
40
|
for (const baseEntity in this.config_entity) {
|
|
41
41
|
const connectionObj = this.config_entity[baseEntity]?.connection
|
|
@@ -387,7 +387,7 @@ export class HelperRest {
|
|
|
387
387
|
const method = 'POST'
|
|
388
388
|
let connOpt: any = {}
|
|
389
389
|
if (connectionObj.options && typeof connectionObj.options === 'object') {
|
|
390
|
-
connOpt =
|
|
390
|
+
connOpt = structuredClone(connectionObj.options)
|
|
391
391
|
}
|
|
392
392
|
if (!connOpt.headers) connOpt.headers = {}
|
|
393
393
|
connOpt.headers['Content-Type'] = 'application/x-www-form-urlencoded' // body must be query string formatted (no JSON)
|
|
@@ -494,7 +494,7 @@ export class HelperRest {
|
|
|
494
494
|
let orgConnection: any
|
|
495
495
|
if (opt?.connection) { // allow overriding/extending configuration connection by caller argument opt.connection
|
|
496
496
|
let org = connectionObj
|
|
497
|
-
orgConnection =
|
|
497
|
+
orgConnection = structuredClone(org)
|
|
498
498
|
if (!org) org = {}
|
|
499
499
|
org = utils.extendObj(org, opt.connection)
|
|
500
500
|
}
|
|
@@ -533,7 +533,7 @@ export class HelperRest {
|
|
|
533
533
|
}
|
|
534
534
|
|
|
535
535
|
if (connectionObj.options) { // http connect options
|
|
536
|
-
const connOpt: any =
|
|
536
|
+
const connOpt: any = structuredClone(connectionObj.options)
|
|
537
537
|
try {
|
|
538
538
|
// using fs.readFileSync().toString() instead of Bun.file().text() for nodejs compability
|
|
539
539
|
if (connOpt?.tls?.key) connOpt.tls.key = fs.readFileSync(connOpt.tls.key).toString()
|
|
@@ -565,7 +565,7 @@ export class HelperRest {
|
|
|
565
565
|
if (ctx?.headers?.get) { // Auth PassThrough using ctx header
|
|
566
566
|
this._serviceClient[baseEntity].options.headers['Authorization'] = ctx.headers.get('authorization')
|
|
567
567
|
}
|
|
568
|
-
const cli: any =
|
|
568
|
+
const cli: any = structuredClone(this._serviceClient[baseEntity]) // client ready
|
|
569
569
|
|
|
570
570
|
// failover support
|
|
571
571
|
path = this._serviceClient[baseEntity].baseUrl + path
|
|
@@ -611,7 +611,7 @@ export class HelperRest {
|
|
|
611
611
|
|
|
612
612
|
// merge any argument options - basic auth header is supported through {auth:{type:"basic",options:{username:"username",password:"password"}}}
|
|
613
613
|
if (opt) {
|
|
614
|
-
const o: any =
|
|
614
|
+
const o: any = structuredClone(opt)
|
|
615
615
|
if (o?.auth?.type === 'basic') {
|
|
616
616
|
options.headers['Authorization'] = 'Basic ' + Buffer.from(`${o.auth?.options?.username}:${o.auth?.options?.password}`).toString('base64')
|
|
617
617
|
delete o.auth
|
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()
|
|
@@ -67,6 +65,8 @@ const config = scimgateway.getConfig()
|
|
|
67
65
|
scimgateway.authPassThroughAllowed = false
|
|
68
66
|
// end - mandatory plugin initialization
|
|
69
67
|
|
|
68
|
+
const newHelper = new HelperRest(scimgateway)
|
|
69
|
+
|
|
70
70
|
if (config.map) { // having licensDetails map here instead of config file
|
|
71
71
|
config.map.licenseDetails = {
|
|
72
72
|
servicePlanId: {
|
|
@@ -143,6 +143,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
143
143
|
let options: Record<string, any> = {}
|
|
144
144
|
let isExpandManager = true
|
|
145
145
|
|
|
146
|
+
if (Object.hasOwn(getObj, 'value')) getObj.value = encodeURIComponent(getObj.value)
|
|
146
147
|
if (!Object.hasOwn(getObj, 'count')) getObj.count = 200
|
|
147
148
|
if (getObj.count > 500) getObj.count = 500 // Entra ID max 999
|
|
148
149
|
|
|
@@ -266,7 +267,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
|
|
|
266
267
|
const id = res?.body?.id || userObj.userName
|
|
267
268
|
await scimgateway.modifyUser(baseEntity, id, addonObj, ctx) // manager, proxyAddresses, servicePlan
|
|
268
269
|
}
|
|
269
|
-
return
|
|
270
|
+
return res?.body
|
|
270
271
|
} catch (err: any) {
|
|
271
272
|
const newErr = new Error(`${action} error: ${err.message}`)
|
|
272
273
|
if (err.message.includes('userPrincipalName already exists')) newErr.name += '#409' // customErrCode
|
|
@@ -402,6 +403,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
402
403
|
totalResults: null,
|
|
403
404
|
}
|
|
404
405
|
|
|
406
|
+
if (Object.hasOwn(getObj, 'value')) getObj.value = encodeURIComponent(getObj.value)
|
|
405
407
|
if (attributes.length === 0) attributes = groupAttributes
|
|
406
408
|
let includeMembers = false
|
|
407
409
|
if (attributes.includes('members.value') || attributes.includes('members')) {
|
|
@@ -413,6 +415,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
413
415
|
const body = null
|
|
414
416
|
let path
|
|
415
417
|
let options: Record<string, any> = {}
|
|
418
|
+
let isUserMemberOf = getObj?.operator === 'eq' && getObj?.attribute === 'members.value'
|
|
416
419
|
|
|
417
420
|
if (!Object.hasOwn(getObj, 'count')) getObj.count = 500
|
|
418
421
|
if (getObj.count > 500) getObj.count = 500 // Entra ID max 999
|
|
@@ -428,10 +431,10 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
428
431
|
if (includeMembers) path = `/groups?$filter=${getObj.attribute} eq '${getObj.value}'&$select=${attrs.join()}&$expand=members($select=id,displayName)`
|
|
429
432
|
else path = `/groups?$filter=${getObj.attribute} eq '${getObj.value}'&$select=${attrs.join()}`
|
|
430
433
|
}
|
|
431
|
-
} else if (
|
|
434
|
+
} else if (isUserMemberOf) {
|
|
432
435
|
// mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
|
|
433
436
|
// Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
|
|
434
|
-
path = `/users/${getObj.value}/
|
|
437
|
+
path = `/users/${getObj.value}/transitiveMemberOf/microsoft.graph.group?$top=${getObj.count}&$count=true&select=id,displayName`
|
|
435
438
|
} else {
|
|
436
439
|
// optional - simpel filtering
|
|
437
440
|
if (getObj.attribute) {
|
|
@@ -471,8 +474,69 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
471
474
|
if (!ctx) ctx = { paging }
|
|
472
475
|
else ctx.paging = paging
|
|
473
476
|
|
|
477
|
+
const newCtx = { ...ctx }
|
|
478
|
+
newCtx.paging = { startIndex: 1 }
|
|
479
|
+
|
|
474
480
|
try {
|
|
475
|
-
let response
|
|
481
|
+
let response: any
|
|
482
|
+
let responseMemberOf: any
|
|
483
|
+
if (!isUserMemberOf) response = await helper.doRequest(baseEntity, method, path, body, ctx, options)
|
|
484
|
+
else {
|
|
485
|
+
// request both the default transitiveMemberOf (includes nested groups) and memberOf because we want to distinguish SCIM type=direct/indirect
|
|
486
|
+
const pathMemberOf = `/users/${getObj.value}/memberOf/microsoft.graph.group?$top=${getObj.count}&$count=true&select=id,displayName`
|
|
487
|
+
const allErrors: string[] = []
|
|
488
|
+
const results = await Promise.allSettled([
|
|
489
|
+
helper.doRequest(baseEntity, method, path, body, ctx, options),
|
|
490
|
+
newHelper.doRequest(baseEntity, method, pathMemberOf, body, newCtx, options), // using newHelper to avoid shared internal helperRest paging
|
|
491
|
+
])
|
|
492
|
+
const errors = results
|
|
493
|
+
.filter(r => r.status === 'rejected')
|
|
494
|
+
.map(r => (r as PromiseRejectedResult).reason.message)
|
|
495
|
+
.filter(msg => !msg.includes('already exist'))
|
|
496
|
+
allErrors.push(...errors)
|
|
497
|
+
|
|
498
|
+
if (allErrors.length > 0) {
|
|
499
|
+
throw new Error(allErrors.join(', '))
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
response = (results[0] as PromiseFulfilledResult<any>).value // includes all groups (also nested)
|
|
503
|
+
responseMemberOf = (results[1] as PromiseFulfilledResult<any>).value // do not include nested groups
|
|
504
|
+
|
|
505
|
+
let nextStartIndex = scimgateway.getNextStartIndex(responseMemberOf.body.value.length * 2, newCtx.paging.startIndex, responseMemberOf.body.value.length)
|
|
506
|
+
if (nextStartIndex > newCtx.paging.startIndex && responseMemberOf && responseMemberOf.body.value && Array.isArray(responseMemberOf.body.value)) {
|
|
507
|
+
// use paging to ensure responseMemberOf is complete
|
|
508
|
+
let totalResults = responseMemberOf.body.value.length
|
|
509
|
+
let startIndex = 1
|
|
510
|
+
let res: any
|
|
511
|
+
do {
|
|
512
|
+
try {
|
|
513
|
+
startIndex = nextStartIndex
|
|
514
|
+
newCtx.paging.startIndex = startIndex
|
|
515
|
+
res = await newHelper.doRequest(baseEntity, method, pathMemberOf, body, newCtx, options)
|
|
516
|
+
} catch (err) { void 0 }
|
|
517
|
+
if (res?.body && res.body.value && Array.isArray(res.body.value) && res.body.value.length > 0) {
|
|
518
|
+
const count = res.body.value.length
|
|
519
|
+
totalResults += count
|
|
520
|
+
nextStartIndex = scimgateway.getNextStartIndex(totalResults + count, startIndex, count)
|
|
521
|
+
for (let i = 0; i < res.body.value.length; i++) {
|
|
522
|
+
if (!res.body.value[i].id) continue
|
|
523
|
+
responseMemberOf.body.value.push(res.body.value[i])
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} while (nextStartIndex > startIndex)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (response.body && response.body.value && Array.isArray(response.body.value)) {
|
|
530
|
+
const directIds = new Set()
|
|
531
|
+
if (responseMemberOf.body && responseMemberOf.body.value && Array.isArray(responseMemberOf.body.value)) {
|
|
532
|
+
responseMemberOf.body.value.forEach((el: any) => directIds.add(el.id))
|
|
533
|
+
}
|
|
534
|
+
response.body.value.forEach((el: any) => {
|
|
535
|
+
if (directIds.has(el.id)) el.type = 'direct'
|
|
536
|
+
else el.type = 'indirect'
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
}
|
|
476
540
|
if (!response.body) {
|
|
477
541
|
throw new Error(`invalid response: ${JSON.stringify(response)}`)
|
|
478
542
|
}
|
|
@@ -494,6 +558,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
494
558
|
} else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') { // Not using expand-members. Only includes current user as member, but should have requested all...
|
|
495
559
|
members = [{
|
|
496
560
|
value: getObj.value,
|
|
561
|
+
type: response.body.value[i].type || 'direct',
|
|
497
562
|
}]
|
|
498
563
|
}
|
|
499
564
|
|
|
@@ -525,7 +590,7 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
|
|
|
525
590
|
scimgateway.logDebug(baseEntity, `handling ${action} groupObj=${JSON.stringify(groupObj)} passThrough=${ctx ? 'true' : 'false'}`)
|
|
526
591
|
|
|
527
592
|
const body: any = { displayName: groupObj.displayName }
|
|
528
|
-
body.mailNickName = groupObj.displayName
|
|
593
|
+
body.mailNickName = groupObj.displayName?.replace(/[^a-zA-Z0-9]/g, '')
|
|
529
594
|
body.mailEnabled = false
|
|
530
595
|
body.securityEnabled = true
|
|
531
596
|
const method = 'POST'
|
|
@@ -536,8 +601,8 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
|
|
|
536
601
|
if (res && res.Resources && res.Resources.length > 0) {
|
|
537
602
|
throw new Error(`group ${groupObj.displayName} already exist`)
|
|
538
603
|
}
|
|
539
|
-
await helper.doRequest(baseEntity, method, path, body, ctx)
|
|
540
|
-
return
|
|
604
|
+
const response = await helper.doRequest(baseEntity, method, path, body, ctx)
|
|
605
|
+
return response?.body
|
|
541
606
|
} catch (err: any) {
|
|
542
607
|
const newErr = new Error(`${action} error: ${err.message}`)
|
|
543
608
|
if (err.message.includes('already exist')) newErr.name += '#409' // customErrCode
|
|
@@ -743,7 +808,7 @@ const getUser = async (baseEntity: string, uid: string, attributes: string[], ct
|
|
|
743
808
|
|
|
744
809
|
const userPromise = (async () => {
|
|
745
810
|
const method = 'GET'
|
|
746
|
-
const path = `/users/${
|
|
811
|
+
const path = `/users/${uid}?$expand=manager($select=userPrincipalName)`
|
|
747
812
|
const body = null
|
|
748
813
|
const response = await helper.doRequest(baseEntity, method, path, body, ctx)
|
|
749
814
|
const userObj = response.body
|
|
@@ -761,7 +826,7 @@ const getUser = async (baseEntity: string, uid: string, attributes: string[], ct
|
|
|
761
826
|
const licensePromise = (async () => {
|
|
762
827
|
if (!attributes.includes('servicePlans.value')) return null // licenses not requested
|
|
763
828
|
const method = 'GET'
|
|
764
|
-
const path = `/users/${
|
|
829
|
+
const path = `/users/${uid}/licenseDetails`
|
|
765
830
|
const body = null
|
|
766
831
|
const retObj: Record<string, any> = { servicePlan: [] }
|
|
767
832
|
try {
|
package/lib/postinstall.ts
CHANGED
|
@@ -53,10 +53,12 @@ if (!fsExistsSync('../../config/wsdls/UserService.wsdl')) {
|
|
|
53
53
|
fs.writeFileSync('../../config/wsdls/UserService.wsdl', fs.readFileSync('./config/wsdls/UserService.wsdl'))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
fs.writeFileSync('../../config/docker
|
|
57
|
-
fs.writeFileSync('../../config/docker/Dockerfile', fs.readFileSync('./config/docker/Dockerfile'))
|
|
56
|
+
fs.writeFileSync('../../config/docker/.dockerignore', fs.readFileSync('./config/docker/.dockerignore'))
|
|
58
57
|
fs.writeFileSync('../../config/docker/DataDockerfile', fs.readFileSync('./config/docker/DataDockerfile'))
|
|
59
58
|
fs.writeFileSync('../../config/docker/docker-compose-debug.yml', fs.readFileSync('./config/docker/docker-compose-debug.yml'))
|
|
59
|
+
fs.writeFileSync('../../config/docker/docker-compose-mssql.yml', fs.readFileSync('./config/docker/docker-compose-mssql.yml'))
|
|
60
|
+
fs.writeFileSync('../../config/docker/docker-compose.yml', fs.readFileSync('./config/docker/docker-compose.yml'))
|
|
61
|
+
fs.writeFileSync('../../config/docker/Dockerfile', fs.readFileSync('./config/docker/Dockerfile'))
|
|
60
62
|
|
|
61
63
|
fs.writeFileSync('../../LICENSE', fs.readFileSync('./LICENSE'))
|
|
62
64
|
if (!fsExistsSync('../../index.ts')) fs.writeFileSync('../../index.ts', fs.readFileSync('./index.ts'))
|
package/lib/scimgateway.ts
CHANGED
|
@@ -757,7 +757,7 @@ export class ScimGateway {
|
|
|
757
757
|
}
|
|
758
758
|
if (utils.getEncrypted(authToken, arr[i].clientSecret) === arr[i].clientSecret) {
|
|
759
759
|
arr[i].isTokenRequested = true // flagged as true to not allow repeated resolvements because token will also be cleared when expired
|
|
760
|
-
const baseEntities =
|
|
760
|
+
const baseEntities = structuredClone(arr[i].baseEntities)
|
|
761
761
|
let expires
|
|
762
762
|
let readOnly = false
|
|
763
763
|
if (arr[i].readOnly && arr[i].readOnly === true) readOnly = true
|
|
@@ -857,7 +857,7 @@ export class ScimGateway {
|
|
|
857
857
|
}
|
|
858
858
|
|
|
859
859
|
const getHandlerSchemas = async (ctx: Context) => {
|
|
860
|
-
let tx =
|
|
860
|
+
let tx = structuredClone(this.scimDef.Schemas)
|
|
861
861
|
if (this.config.endpoint?.map) {
|
|
862
862
|
// endpointMapper being used
|
|
863
863
|
// Schemas returned should instead reflect what is defined in the plugin config file
|
|
@@ -891,7 +891,7 @@ export class ScimGateway {
|
|
|
891
891
|
uniqueness: (item.mapTo === 'userName') ? 'server' : 'none',
|
|
892
892
|
}
|
|
893
893
|
if (item['x-agent-schema']) {
|
|
894
|
-
const agentSchema =
|
|
894
|
+
const agentSchema = structuredClone(item['x-agent-schema'])
|
|
895
895
|
if (agentSchema.description) {
|
|
896
896
|
attr.description = agentSchema.description
|
|
897
897
|
delete agentSchema.description
|
|
@@ -909,7 +909,7 @@ export class ScimGateway {
|
|
|
909
909
|
if (names.length > 1) {
|
|
910
910
|
const userNameFound = attr.name.includes('userName')
|
|
911
911
|
for (let i = 0; i < names.length; i++) {
|
|
912
|
-
let attrCopy =
|
|
912
|
+
let attrCopy = structuredClone(attr)
|
|
913
913
|
const name = names[i].trim()
|
|
914
914
|
attrCopy.name = name
|
|
915
915
|
if (name === 'id') continue
|
|
@@ -959,7 +959,7 @@ export class ScimGateway {
|
|
|
959
959
|
delete subAttr.uniqueness
|
|
960
960
|
}
|
|
961
961
|
if (item['x-agent-schema']) {
|
|
962
|
-
const hints =
|
|
962
|
+
const hints = structuredClone(item['x-agent-schema'])
|
|
963
963
|
if (hints.description) {
|
|
964
964
|
subAttr.description = hints.description
|
|
965
965
|
delete hints.description
|
|
@@ -1147,7 +1147,7 @@ export class ScimGateway {
|
|
|
1147
1147
|
ctx.request.body = body // now json - ensure final info log will be masked
|
|
1148
1148
|
jsonBody = body
|
|
1149
1149
|
}
|
|
1150
|
-
jsonBody =
|
|
1150
|
+
jsonBody = structuredClone(jsonBody) // no changes to original
|
|
1151
1151
|
} catch (err: any) {
|
|
1152
1152
|
logger.error(`${gwName} [oauth] token request error: ${err.message}`)
|
|
1153
1153
|
ctx.response.status = 401
|
|
@@ -1184,7 +1184,7 @@ export class ScimGateway {
|
|
|
1184
1184
|
if (!arr[i].baseEntities.includes(baseEntity)) continue
|
|
1185
1185
|
}
|
|
1186
1186
|
token = utils.getEncrypted(jsonBody.client_secret, jsonBody.client_secret)
|
|
1187
|
-
baseEntities =
|
|
1187
|
+
baseEntities = structuredClone(arr[i].baseEntities)
|
|
1188
1188
|
if (arr[i].readOnly && arr[i].readOnly === true) readOnly = true
|
|
1189
1189
|
if (arr[i].expires_in && !isNaN(arr[i].expires_in)) expires = arr[i].expires_in
|
|
1190
1190
|
else expires = oAuthTokenExpire
|
|
@@ -1291,7 +1291,7 @@ export class ScimGateway {
|
|
|
1291
1291
|
logger.debug(`${gwName} [Get ${handle.description}] ${getObj.attribute}=${getObj.value}`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
1292
1292
|
|
|
1293
1293
|
try {
|
|
1294
|
-
const ob =
|
|
1294
|
+
const ob = structuredClone(getObj)
|
|
1295
1295
|
const attributes: string[] = ctx.query.attributes ? ctx.query.attributes.split(',').map((item: string) => item.trim()) : []
|
|
1296
1296
|
if (attributes.length > 0 && !attributes.includes('id')) attributes.push('id')
|
|
1297
1297
|
logger.debug(`${gwName} calling ${handle.getMethod}`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
@@ -1523,7 +1523,7 @@ export class ScimGateway {
|
|
|
1523
1523
|
getObj.count = ctx.query.count ? parseInt(ctx.query.count, 10) : 200 // defaults to 200 (plugin may override)
|
|
1524
1524
|
|
|
1525
1525
|
let res: any
|
|
1526
|
-
const obj: any =
|
|
1526
|
+
const obj: any = structuredClone(getObj)
|
|
1527
1527
|
const attributes: string[] = ctx.query.attributes ? ctx.query.attributes.split(',').map((item: string) => item.trim()) : []
|
|
1528
1528
|
if (attributes.length > 0 && !attributes.includes('id')) attributes.push('id') // id is mandatory
|
|
1529
1529
|
|
|
@@ -1659,7 +1659,7 @@ export class ScimGateway {
|
|
|
1659
1659
|
try {
|
|
1660
1660
|
if (!jsonBody) throw new Error('missing body')
|
|
1661
1661
|
if (typeof jsonBody !== 'object' || jsonBody === null) throw new Error('body is not JSON')
|
|
1662
|
-
jsonBody =
|
|
1662
|
+
jsonBody = structuredClone(jsonBody) // no changes to original
|
|
1663
1663
|
} catch (err: any) {
|
|
1664
1664
|
const [e, statusCode] = utilsScim.jsonErr(this.config.scimgateway.scim.version, pluginName, 500, err)
|
|
1665
1665
|
ctx.response.status = statusCode
|
|
@@ -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
|
|
@@ -1966,7 +1978,7 @@ export class ScimGateway {
|
|
|
1966
1978
|
if (!this.config.scimgateway.scim.groupMemberOfUser) {
|
|
1967
1979
|
for (let i = 0; i < scimdata.groups.length; i++) {
|
|
1968
1980
|
if (!scimdata.groups[i].value) continue
|
|
1969
|
-
const obj: any =
|
|
1981
|
+
const obj: any = structuredClone(scimdata.groups[i])
|
|
1970
1982
|
obj.value = decodeURIComponent(obj.value)
|
|
1971
1983
|
groups.push(obj)
|
|
1972
1984
|
}
|
|
@@ -1979,7 +1991,7 @@ export class ScimGateway {
|
|
|
1979
1991
|
res = await replaceUsrGrp(ctx.routeObj.handle, baseEntity, id, scimdata, this.config.scimgateway.scim.usePutSoftSync, ctx.passThrough, undefined)
|
|
1980
1992
|
} else {
|
|
1981
1993
|
logger.debug(`${gwName} calling ${handle.modifyMethod}`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
1982
|
-
finalScimdata =
|
|
1994
|
+
finalScimdata = structuredClone(scimdata)
|
|
1983
1995
|
res = await (this as any)[handle.modifyMethod](baseEntity, id, scimdata, ctx.passThrough)
|
|
1984
1996
|
}
|
|
1985
1997
|
|
|
@@ -2090,11 +2102,11 @@ 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) {
|
|
2097
|
-
objGroups =
|
|
2109
|
+
objGroups = structuredClone(obj.groups)
|
|
2098
2110
|
delete obj.groups
|
|
2099
2111
|
}
|
|
2100
2112
|
}
|
|
@@ -2270,7 +2282,7 @@ export class ScimGateway {
|
|
|
2270
2282
|
const postBulkHandler = async (ctx: Context) => {
|
|
2271
2283
|
const baseEntity = ctx.routeObj.baseEntity
|
|
2272
2284
|
logger.debug(`${gwName} [Bulk Operations]`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
2273
|
-
const bulkBody: SCIMBulkRequest =
|
|
2285
|
+
const bulkBody: SCIMBulkRequest = structuredClone(ctx.request.body)
|
|
2274
2286
|
try {
|
|
2275
2287
|
if (!bulkBody) throw new Error('missing body')
|
|
2276
2288
|
if (typeof bulkBody !== 'object') throw new Error('body is not JSON')
|
|
@@ -2746,9 +2758,11 @@ export class ScimGateway {
|
|
|
2746
2758
|
if (!res.Resources[i].id) continue
|
|
2747
2759
|
const el: any = {}
|
|
2748
2760
|
el.value = res.Resources[i].id
|
|
2761
|
+
const type = (Array.isArray(res.Resources[i].members) && res.Resources[i].members[0]?.type === 'indirect') ? 'indirect' : 'direct'
|
|
2762
|
+
el.type = { value: type }
|
|
2749
2763
|
if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
|
|
2750
|
-
if (isScimv2) el.type =
|
|
2751
|
-
else el.type = { value:
|
|
2764
|
+
if (isScimv2) el.type = type
|
|
2765
|
+
else el.type = { value: type }
|
|
2752
2766
|
groups.push(el) // { "value": "Admins", "display": "Admins", "type": "direct"}
|
|
2753
2767
|
}
|
|
2754
2768
|
nextStartIndex = utilsScim.getNextStartIndex(res.totalResults, startIndex, res.Resources.length)
|
|
@@ -3617,7 +3631,7 @@ export class ScimGateway {
|
|
|
3617
3631
|
logger.info(`${gwName} starting SCIM Stream subscribers...`)
|
|
3618
3632
|
const sub: any = new stream.Subscriber(this, funcHandler)
|
|
3619
3633
|
for (const baseEntity in this.config.scimgateway.stream.subscriber.entity) {
|
|
3620
|
-
const cfgSub: any =
|
|
3634
|
+
const cfgSub: any = structuredClone(this.config.scimgateway.stream.subscriber.entity[baseEntity])
|
|
3621
3635
|
cfgSub.baseUrls = this.config.scimgateway.stream.baseUrls
|
|
3622
3636
|
cfgSub.certificate = this.config.scimgateway.stream.certificate
|
|
3623
3637
|
cfgSub.usePutSoftSync = this.config.scimgateway.scim.usePutSoftSync
|
|
@@ -3631,7 +3645,7 @@ export class ScimGateway {
|
|
|
3631
3645
|
logger.info(`${gwName} starting SCIM Stream publishers...`)
|
|
3632
3646
|
const pub: any = new stream.Publisher(this)
|
|
3633
3647
|
for (const baseEntity in this.config.scimgateway.stream.publisher.entity) {
|
|
3634
|
-
const cfgPub: any =
|
|
3648
|
+
const cfgPub: any = structuredClone(this.config.scimgateway.stream.publisher.entity[baseEntity])
|
|
3635
3649
|
cfgPub.baseUrls = this.config.scimgateway.stream.baseUrls
|
|
3636
3650
|
cfgPub.certificate = this.config.scimgateway.stream.certificate
|
|
3637
3651
|
pub.add(baseEntity, cfgPub)
|
|
@@ -3783,6 +3797,7 @@ export class ScimGateway {
|
|
|
3783
3797
|
|
|
3784
3798
|
/**
|
|
3785
3799
|
* copyObj returns a copy of the object
|
|
3800
|
+
* Note: prefer using structuredClone(obj)
|
|
3786
3801
|
* @param obj object to be copied
|
|
3787
3802
|
* @returns copy of object
|
|
3788
3803
|
**/
|
|
@@ -3800,6 +3815,17 @@ export class ScimGateway {
|
|
|
3800
3815
|
return utils.extendObj(obj, src)
|
|
3801
3816
|
}
|
|
3802
3817
|
|
|
3818
|
+
/**
|
|
3819
|
+
* getNextStartIndex returns the next SCIM pagination startIndex based on current result set
|
|
3820
|
+
* @param totalResults current totalResults
|
|
3821
|
+
* @param startIndex: current startIndex
|
|
3822
|
+
* @param count: current count
|
|
3823
|
+
* @returns next startIndex
|
|
3824
|
+
**/
|
|
3825
|
+
getNextStartIndex(totalResults: number, startIndex: number, itemsPerPage: number): number {
|
|
3826
|
+
return utilsScim.getNextStartIndex(totalResults, startIndex, itemsPerPage)
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3803
3829
|
/**
|
|
3804
3830
|
* Lock for mutual exclusion
|
|
3805
3831
|
* - const lock = new scimgateway.Lock()
|
package/lib/utils-scim.ts
CHANGED
|
@@ -26,7 +26,7 @@ type SCIMBulkOperation = {
|
|
|
26
26
|
*/
|
|
27
27
|
export function convertedScim(obj: any, multiValueTypes: string[]): any {
|
|
28
28
|
let err: any = null
|
|
29
|
-
const scimdata: any =
|
|
29
|
+
const scimdata: any = structuredClone(obj)
|
|
30
30
|
if (scimdata.schemas) delete scimdata.schemas
|
|
31
31
|
const newMulti: Record<string, any> = {}
|
|
32
32
|
if (!multiValueTypes) multiValueTypes = []
|
|
@@ -156,7 +156,7 @@ export function convertedScim(obj: any, multiValueTypes: string[]): any {
|
|
|
156
156
|
export function convertedScim20(obj: any, multiValueTypes: string[]): any {
|
|
157
157
|
if (!obj.Operations || !Array.isArray(obj.Operations)) return {}
|
|
158
158
|
let scimdata: { [key: string]: any } = { meta: { attributes: [] } } // meta is used for deleted attributes
|
|
159
|
-
const o: any =
|
|
159
|
+
const o: any = structuredClone(obj)
|
|
160
160
|
const arrPrimaryDone: any = []
|
|
161
161
|
const primaryOrgType: any = {}
|
|
162
162
|
|
|
@@ -199,7 +199,7 @@ export function convertedScim20(obj: any, multiValueTypes: string[]): any {
|
|
|
199
199
|
typeElement = arrMatches[3] // streetAddress
|
|
200
200
|
|
|
201
201
|
if (type === 'primary' && !arrPrimaryDone.includes(arrMatches[1])) { // make sure primary is included
|
|
202
|
-
const pObj: any =
|
|
202
|
+
const pObj: any = structuredClone(element)
|
|
203
203
|
pObj.path = pObj.path.substring(0, pObj.path.lastIndexOf('.')) + '.primary'
|
|
204
204
|
pObj.value = primaryValue
|
|
205
205
|
o.Operations.push(pObj)
|
|
@@ -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++) {
|
|
@@ -192,7 +192,8 @@ export const copyObj = (o: any): any => { // deep copy/clone faster than JSON.pa
|
|
|
192
192
|
if (typeof v === 'object' && v !== null) {
|
|
193
193
|
const objProp = Object.getPrototypeOf(v) // e.g. HttpsProxyAgent {}
|
|
194
194
|
if (objProp !== null && objProp !== Object.getPrototypeOf({}) && objProp !== Object.getPrototypeOf([])) {
|
|
195
|
-
|
|
195
|
+
const proto = Object.getPrototypeOf(v)
|
|
196
|
+
output[key] = Object.assign(Object.create(proto), v) // e.g. { HttpsProxyAgent {...} }
|
|
196
197
|
} else output[key] = copyObj(v)
|
|
197
198
|
} else output[key] = v
|
|
198
199
|
}
|
|
@@ -210,7 +211,7 @@ const _extendObj = (obj: Record<any, any>, src: Record<any, any>) => {
|
|
|
210
211
|
for (let i = 0; i < src[key].length; i++) {
|
|
211
212
|
const val = src[key][i]
|
|
212
213
|
if (typeof val === 'object') {
|
|
213
|
-
if (Object.
|
|
214
|
+
if (Object.hasOwn(val, 'type')) {
|
|
214
215
|
if (!obj[key]) obj[key] = [val]
|
|
215
216
|
else {
|
|
216
217
|
for (let j = 0; j < obj[key].length; j++) {
|
|
@@ -223,7 +224,7 @@ const _extendObj = (obj: Record<any, any>, src: Record<any, any>) => {
|
|
|
223
224
|
}
|
|
224
225
|
obj[key].push(val)
|
|
225
226
|
}
|
|
226
|
-
} else if (Object.
|
|
227
|
+
} else if (Object.hasOwn(val, 'value')) {
|
|
227
228
|
if (!obj[key]) obj[key] = [val]
|
|
228
229
|
else {
|
|
229
230
|
for (let j = 0; j < obj[key].length; j++) {
|
|
@@ -258,7 +259,7 @@ export const extendObjClear = (obj: Record<string, any>, src: Record<string, any
|
|
|
258
259
|
Object.keys(src).forEach((key) => {
|
|
259
260
|
if (src[key] === null) return
|
|
260
261
|
if (typeof src[key] !== 'object') { // last key
|
|
261
|
-
if (Object.
|
|
262
|
+
if (Object.hasOwn(obj, key)) return
|
|
262
263
|
if (isSoftSync) obj[key] = src[key]
|
|
263
264
|
else {
|
|
264
265
|
switch (typeof src[key]) {
|
|
@@ -288,7 +289,7 @@ export const extendObjClear = (obj: Record<string, any>, src: Record<string, any
|
|
|
288
289
|
if (typeof val !== 'object') {
|
|
289
290
|
if (!obj[key].includes(val)) obj[key].push(val) // e.g. ["value1", "value2"]
|
|
290
291
|
} else {
|
|
291
|
-
if (Object.
|
|
292
|
+
if (Object.hasOwn(val, 'type') && key !== 'members' && key !== 'groups') {
|
|
292
293
|
if (obj[key].length < 1) {
|
|
293
294
|
const v: any = copyObj(val)
|
|
294
295
|
if (!isSoftSync) v.operation = 'delete'
|
|
@@ -301,7 +302,7 @@ export const extendObjClear = (obj: Record<string, any>, src: Record<string, any
|
|
|
301
302
|
found = true
|
|
302
303
|
for (const kv in val) {
|
|
303
304
|
if (kv === 'type' || kv === 'value' || isSoftSync) continue // don't clear type/value
|
|
304
|
-
if (Object.
|
|
305
|
+
if (Object.hasOwn(el, kv)) continue
|
|
305
306
|
switch (typeof val[kv]) {
|
|
306
307
|
case 'string':
|
|
307
308
|
el[kv] = ''
|
|
@@ -324,7 +325,7 @@ export const extendObjClear = (obj: Record<string, any>, src: Record<string, any
|
|
|
324
325
|
obj[key].push(v)
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
|
-
} else if (Object.
|
|
328
|
+
} else if (Object.hasOwn(val, 'value')) { // no type
|
|
328
329
|
if (obj[key].length < 1) {
|
|
329
330
|
const v: any = copyObj(val)
|
|
330
331
|
if (!isSoftSync) v.operation = 'delete'
|
|
@@ -370,7 +371,7 @@ export const deltaObj = (obj: Record<string, any>, src: Record<string, any>) =>
|
|
|
370
371
|
const el = arr[i]
|
|
371
372
|
if (el.operation) continue // keep operation
|
|
372
373
|
if (el.type) {
|
|
373
|
-
if (Object.
|
|
374
|
+
if (Object.hasOwn(el, 'value')) {
|
|
374
375
|
const a = src[key].filter(o => o.type === el.type && o.value === el.value)
|
|
375
376
|
if (a.length === 1) {
|
|
376
377
|
arr.splice(i, 1)
|
|
@@ -388,7 +389,7 @@ export const deltaObj = (obj: Record<string, any>, src: Record<string, any>) =>
|
|
|
388
389
|
i -= 1
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
|
-
} else if (Object.
|
|
392
|
+
} else if (Object.hasOwn(el, 'value')) {
|
|
392
393
|
const a = src[key].filter(o => o.value === el.value)
|
|
393
394
|
if (a.length === 1) {
|
|
394
395
|
arr.splice(i, 1)
|
|
@@ -441,9 +442,9 @@ export const stripObj = (obj: Record<string, any>, attributes?: string, excluded
|
|
|
441
442
|
const ret: Record<string, any> = {}
|
|
442
443
|
for (let i = 0; i < arrAttr.length; i++) {
|
|
443
444
|
const attr = arrAttr[i].split('.') // title / name.familyName / emails.value
|
|
444
|
-
if (Object.
|
|
445
|
+
if (Object.hasOwn(obj, attr[0])) {
|
|
445
446
|
if (attr.length === 1) ret[attr[0]] = obj[attr[0]]
|
|
446
|
-
else if (Object.
|
|
447
|
+
else if (Object.hasOwn(obj[attr[0]], attr[1])) { // name.familyName
|
|
447
448
|
if (!ret[attr[0]]) ret[attr[0]] = {}
|
|
448
449
|
ret[attr[0]][attr[1]] = obj[attr[0]][attr[1]]
|
|
449
450
|
} else if (Array.isArray(obj[attr[0]])) { // emails.value / phoneNumbers.type
|
|
@@ -452,7 +453,7 @@ export const stripObj = (obj: Record<string, any>, attributes?: string, excluded
|
|
|
452
453
|
for (let j = 0; j < arr.length; j++) {
|
|
453
454
|
if (typeof arr[j] !== 'object') {
|
|
454
455
|
ret[attr[0]].push(arr[j])
|
|
455
|
-
} else if (Object.
|
|
456
|
+
} else if (Object.hasOwn(arr[j], attr[1])) {
|
|
456
457
|
if (ret[attr[0]].length !== arr.length) { // initiate
|
|
457
458
|
for (let i = 0; i < arr.length; i++) ret[attr[0]].push({}) // need arrCheckEmpty
|
|
458
459
|
}
|
|
@@ -481,14 +482,14 @@ export const stripObj = (obj: Record<string, any>, attributes?: string, excluded
|
|
|
481
482
|
const ret: any = copyObj(obj)
|
|
482
483
|
for (let i = 0; i < arrAttr.length; i++) {
|
|
483
484
|
const attr = arrAttr[i].split('.') // title / name.familyName / emails.value
|
|
484
|
-
if (Object.
|
|
485
|
+
if (Object.hasOwn(ret, attr[0])) {
|
|
485
486
|
if (attr.length === 1) delete ret[attr[0]]
|
|
486
|
-
else if (Object.
|
|
487
|
+
else if (Object.hasOwn(ret[attr[0]], attr[1])) delete ret[attr[0]][attr[1]] // name.familyName
|
|
487
488
|
else if (Array.isArray(ret[attr[0]])) { // emails.value / phoneNumbers.type
|
|
488
489
|
const arr = ret[attr[0]]
|
|
489
490
|
for (let j = 0; j < arr.length; j++) {
|
|
490
|
-
if (Object.
|
|
491
|
-
const index = arr.findIndex((el: Record<string, any>) => ((Object.
|
|
491
|
+
if (Object.hasOwn(arr[j], attr[1])) {
|
|
492
|
+
const index = arr.findIndex((el: Record<string, any>) => ((Object.hasOwn(el, attr[1]))))
|
|
492
493
|
if (index > -1) {
|
|
493
494
|
delete arr[index][attr[1]]
|
|
494
495
|
try {
|
|
@@ -516,7 +517,7 @@ export const sortByKey = (key: string, order: string = 'ascending') => {
|
|
|
516
517
|
const val: any = [undefined, undefined]
|
|
517
518
|
const arrIter = [a, b]
|
|
518
519
|
const levels = key.split('.')
|
|
519
|
-
if (!Object.
|
|
520
|
+
if (!Object.hasOwn(a, levels[0]) || !Object.hasOwn(b, levels[0])) return 0
|
|
520
521
|
arrIter.forEach((el, index) => {
|
|
521
522
|
let parent = el
|
|
522
523
|
for (let i = 0; i < levels.length; i++) {
|
package/package.json
CHANGED