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.
@@ -6,7 +6,7 @@
6
6
  "scim": {
7
7
  "version": "2.0",
8
8
  "customSchema": null,
9
- "skipTypeConvert": false,
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' spesifies 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."
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": "Read-only attribute representing entitlements. 'entitlements.type' spesifies the entitlement category. For Entra ID licenses we have 'entitlements.type' = 'License' and corresponding 'entitlements.value' = License SKU ID (unique identifier) and 'entitlements.display' = User-friendly license name."
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": {
@@ -180,10 +180,7 @@
180
180
  },
181
181
  "userName": {
182
182
  "mapTo": "userName",
183
- "type": "string",
184
- "xvalueMap": {
185
- "bjensen": "Xbjensen"
186
- }
183
+ "type": "string"
187
184
  },
188
185
  "active": {
189
186
  "mapTo": "active",
@@ -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' // beta instead of 'v1.0' gives all user attributes when no $select
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
- urlObj = new URL(path)
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.path = `${urlObj.pathname}${urlObj.search}`
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
- path: urlObj.pathname + urlObj.search,
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
- const options = cli.options
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
- let url = `${options.protocol}//${options.host}${options.port ? ':' + options.port : ''}${options.path}`
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.protocol}//${options.host}${(options.port ? `:${options.port}` : '')}${options.path} Body = ${JSON.stringify(body)} Response = ${JSON.stringify(result)}`)
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/beta/users?$top=100&$skiptoken=xxx"}
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.substring(0, url.indexOf('$skiptoken') - 1))
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} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
825
- } else this.scimgateway.logError(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
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/beta]
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/beta]
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",