scimgateway 6.2.0 → 6.2.2
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 +1117 -3440
- package/config/plugin-entra-id.json +3 -3
- package/config/plugin-generic.json +1 -4
- package/lib/helper-rest.ts +23 -46
- package/lib/plugin-entra-id.ts +587 -187
- package/lib/plugin-generic.ts +31 -18
- package/lib/plugin-ldap.ts +15 -2
- package/lib/plugin-loki.ts +11 -0
- package/lib/plugin-mongodb.ts +11 -0
- package/lib/plugin-mssql.ts +11 -0
- package/lib/plugin-saphana.ts +11 -0
- package/lib/plugin-soap.ts +11 -0
- package/lib/scimgateway.ts +55 -37
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"scim": {
|
|
7
7
|
"version": "2.0",
|
|
8
8
|
"customSchema": null,
|
|
9
|
-
"skipTypeConvert":
|
|
9
|
+
"skipTypeConvert": true,
|
|
10
10
|
"groupMemberOfUser": false,
|
|
11
11
|
"usePutSoftSync": false
|
|
12
12
|
},
|
|
@@ -198,14 +198,14 @@
|
|
|
198
198
|
"mapTo": "roles",
|
|
199
199
|
"type": "complexArray",
|
|
200
200
|
"x-agent-schema": {
|
|
201
|
-
"description": "Attribute representing Entra ID roles. 'roles.type'
|
|
201
|
+
"description": "Attribute representing Entra ID roles. 'roles.type' specifies the role category, 'Permanent' or 'Eligible'. 'roles.value' = The unique identifier of the role and 'roles.display' = Entra ID user-friendly rolename. When adding or modifying user roles, if 'roles.type' is not specified, it defaults to 'Eligible' if the tenant uses PIM; otherwise, it defaults to 'Permanent'. The agent should omit 'roles.type' unless explicitly specified by the user. When deleting a role, 'roles.type' must be included."
|
|
202
202
|
}
|
|
203
203
|
},
|
|
204
204
|
"entitlements": {
|
|
205
205
|
"mapTo": "entitlements",
|
|
206
206
|
"type": "complexArray",
|
|
207
207
|
"x-agent-schema": {
|
|
208
|
-
"description": "
|
|
208
|
+
"description": "Attribute representing entitlements. 'entitlements.type' specifies the entitlement category: 'License' (read-only) or 'AccessPackage' (read-write). 'entitlements.value' = License SKU ID or AccessPackage ID (unique identifier). 'entitlements.display' = User-friendly name."
|
|
209
209
|
}
|
|
210
210
|
},
|
|
211
211
|
"userType": {
|
package/lib/helper-rest.ts
CHANGED
|
@@ -26,7 +26,7 @@ export class HelperRest {
|
|
|
26
26
|
private config_entity: any
|
|
27
27
|
private scimgateway: any
|
|
28
28
|
private idleTimeout: number
|
|
29
|
-
private graphUrl = 'https://graph.microsoft.com/beta' //
|
|
29
|
+
private graphUrl = 'https://graph.microsoft.com/beta' // using 'beta' which returns all user attributes when no $select and supports IGA Access Packages assignments
|
|
30
30
|
private googleUrl = 'https://www.googleapis.com'
|
|
31
31
|
|
|
32
32
|
constructor(scimgateway: any, optionalEntities?: Record<string, any>) {
|
|
@@ -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):
|
|
@@ -741,7 +718,7 @@ export class HelperRest {
|
|
|
741
718
|
// if (!ctx) ctx = { paging }
|
|
742
719
|
// else ctx.paging = paging
|
|
743
720
|
if (result.body && typeof result.body === 'object') {
|
|
744
|
-
if (result.body['@odata.nextLink']) { // {"@odata.nextLink": "https://graph.microsoft.com/
|
|
721
|
+
if (result.body['@odata.nextLink']) { // {"@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$top=100&$skiptoken=xxx"}
|
|
745
722
|
if (!ctx) ctx = {}
|
|
746
723
|
if (!ctx.paging) ctx.paging = {}
|
|
747
724
|
const nextLinkBase = decodeURIComponent(result.body['@odata.nextLink'].substring(0, result.body['@odata.nextLink'].indexOf('$skiptoken') - 1))
|
|
@@ -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
|
}
|
|
@@ -888,7 +865,7 @@ export class HelperRest {
|
|
|
888
865
|
* {
|
|
889
866
|
* "type": "oauth",
|
|
890
867
|
* "options": {
|
|
891
|
-
* "azureTenantId": "<Entra ID azureTenantId", // Entra ID authentication - if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/
|
|
868
|
+
* "azureTenantId": "<Entra ID azureTenantId", // Entra ID authentication - if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/v1.0]
|
|
892
869
|
* "tokenUrl": "<tokenUrl>", // must be set if not using azureTenantId
|
|
893
870
|
* "clientId": "<clientId>",
|
|
894
871
|
* "clientSecret": "<clientSecret>"
|
|
@@ -947,7 +924,7 @@ export class HelperRest {
|
|
|
947
924
|
* {
|
|
948
925
|
* "type": "oauthJwtBearer",
|
|
949
926
|
* "options": {
|
|
950
|
-
* "azureTenantId": "<Entra ID azureTenantId", // Entra ID authentication, if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/
|
|
927
|
+
* "azureTenantId": "<Entra ID azureTenantId", // Entra ID authentication, if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/v1.0]
|
|
951
928
|
* "clientId": "<clientId>",
|
|
952
929
|
* "tls": { // files located in ./config/certs
|
|
953
930
|
* "key": "key.pem",
|