scimgateway 6.1.9 → 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 +7 -0
- package/lib/helper-rest.ts +7 -7
- package/lib/plugin-entra-id.ts +68 -3
- package/lib/postinstall.ts +4 -2
- package/lib/scimgateway.ts +32 -18
- package/lib/utils-scim.ts +3 -3
- package/lib/utils.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1303,6 +1303,13 @@ 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
|
+
|
|
1306
1313
|
### v6.1.9
|
|
1307
1314
|
|
|
1308
1315
|
[Improved]
|
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
|
@@ -65,6 +65,8 @@ const config = scimgateway.getConfig()
|
|
|
65
65
|
scimgateway.authPassThroughAllowed = false
|
|
66
66
|
// end - mandatory plugin initialization
|
|
67
67
|
|
|
68
|
+
const newHelper = new HelperRest(scimgateway)
|
|
69
|
+
|
|
68
70
|
if (config.map) { // having licensDetails map here instead of config file
|
|
69
71
|
config.map.licenseDetails = {
|
|
70
72
|
servicePlanId: {
|
|
@@ -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
|
|
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
|
|
@@ -1978,7 +1978,7 @@ export class ScimGateway {
|
|
|
1978
1978
|
if (!this.config.scimgateway.scim.groupMemberOfUser) {
|
|
1979
1979
|
for (let i = 0; i < scimdata.groups.length; i++) {
|
|
1980
1980
|
if (!scimdata.groups[i].value) continue
|
|
1981
|
-
const obj: any =
|
|
1981
|
+
const obj: any = structuredClone(scimdata.groups[i])
|
|
1982
1982
|
obj.value = decodeURIComponent(obj.value)
|
|
1983
1983
|
groups.push(obj)
|
|
1984
1984
|
}
|
|
@@ -1991,7 +1991,7 @@ export class ScimGateway {
|
|
|
1991
1991
|
res = await replaceUsrGrp(ctx.routeObj.handle, baseEntity, id, scimdata, this.config.scimgateway.scim.usePutSoftSync, ctx.passThrough, undefined)
|
|
1992
1992
|
} else {
|
|
1993
1993
|
logger.debug(`${gwName} calling ${handle.modifyMethod}`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
1994
|
-
finalScimdata =
|
|
1994
|
+
finalScimdata = structuredClone(scimdata)
|
|
1995
1995
|
res = await (this as any)[handle.modifyMethod](baseEntity, id, scimdata, ctx.passThrough)
|
|
1996
1996
|
}
|
|
1997
1997
|
|
|
@@ -2106,7 +2106,7 @@ export class ScimGateway {
|
|
|
2106
2106
|
let objGroups: any
|
|
2107
2107
|
if (obj.groups) {
|
|
2108
2108
|
if (!this.config.scimgateway.scim.groupMemberOfUser) {
|
|
2109
|
-
objGroups =
|
|
2109
|
+
objGroups = structuredClone(obj.groups)
|
|
2110
2110
|
delete obj.groups
|
|
2111
2111
|
}
|
|
2112
2112
|
}
|
|
@@ -2282,7 +2282,7 @@ export class ScimGateway {
|
|
|
2282
2282
|
const postBulkHandler = async (ctx: Context) => {
|
|
2283
2283
|
const baseEntity = ctx.routeObj.baseEntity
|
|
2284
2284
|
logger.debug(`${gwName} [Bulk Operations]`, { baseEntity: ctx?.routeObj?.baseEntity })
|
|
2285
|
-
const bulkBody: SCIMBulkRequest =
|
|
2285
|
+
const bulkBody: SCIMBulkRequest = structuredClone(ctx.request.body)
|
|
2286
2286
|
try {
|
|
2287
2287
|
if (!bulkBody) throw new Error('missing body')
|
|
2288
2288
|
if (typeof bulkBody !== 'object') throw new Error('body is not JSON')
|
|
@@ -2758,9 +2758,11 @@ export class ScimGateway {
|
|
|
2758
2758
|
if (!res.Resources[i].id) continue
|
|
2759
2759
|
const el: any = {}
|
|
2760
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 }
|
|
2761
2763
|
if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
|
|
2762
|
-
if (isScimv2) el.type =
|
|
2763
|
-
else el.type = { value:
|
|
2764
|
+
if (isScimv2) el.type = type
|
|
2765
|
+
else el.type = { value: type }
|
|
2764
2766
|
groups.push(el) // { "value": "Admins", "display": "Admins", "type": "direct"}
|
|
2765
2767
|
}
|
|
2766
2768
|
nextStartIndex = utilsScim.getNextStartIndex(res.totalResults, startIndex, res.Resources.length)
|
|
@@ -3629,7 +3631,7 @@ export class ScimGateway {
|
|
|
3629
3631
|
logger.info(`${gwName} starting SCIM Stream subscribers...`)
|
|
3630
3632
|
const sub: any = new stream.Subscriber(this, funcHandler)
|
|
3631
3633
|
for (const baseEntity in this.config.scimgateway.stream.subscriber.entity) {
|
|
3632
|
-
const cfgSub: any =
|
|
3634
|
+
const cfgSub: any = structuredClone(this.config.scimgateway.stream.subscriber.entity[baseEntity])
|
|
3633
3635
|
cfgSub.baseUrls = this.config.scimgateway.stream.baseUrls
|
|
3634
3636
|
cfgSub.certificate = this.config.scimgateway.stream.certificate
|
|
3635
3637
|
cfgSub.usePutSoftSync = this.config.scimgateway.scim.usePutSoftSync
|
|
@@ -3643,7 +3645,7 @@ export class ScimGateway {
|
|
|
3643
3645
|
logger.info(`${gwName} starting SCIM Stream publishers...`)
|
|
3644
3646
|
const pub: any = new stream.Publisher(this)
|
|
3645
3647
|
for (const baseEntity in this.config.scimgateway.stream.publisher.entity) {
|
|
3646
|
-
const cfgPub: any =
|
|
3648
|
+
const cfgPub: any = structuredClone(this.config.scimgateway.stream.publisher.entity[baseEntity])
|
|
3647
3649
|
cfgPub.baseUrls = this.config.scimgateway.stream.baseUrls
|
|
3648
3650
|
cfgPub.certificate = this.config.scimgateway.stream.certificate
|
|
3649
3651
|
pub.add(baseEntity, cfgPub)
|
|
@@ -3795,6 +3797,7 @@ export class ScimGateway {
|
|
|
3795
3797
|
|
|
3796
3798
|
/**
|
|
3797
3799
|
* copyObj returns a copy of the object
|
|
3800
|
+
* Note: prefer using structuredClone(obj)
|
|
3798
3801
|
* @param obj object to be copied
|
|
3799
3802
|
* @returns copy of object
|
|
3800
3803
|
**/
|
|
@@ -3812,6 +3815,17 @@ export class ScimGateway {
|
|
|
3812
3815
|
return utils.extendObj(obj, src)
|
|
3813
3816
|
}
|
|
3814
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
|
+
|
|
3815
3829
|
/**
|
|
3816
3830
|
* Lock for mutual exclusion
|
|
3817
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)
|
package/lib/utils.ts
CHANGED
|
@@ -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
|
}
|
package/package.json
CHANGED