scimgateway 6.2.0 → 6.2.1
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 +8 -2
- package/config/plugin-generic.json +1 -4
- package/lib/helper-rest.ts +19 -42
- package/lib/plugin-generic.ts +20 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1305,16 +1305,22 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1305
1305
|
|
|
1306
1306
|
## Change log
|
|
1307
1307
|
|
|
1308
|
+
### v6.2.1
|
|
1309
|
+
|
|
1310
|
+
[Fixed]
|
|
1311
|
+
|
|
1312
|
+
- `HelperRest`: fixed some minor log cosmetics introduced in v6.2.0
|
|
1313
|
+
|
|
1308
1314
|
### v6.2.0
|
|
1309
1315
|
|
|
1310
1316
|
[Fixed]
|
|
1311
1317
|
|
|
1312
|
-
- `
|
|
1318
|
+
- `HelperRest`: failed on Bun v1.3.14 due to stricter compliance with Fetch standards.
|
|
1313
1319
|
|
|
1314
1320
|
[Improved]
|
|
1315
1321
|
|
|
1316
1322
|
- New `plugin-generic` replacing previous `plugin-scim`. This new plugin use the endpointMapper for flexible attribute mapping and also supports the new mapper option `valueMap` (e.g., group filtering and mapping). The default configuration uses one-to-one SCIM mapping, with plugin-loki as the target SCIM endpoint.
|
|
1317
|
-
- endpointMapper now supports the
|
|
1323
|
+
- endpointMapper now supports the `valueMap` option
|
|
1318
1324
|
|
|
1319
1325
|
Example configuration:
|
|
1320
1326
|
|
package/lib/helper-rest.ts
CHANGED
|
@@ -435,10 +435,9 @@ export class HelperRest {
|
|
|
435
435
|
private async getServiceClient(baseEntity: string, connectionObj: Record<string, any>, method: string, path: string, opt?: any, ctx?: any) {
|
|
436
436
|
const action = 'getServiceClient'
|
|
437
437
|
if (typeof connectionObj !== 'object' || connectionObj === null) connectionObj = {}
|
|
438
|
-
let urlObj: any
|
|
439
438
|
if (!path) path = ''
|
|
440
439
|
try {
|
|
441
|
-
|
|
440
|
+
new URL(path)
|
|
442
441
|
} catch (err) {
|
|
443
442
|
//
|
|
444
443
|
// path (no url) - default approach and client will be cached based on config
|
|
@@ -473,18 +472,12 @@ export class HelperRest {
|
|
|
473
472
|
const err = new Error(`missing connection configuration: baseUrls`)
|
|
474
473
|
throw err
|
|
475
474
|
}
|
|
476
|
-
urlObj = new URL(connectionObj.baseUrls[0])
|
|
477
475
|
const param: any = {
|
|
478
476
|
baseUrl: connectionObj.baseUrls[0],
|
|
479
477
|
options: {
|
|
480
|
-
json: true, // json-object response instead of string
|
|
481
478
|
headers: {
|
|
482
479
|
Accept: 'application/json',
|
|
483
480
|
},
|
|
484
|
-
host: urlObj.hostname,
|
|
485
|
-
port: urlObj.port, // null if https and 443 defined in url
|
|
486
|
-
protocol: urlObj.protocol, // http: or https:
|
|
487
|
-
// 'method' and 'path' added at the end
|
|
488
481
|
},
|
|
489
482
|
}
|
|
490
483
|
|
|
@@ -567,16 +560,9 @@ export class HelperRest {
|
|
|
567
560
|
}
|
|
568
561
|
const cli: any = structuredClone(this._serviceClient[baseEntity]) // client ready
|
|
569
562
|
|
|
570
|
-
// failover support
|
|
571
|
-
path = this._serviceClient[baseEntity].baseUrl + path
|
|
572
|
-
urlObj = new URL(path)
|
|
573
|
-
cli.options.host = urlObj.hostname
|
|
574
|
-
cli.options.port = urlObj.port
|
|
575
|
-
cli.options.protocol = urlObj.protocol
|
|
576
|
-
|
|
577
|
-
// adding none static
|
|
578
563
|
cli.options.method = method
|
|
579
|
-
cli.options.
|
|
564
|
+
cli.options.url = this._serviceClient[baseEntity].baseUrl + path // failover supported
|
|
565
|
+
|
|
580
566
|
if (opt) {
|
|
581
567
|
if (opt?.connection) delete opt.connection // only used for internal connection options
|
|
582
568
|
cli.options = utils.extendObj(cli.options, opt) // merge with argument options
|
|
@@ -589,15 +575,11 @@ export class HelperRest {
|
|
|
589
575
|
//
|
|
590
576
|
this.scimgateway.logDebug(baseEntity, `${action}: Using raw client`)
|
|
591
577
|
let options: any = {
|
|
592
|
-
json: true,
|
|
593
578
|
headers: {
|
|
594
579
|
Accept: 'application/json',
|
|
595
580
|
},
|
|
596
|
-
host: urlObj.hostname,
|
|
597
|
-
port: urlObj.port,
|
|
598
|
-
protocol: urlObj.protocol,
|
|
599
581
|
method: method,
|
|
600
|
-
|
|
582
|
+
url: path,
|
|
601
583
|
}
|
|
602
584
|
|
|
603
585
|
// proxy
|
|
@@ -646,12 +628,13 @@ export class HelperRest {
|
|
|
646
628
|
**/
|
|
647
629
|
private async doRequestHandler(baseEntity: string, method: string, path: string, body?: any, ctx?: any, opt?: any, retryCount?: number): Promise<any> {
|
|
648
630
|
const connectionObj = this.config_entity[baseEntity]?.connection ?? {}
|
|
631
|
+
let options: Record<any, any> = {}
|
|
649
632
|
let retryAfter = 0
|
|
650
633
|
try {
|
|
651
634
|
const controller = new AbortController()
|
|
652
635
|
const signal = controller.signal
|
|
653
636
|
const cli = await this.getServiceClient(baseEntity, connectionObj, method, path, opt, ctx)
|
|
654
|
-
|
|
637
|
+
options = cli.options
|
|
655
638
|
const timeout = setTimeout(() => controller.abort(), options.abortTimeout ? options.abortTimeout * 1000 : this.idleTimeout * 1000) // 120 seconds default abort timeout
|
|
656
639
|
options.signal = signal
|
|
657
640
|
|
|
@@ -676,39 +659,33 @@ export class HelperRest {
|
|
|
676
659
|
options.body = dataString
|
|
677
660
|
} else if (options.headers) delete options.headers['Content-Type']
|
|
678
661
|
|
|
679
|
-
|
|
680
|
-
if (this._serviceClient[baseEntity]?.nextLink[url]) {
|
|
662
|
+
if (this._serviceClient[baseEntity]?.nextLink[options.url]) {
|
|
681
663
|
if (ctx?.paging?.startIndex && ctx.paging.startIndex > 1) {
|
|
682
|
-
if (ctx.paging.startIndex === this._serviceClient[baseEntity]?.nextLink[url].startIndex) {
|
|
683
|
-
url = this._serviceClient[baseEntity]?.nextLink[url]['@odata.nextLink']
|
|
664
|
+
if (ctx.paging.startIndex === this._serviceClient[baseEntity]?.nextLink[options.url].startIndex) {
|
|
665
|
+
options.url = this._serviceClient[baseEntity]?.nextLink[options.url]['@odata.nextLink']
|
|
684
666
|
} else {
|
|
685
667
|
if (!ctx) ctx = {}
|
|
686
668
|
if (!ctx.paging) ctx.paging = {}
|
|
687
|
-
if (this._serviceClient[baseEntity]?.nextLink[url].totalResults
|
|
688
|
-
&& ctx.paging.startIndex > this._serviceClient[baseEntity]?.nextLink[url].totalResults) {
|
|
689
|
-
ctx.paging.totalResults = this._serviceClient[baseEntity]?.nextLink[url].totalResults
|
|
669
|
+
if (this._serviceClient[baseEntity]?.nextLink[options.url].totalResults
|
|
670
|
+
&& ctx.paging.startIndex > this._serviceClient[baseEntity]?.nextLink[options.url].totalResults) {
|
|
671
|
+
ctx.paging.totalResults = this._serviceClient[baseEntity]?.nextLink[options.url].totalResults
|
|
690
672
|
return { body: { value: [] } }
|
|
691
673
|
} else {
|
|
692
674
|
// reset the paging cursor - none expected startIndex sequence, using default none paged url
|
|
693
675
|
ctx.paging.startIndex = 1 // caller should check and return this new startIndex in final response
|
|
694
|
-
delete this._serviceClient[baseEntity].nextLink[url]
|
|
676
|
+
delete this._serviceClient[baseEntity].nextLink[options.url]
|
|
695
677
|
}
|
|
696
678
|
}
|
|
697
679
|
}
|
|
698
680
|
} else {
|
|
699
|
-
if (ctx?.paging?.startIndex > 1 && !this._serviceClient[baseEntity]?.nextLink[url]) { // no previous paging and invalid startIndex
|
|
681
|
+
if (ctx?.paging?.startIndex > 1 && !this._serviceClient[baseEntity]?.nextLink[options.url]) { // no previous paging and invalid startIndex
|
|
700
682
|
ctx.paging.totalResults = ctx.paging.startIndex - 1
|
|
701
683
|
return { body: { value: [] } }
|
|
702
684
|
}
|
|
703
685
|
}
|
|
704
686
|
|
|
705
|
-
// Bun v1.3.14 became stricter and more aligned with standards.
|
|
706
|
-
delete options.host
|
|
707
|
-
delete options.port
|
|
708
|
-
delete options.protocol
|
|
709
|
-
|
|
710
687
|
// execute request
|
|
711
|
-
const f = await fetch(url, options)
|
|
688
|
+
const f = await fetch(options.url, options)
|
|
712
689
|
if (!f.status) throw new Error('Response missing status code')
|
|
713
690
|
|
|
714
691
|
const result: any = {
|
|
@@ -733,7 +710,7 @@ export class HelperRest {
|
|
|
733
710
|
}
|
|
734
711
|
throw new Error(JSON.stringify(result))
|
|
735
712
|
}
|
|
736
|
-
this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${options.
|
|
713
|
+
this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${options.url} Body = ${JSON.stringify(body)} Response = ${JSON.stringify(result)}`)
|
|
737
714
|
|
|
738
715
|
// OData paging logic
|
|
739
716
|
// client prerequisite for enabling doRequest() OData paging support (see plugin-entra-id):
|
|
@@ -766,7 +743,7 @@ export class HelperRest {
|
|
|
766
743
|
ctx.paging.totalResults = totalResults
|
|
767
744
|
}
|
|
768
745
|
} else { // no more paging
|
|
769
|
-
const linkBase = decodeURIComponent(url
|
|
746
|
+
const linkBase = decodeURIComponent(options.url?.substring(0, options.url?.indexOf('$skiptoken') - 1))
|
|
770
747
|
if (ctx?.paging?.startIndex && ctx.paging.startIndex > 1 && this._serviceClient[baseEntity]?.nextLink[linkBase]) {
|
|
771
748
|
if (!this._serviceClient[baseEntity]?.nextLink[linkBase].isCount) { // final no count page
|
|
772
749
|
const itemsPerPage = result.body.value.length
|
|
@@ -821,8 +798,8 @@ export class HelperRest {
|
|
|
821
798
|
}
|
|
822
799
|
} else {
|
|
823
800
|
if (statusCode === 404) { // not logged as error e.g. getUser-manager
|
|
824
|
-
this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${
|
|
825
|
-
} else this.scimgateway.logError(baseEntity, `doRequest ${method} ${
|
|
801
|
+
this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${options.url} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
|
|
802
|
+
} else this.scimgateway.logError(baseEntity, `doRequest ${method} ${options.url} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
|
|
826
803
|
if (statusCode === 401) delete this._serviceClient[baseEntity]
|
|
827
804
|
throw err
|
|
828
805
|
}
|
package/lib/plugin-generic.ts
CHANGED
|
@@ -52,6 +52,14 @@ const config = scimgateway.getConfig()
|
|
|
52
52
|
scimgateway.authPassThroughAllowed = false
|
|
53
53
|
// end - mandatory plugin initialization
|
|
54
54
|
|
|
55
|
+
const isAllowlistingUser = config.map?.user
|
|
56
|
+
? Object.values(config.map.user).some((item: any) => typeof item?.valueMap === 'object' && Object.keys(item.valueMap).length > 0)
|
|
57
|
+
: false
|
|
58
|
+
|
|
59
|
+
const isAllowlistingGroup = config.map?.group
|
|
60
|
+
? Object.values(config.map.group).some((item: any) => typeof item.valueMap === 'object' && Object.keys(item.valueMap).length > 0)
|
|
61
|
+
: false
|
|
62
|
+
|
|
55
63
|
// =================================================
|
|
56
64
|
// getUsers
|
|
57
65
|
// =================================================
|
|
@@ -114,16 +122,13 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
114
122
|
totalResults: null,
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
|
|
118
|
-
? Object.values(config.map.user).some((item: any) => typeof item?.valueMap === 'object' && Object.keys(item.valueMap).length > 0)
|
|
119
|
-
: false
|
|
120
|
-
let currentStartIndex = isAllowlisting ? 1 : targetStartIndex
|
|
125
|
+
let currentStartIndex = isAllowlistingUser ? 1 : targetStartIndex
|
|
121
126
|
let allValidResources: any[] = []
|
|
122
127
|
let totalSkipped = 0
|
|
123
128
|
let targetTotalResults: number | null = null
|
|
124
129
|
let iteration = 0
|
|
125
130
|
const maxIterations = 5 // Safety limit for look-ahead fetching
|
|
126
|
-
const resourcesNeeded =
|
|
131
|
+
const resourcesNeeded = isAllowlistingUser ? targetStartIndex + targetCount - 1 : targetCount
|
|
127
132
|
|
|
128
133
|
try {
|
|
129
134
|
while (allValidResources.length < resourcesNeeded && iteration < maxIterations) {
|
|
@@ -166,8 +171,8 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
166
171
|
|
|
167
172
|
if (targetTotalResults === null) {
|
|
168
173
|
// Target endpoint returned full list
|
|
169
|
-
ret.totalResults =
|
|
170
|
-
ret.Resources =
|
|
174
|
+
ret.totalResults = isAllowlistingUser ? allValidResources.length : targetStartIndex - 1 + allValidResources.length
|
|
175
|
+
ret.Resources = isAllowlistingUser ? allValidResources.slice(targetStartIndex - 1, targetStartIndex - 1 + targetCount) : allValidResources.slice(0, targetCount)
|
|
171
176
|
return ret
|
|
172
177
|
}
|
|
173
178
|
|
|
@@ -185,8 +190,8 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
185
190
|
}
|
|
186
191
|
if (ctx?.paging && Object.hasOwn(ctx.paging, 'totalResults')) targetTotalResults = ctx.paging.totalResults
|
|
187
192
|
|
|
188
|
-
ret.totalResults = (targetTotalResults !== null && targetTotalResults > totalSkipped) ? targetTotalResults - totalSkipped : (
|
|
189
|
-
ret.Resources =
|
|
193
|
+
ret.totalResults = (targetTotalResults !== null && targetTotalResults > totalSkipped) ? targetTotalResults - totalSkipped : (isAllowlistingUser ? allValidResources.length : targetStartIndex - 1 + allValidResources.length)
|
|
194
|
+
ret.Resources = isAllowlistingUser ? allValidResources.slice(targetStartIndex - 1, targetStartIndex - 1 + targetCount) : allValidResources.slice(0, targetCount)
|
|
190
195
|
if (!ret.startIndex) ret.startIndex = targetStartIndex
|
|
191
196
|
|
|
192
197
|
return ret
|
|
@@ -345,16 +350,13 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
345
350
|
}
|
|
346
351
|
*/
|
|
347
352
|
|
|
348
|
-
|
|
349
|
-
? Object.values(config.map.group).some((item: any) => typeof item.valueMap === 'object' && Object.keys(item.valueMap).length > 0)
|
|
350
|
-
: false
|
|
351
|
-
let currentStartIndex = isAllowlisting ? 1 : targetStartIndex
|
|
353
|
+
let currentStartIndex = isAllowlistingGroup ? 1 : targetStartIndex
|
|
352
354
|
let allValidResources: any[] = []
|
|
353
355
|
let totalSkipped = 0
|
|
354
356
|
let targetTotalResults: number | null = null
|
|
355
357
|
let iteration = 0
|
|
356
358
|
const maxIterations = 5 // Safety limit for look-ahead fetching
|
|
357
|
-
const resourcesNeeded =
|
|
359
|
+
const resourcesNeeded = isAllowlistingGroup ? targetStartIndex + targetCount - 1 : targetCount
|
|
358
360
|
|
|
359
361
|
try {
|
|
360
362
|
while (allValidResources.length < resourcesNeeded && iteration < maxIterations) {
|
|
@@ -399,8 +401,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
399
401
|
|
|
400
402
|
if (targetTotalResults === null) {
|
|
401
403
|
// Target endpoint returned full list
|
|
402
|
-
ret.totalResults =
|
|
403
|
-
ret.Resources =
|
|
404
|
+
ret.totalResults = isAllowlistingGroup ? allValidResources.length : targetStartIndex - 1 + allValidResources.length
|
|
405
|
+
ret.Resources = isAllowlistingGroup ? allValidResources.slice(targetStartIndex - 1, targetStartIndex - 1 + targetCount) : allValidResources.slice(0, targetCount)
|
|
404
406
|
return ret
|
|
405
407
|
}
|
|
406
408
|
|
|
@@ -418,8 +420,8 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
418
420
|
}
|
|
419
421
|
if (ctx?.paging && Object.hasOwn(ctx.paging, 'totalResults')) targetTotalResults = ctx.paging.totalResults
|
|
420
422
|
|
|
421
|
-
ret.totalResults = (targetTotalResults !== null && targetTotalResults > totalSkipped) ? targetTotalResults - totalSkipped : (
|
|
422
|
-
ret.Resources =
|
|
423
|
+
ret.totalResults = (targetTotalResults !== null && targetTotalResults > totalSkipped) ? targetTotalResults - totalSkipped : (isAllowlistingGroup ? allValidResources.length : targetStartIndex - 1 + allValidResources.length)
|
|
424
|
+
ret.Resources = isAllowlistingGroup ? allValidResources.slice(targetStartIndex - 1, targetStartIndex - 1 + targetCount) : allValidResources.slice(0, targetCount)
|
|
423
425
|
if (!ret.startIndex) ret.startIndex = targetStartIndex
|
|
424
426
|
|
|
425
427
|
return ret
|
package/package.json
CHANGED