scimgateway 6.2.5 → 6.2.7
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/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/bun.lock +95 -91
- package/config/plugin-entra-id.json +616 -616
- package/lib/azure-license-mapping.json +17 -0
- package/lib/helper-rest.ts +102 -51
- package/lib/plugin-entra-id.ts +43 -40
- package/lib/saml/LICENSE +21 -0
- package/lib/saml/README.md +66 -0
- package/lib/saml/index.js +2 -0
- package/lib/saml/saml11.js +197 -0
- package/lib/saml/saml20.js +205 -0
- package/lib/saml/utils.js +94 -0
- package/lib/saml/xml/encrypt.js +56 -0
- package/lib/saml/xml/sign.js +104 -0
- package/lib/samlAssertion.ts +1 -2
- package/lib/scimgateway.ts +2 -0
- package/package.json +13 -13
|
@@ -27,16 +27,24 @@
|
|
|
27
27
|
"Microsoft_365_F1":{"displayName":"Microsoft 365 F1","category":"Microsoft 365","licenseCategory":"Paid","isBillable":true,"priceUSD":2.25,"includes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"],"derivedIncludes":["EXCHANGESTANDARD","SHAREPOINTSTANDARD","MCOSTANDARD","ONEDRIVESTANDARD"]},
|
|
28
28
|
"ENTRA_IDENTITY_PROTECTION":{"displayName":"Microsoft Entra ID Protection","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":["AAD_PREMIUM_P2"],"derivedIncludes":["AAD_PREMIUM_P2","AAD_PREMIUM"]},
|
|
29
29
|
"ENTRA_IDENTITY_GOVERNANCE_STEPUP":{"displayName":"Microsoft Entra ID Governance Step-Up","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":4,"includes":["AAD_PREMIUM_P1"],"derivedIncludes":["AAD_PREMIUM_P1"]},
|
|
30
|
+
"M365_E5_SECURITY":{"displayName":"Microsoft 365 E5 Security","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":12,"includes":["MDE_ADVANCED","DEFENDER_OFFICE_365","IDENTITY_THREAT_PROTECTION","DEFENDER_CLOUD_APPS"],"derivedIncludes":["MDE_ADVANCED","DEFENDER_OFFICE_365","IDENTITY_THREAT_PROTECTION","DEFENDER_CLOUD_APPS"]},
|
|
31
|
+
"M365_E5_COMPLIANCE":{"displayName":"Microsoft 365 E5 Compliance","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":10,"includes":[]},
|
|
30
32
|
"ENTRA_PERMISSIONS_MANAGEMENT":{"displayName":"Microsoft Entra Permissions Management","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":10,"includes":[]},
|
|
33
|
+
"ENTRA_WORKLOAD_ID":{"displayName":"Microsoft Entra Workload ID","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":3,"includes":[]},
|
|
34
|
+
"ENTRA_VERIFIED_ID":{"displayName":"Microsoft Entra Verified ID","category":"Entra","licenseCategory":"Consumption","isBillable":true,"priceUSD":0,"includes":[]},
|
|
31
35
|
"AAD_PREMIUM_P1":{"displayName":"Microsoft Entra ID P1","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":[]},
|
|
32
36
|
"AAD_B2C":{"displayName":"Azure AD B2C","category":"Entra","licenseCategory":"Consumption","isBillable":true,"priceUSD":0,"includes":[]},
|
|
33
37
|
"AAD_BASIC_EDU":{"displayName":"Azure Active Directory Basic (Education)","category":"Entra","licenseCategory":"Free","isBillable":false,"priceUSD":0,"includes":[]},
|
|
34
38
|
"M365_DEFENDER_IDENTITY":{"displayName":"Microsoft 365 Defender for Identity","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":[]},
|
|
39
|
+
"TEAMS_PREMIUM":{"displayName":"Microsoft Teams Premium","category":"Collaboration","licenseCategory":"Paid","isBillable":true,"priceUSD":10,"includes":[]},
|
|
35
40
|
"Microsoft_Teams_EEA_New": { "displayName": "Microsoft Teams EEA New", "category": "Collaboration", "licenseCategory": "Paid", "isBillable": true, "priceUSD": 4.00, "includes": [] },
|
|
41
|
+
"MCOEV_VIRTUALUSER":{"displayName":"Teams Phone Resource Account License","category":"Collaboration","licenseCategory":"Paid","isBillable":true,"priceUSD":0,"includes":[]},
|
|
42
|
+
"TEAMS_AUDIO_CONF":{"displayName":"Microsoft Teams Audio Conferencing","category":"Collaboration","licenseCategory":"Paid","isBillable":true,"priceUSD":4,"includes":[]},
|
|
36
43
|
"POWERAPPS_DEV": { "displayName": "Power Apps Developer Plan", "category": "Power Platform", "licenseCategory": "Free", "isBillable": false, "priceUSD": 0.00, "includes": [] },
|
|
37
44
|
"EXCHANGESTANDARD":{"displayName":"Exchange Online Plan 1","category":"Exchange","licenseCategory":"Paid","isBillable":true,"priceUSD":4,"includes":[]},
|
|
38
45
|
"EXCHANGEENTERPRISE":{"displayName":"Exchange Online Plan 2","category":"Exchange","licenseCategory":"Paid","isBillable":true,"priceUSD":8,"includes":[]},
|
|
39
46
|
"EXCHANGEARCHIVE_ADDON":{"displayName":"Exchange Online Archiving","category":"Exchange","licenseCategory":"Paid","isBillable":true,"priceUSD":3,"includes":[]},
|
|
47
|
+
"EXCHANGE_KIOSK":{"displayName":"Exchange Online Kiosk","category":"Exchange","licenseCategory":"Paid","isBillable":true,"priceUSD":2,"includes":[]},
|
|
40
48
|
"SHAREPOINTSTANDARD":{"displayName":"SharePoint Online Plan 1","category":"SharePoint","licenseCategory":"Paid","isBillable":true,"priceUSD":5,"includes":[]},
|
|
41
49
|
"SHAREPOINTENTERPRISE":{"displayName":"SharePoint Online Plan 2","category":"SharePoint","licenseCategory":"Paid","isBillable":true,"priceUSD":10,"includes":[]},
|
|
42
50
|
"MCOSTANDARD":{"displayName":"Microsoft Teams","category":"Collaboration","licenseCategory":"Paid","isBillable":true,"priceUSD":5,"includes":[]},
|
|
@@ -44,6 +52,8 @@
|
|
|
44
52
|
"PHONESYSTEM_VIRTUALUSER":{"displayName":"Microsoft Teams Phone Resource Account","category":"Collaboration","licenseCategory":"Paid","isBillable":true,"priceUSD":0,"includes":[]},
|
|
45
53
|
"MCOPSTN1":{"displayName":"Microsoft Teams Domestic Calling Plan","category":"Collaboration","licenseCategory":"Paid","isBillable":true,"priceUSD":8,"includes":[]},
|
|
46
54
|
"MCOPSTN2":{"displayName":"Microsoft Teams International Calling Plan","category":"Collaboration","licenseCategory":"Paid","isBillable":true,"priceUSD":12,"includes":[]},
|
|
55
|
+
"POWER_AUTOMATE_PER_FLOW":{"displayName":"Power Automate Per Flow","category":"Power Platform","licenseCategory":"Paid","isBillable":true,"priceUSD":100,"includes":[]},
|
|
56
|
+
"POWER_PAGES":{"displayName":"Power Pages","category":"Power Platform","licenseCategory":"Consumption","isBillable":true,"priceUSD":0,"includes":[]},
|
|
47
57
|
"POWER_BI_STANDARD":{"displayName":"Microsoft Fabric (Free)","category":"Power Platform","licenseCategory":"Free","isBillable":false,"priceUSD":0,"includes":[]},
|
|
48
58
|
"POWER_BI_PRO":{"displayName":"Power BI Pro","category":"Power Platform","licenseCategory":"Paid","isBillable":true,"priceUSD":13,"includes":[]},
|
|
49
59
|
"POWERAPPS_PER_USER":{"displayName":"Power Apps Per User","category":"Power Platform","licenseCategory":"Paid","isBillable":true,"priceUSD":10,"includes":[]},
|
|
@@ -58,10 +68,15 @@
|
|
|
58
68
|
"VISIO_P2":{"displayName":"Visio Plan 2","category":"Visio","licenseCategory":"Paid","isBillable":true,"priceUSD":12,"includes":[]},
|
|
59
69
|
"MDE_ADVANCED":{"displayName":"Microsoft Defender for Endpoint","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":8,"includes":[]},
|
|
60
70
|
"DEFENDER_OFFICE_365":{"displayName":"Microsoft Defender for Office 365","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":5,"includes":[]},
|
|
71
|
+
"DEFENDER_ENDPOINT_P1":{"displayName":"Microsoft Defender for Endpoint Plan 1","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":3,"includes":[]},
|
|
72
|
+
"DEFENDER_ENDPOINT_P2":{"displayName":"Microsoft Defender for Endpoint Plan 2","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":5,"includes":[]},
|
|
73
|
+
"DEFENDER_IDENTITY_STANDALONE":{"displayName":"Microsoft Defender for Identity (Standalone)","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":[]},
|
|
61
74
|
"IDENTITY_THREAT_PROTECTION":{"displayName":"Microsoft Defender for Identity","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":[]},
|
|
62
75
|
"DEFENDER_CLOUD_APPS":{"displayName":"Microsoft Defender for Cloud Apps","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":7,"includes":[]},
|
|
63
76
|
"M365_COPILOT":{"displayName":"Microsoft 365 Copilot","category":"AI","licenseCategory":"Paid","isBillable":true,"priceUSD":30,"includes":[]},
|
|
64
77
|
"COPILOT_STUDIO":{"displayName":"Copilot Studio","category":"AI","licenseCategory":"Paid","isBillable":true,"priceUSD":20,"includes":[]},
|
|
78
|
+
"GITHUB_COPILOT":{"displayName":"GitHub Copilot Business","category":"AI","licenseCategory":"Paid","isBillable":true,"priceUSD":19,"includes":[]},
|
|
79
|
+
"DYNAMICS_COPILOT":{"displayName":"Dynamics 365 Copilot","category":"AI","licenseCategory":"Paid","isBillable":true,"priceUSD":0,"includes":[]},
|
|
65
80
|
"VIVA_CONNECTIONS":{"displayName":"Microsoft Viva Connections","category":"Viva","licenseCategory":"Paid","isBillable":true,"priceUSD":3,"includes":[]},
|
|
66
81
|
"VIVA_INSIGHTS":{"displayName":"Microsoft Viva Insights","category":"Viva","licenseCategory":"Paid","isBillable":true,"priceUSD":3,"includes":[]},
|
|
67
82
|
"VIVA_LEARNING":{"displayName":"Microsoft Viva Learning","category":"Viva","licenseCategory":"Paid","isBillable":true,"priceUSD":4,"includes":[]},
|
|
@@ -76,6 +91,8 @@
|
|
|
76
91
|
"ENTRA_PRIVATE_ACCESS":{"displayName":"Microsoft Entra Private Access","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":8,"includes":[]},
|
|
77
92
|
"ENTRA_INTERNET_ACCESS":{"displayName":"Microsoft Entra Internet Access","category":"Entra","licenseCategory":"Paid","isBillable":true,"priceUSD":5,"includes":[]},
|
|
78
93
|
"WINDOWS_STORE":{"displayName":"Microsoft Store for Business","category":"Windows","licenseCategory":"Free","isBillable":false,"priceUSD":0,"includes":[]},
|
|
94
|
+
"WINDOWS_E3":{"displayName":"Windows Enterprise E3","category":"Windows","licenseCategory":"Paid","isBillable":true,"priceUSD":7,"includes":[]},
|
|
95
|
+
"WINDOWS_E5":{"displayName":"Windows Enterprise E5","category":"Windows","licenseCategory":"Paid","isBillable":true,"priceUSD":11,"includes":[]},
|
|
79
96
|
"WIN_DEF_ATP":{"displayName":"Windows Defender ATP","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":5,"includes":[]},
|
|
80
97
|
"RMS_S_ENTERPRISE":{"displayName":"Azure Information Protection Premium P1","category":"Security","licenseCategory":"Paid","isBillable":true,"priceUSD":6,"includes":[]},
|
|
81
98
|
"INTUNE_A":{"displayName":"Microsoft Intune","category":"Device Management","licenseCategory":"Paid","isBillable":true,"priceUSD":8,"includes":[]},
|
package/lib/helper-rest.ts
CHANGED
|
@@ -59,6 +59,35 @@ export class HelperRest {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
;(async () => {
|
|
64
|
+
// housekeeping - odata pagination
|
|
65
|
+
while (true) {
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000)) // 5 minutes
|
|
67
|
+
for (const baseEntity in this.config_entity) {
|
|
68
|
+
if (!this._serviceClient[baseEntity]?.index) continue
|
|
69
|
+
for (const index in this._serviceClient[baseEntity].index) {
|
|
70
|
+
const parsed = parseInt(index, 10)
|
|
71
|
+
if (Number.isNaN(parsed)) continue
|
|
72
|
+
if (Object.keys(this._serviceClient[baseEntity].index[parsed]).length === 0) {
|
|
73
|
+
delete this._serviceClient[baseEntity].index[parsed]
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
for (const optionsUrl in this._serviceClient[baseEntity].index[parsed]) {
|
|
77
|
+
const validTo = this._serviceClient[baseEntity].index[parsed][optionsUrl].validTo
|
|
78
|
+
if (Number.isNaN(validTo)) {
|
|
79
|
+
delete this._serviceClient[baseEntity].index[parsed][optionsUrl]
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
const now = Math.floor(Date.now() / 1000)
|
|
83
|
+
if (now > validTo) {
|
|
84
|
+
delete this._serviceClient[baseEntity].index[parsed][optionsUrl]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})()
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
/**
|
|
@@ -659,28 +688,25 @@ export class HelperRest {
|
|
|
659
688
|
options.body = dataString
|
|
660
689
|
} else if (options.headers) delete options.headers['Content-Type']
|
|
661
690
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
ctx.paging.startIndex = 1 // caller should check and return this new startIndex in final response
|
|
676
|
-
delete this._serviceClient[baseEntity].nextLink[options.url]
|
|
677
|
-
}
|
|
691
|
+
const optionsUrl = options.url
|
|
692
|
+
const startIndex: number = ctx?.paging?.startIndex || 0
|
|
693
|
+
if (startIndex > 1) {
|
|
694
|
+
if (this._serviceClient[baseEntity].index && this._serviceClient[baseEntity].index[startIndex]
|
|
695
|
+
&& this._serviceClient[baseEntity].index[startIndex][optionsUrl] && this._serviceClient[baseEntity].index[startIndex][optionsUrl].nextLink) {
|
|
696
|
+
options.url = this._serviceClient[baseEntity].index[startIndex][optionsUrl].nextLink
|
|
697
|
+
} else {
|
|
698
|
+
let max = 0
|
|
699
|
+
for (const key in this._serviceClient[baseEntity].index) {
|
|
700
|
+
const parsed = parseInt(key, 10)
|
|
701
|
+
if (Number.isNaN(parsed)) continue
|
|
702
|
+
if (!this._serviceClient[baseEntity].index[parsed][optionsUrl]?.totalResults) continue
|
|
703
|
+
if (parsed > max) max = parsed
|
|
678
704
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
705
|
+
if (max > 0) {
|
|
706
|
+
ctx.paging.totalResults = this._serviceClient[baseEntity].index[max][optionsUrl].totalResults
|
|
707
|
+
if (startIndex > ctx.paging.totalResults) return { body: { value: [] } }
|
|
708
|
+
}
|
|
709
|
+
throw new Error(`Strict sequencing is required; startIndex=${ctx.paging.startIndex} is not the expected value.`)
|
|
684
710
|
}
|
|
685
711
|
}
|
|
686
712
|
|
|
@@ -721,38 +747,56 @@ export class HelperRest {
|
|
|
721
747
|
// if (!ctx) ctx = { paging }
|
|
722
748
|
// else ctx.paging = paging
|
|
723
749
|
if (result.body && typeof result.body === 'object') {
|
|
724
|
-
if (result.body['@odata.nextLink']) { // {"@odata.nextLink": "https://graph.microsoft.com/
|
|
750
|
+
if (result.body['@odata.nextLink']) { // {"@odata.nextLink": "https://graph.microsoft.com/beta/users?$top=100&$skiptoken=xxx"}
|
|
725
751
|
if (!ctx) ctx = {}
|
|
726
752
|
if (!ctx.paging) ctx.paging = {}
|
|
727
|
-
const nextLinkBase = decodeURIComponent(result.body['@odata.nextLink'].substring(0, result.body['@odata.nextLink'].indexOf('$skiptoken') - 1))
|
|
728
753
|
const count = result.body['@odata.count']
|
|
729
754
|
if (count !== undefined) {
|
|
730
755
|
ctx.paging.totalResults = count
|
|
731
756
|
}
|
|
732
|
-
let totalResults = ctx.paging.totalResults
|
|
733
|
-
if (!totalResults) totalResults = (this._serviceClient[baseEntity].nextLink[nextLinkBase]?.totalResults)
|
|
734
|
-
let isCount = this._serviceClient[baseEntity].nextLink[nextLinkBase]?.isCount || count !== undefined
|
|
735
757
|
const itemsPerPage = result.body.value.length
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
this._serviceClient[baseEntity].
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
758
|
+
const nextStartIndex = ctx.paging.startIndex ? ctx.paging.startIndex + itemsPerPage : itemsPerPage + 1
|
|
759
|
+
let totalResults = ctx.paging.totalResults
|
|
760
|
+
if (ctx.paging.startIndex && this._serviceClient[baseEntity].index && this._serviceClient[baseEntity].index[ctx.paging.startIndex]) {
|
|
761
|
+
if (this._serviceClient[baseEntity].index[ctx.paging.startIndex][optionsUrl]) {
|
|
762
|
+
if (!totalResults) totalResults = this._serviceClient[baseEntity].index[ctx.paging.startIndex][optionsUrl].totalResults
|
|
763
|
+
}
|
|
764
|
+
delete this._serviceClient[baseEntity].index[ctx.paging.startIndex] // could consider not to delte prior and let houskeeping do the cleanup
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (!this._serviceClient[baseEntity].index) this._serviceClient[baseEntity].index = {}
|
|
768
|
+
if (!this._serviceClient[baseEntity].index[nextStartIndex]) this._serviceClient[baseEntity].index[nextStartIndex] = {}
|
|
769
|
+
if (!this._serviceClient[baseEntity].index[nextStartIndex][optionsUrl]) this._serviceClient[baseEntity].index[nextStartIndex][optionsUrl] = {}
|
|
770
|
+
this._serviceClient[baseEntity].index[nextStartIndex][optionsUrl].nextLink = result.body['@odata.nextLink']
|
|
771
|
+
ctx.paging.nextLink = result.body['@odata.nextLink'] // gives client option to auto-paginate without using startIndex
|
|
772
|
+
if (ctx.paging.nextLink.startsWith(this._serviceClient[baseEntity].baseUrl)) {
|
|
773
|
+
ctx.paging.nextLink = ctx.paging.nextLink.slice(this._serviceClient[baseEntity].baseUrl.length)
|
|
774
|
+
if (!ctx.paging.nextLink.startsWith('/')) ctx.paging.nextLink = '/' + ctx.paging.nextLink
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (count) {
|
|
778
|
+
this._serviceClient[baseEntity].index[nextStartIndex][optionsUrl].totalResults = totalResults // count=true ignored when using nextLink
|
|
742
779
|
ctx.paging.totalResults = totalResults
|
|
743
780
|
} else {
|
|
744
781
|
const totalResults = ctx.paging.startIndex - 1 + (itemsPerPage * 2) // ensure new client paging
|
|
745
|
-
this._serviceClient[baseEntity].
|
|
782
|
+
this._serviceClient[baseEntity].index[nextStartIndex][optionsUrl].totalResults = totalResults
|
|
746
783
|
ctx.paging.totalResults = totalResults
|
|
747
784
|
}
|
|
785
|
+
const d = Math.floor((Date.now() + 5 * 60 * 1000) / 1000)// now + 5 minutes
|
|
786
|
+
this._serviceClient[baseEntity].index[nextStartIndex][optionsUrl].validTo = d
|
|
748
787
|
} else { // no more paging
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (
|
|
788
|
+
if (ctx?.paging) {
|
|
789
|
+
if (ctx.paging.nextLink) delete ctx.paging.nextLink
|
|
790
|
+
if (ctx.paging.startIndex) {
|
|
752
791
|
const itemsPerPage = result.body.value.length
|
|
753
792
|
const totalResults = ctx.paging.startIndex - 1 + itemsPerPage
|
|
754
|
-
this._serviceClient[baseEntity].
|
|
755
|
-
|
|
793
|
+
if (this._serviceClient[baseEntity].index && this._serviceClient[baseEntity].index[ctx.paging.startIndex] && this._serviceClient[baseEntity].index[ctx.paging.startIndex][optionsUrl]) {
|
|
794
|
+
// keeping the last one with updated totalResults to catch startIndex > totalResults, houskeeping will clean up after 5 minutes
|
|
795
|
+
this._serviceClient[baseEntity].index[ctx.paging.startIndex][optionsUrl].totalResults = totalResults // update the last one with correct totalResults
|
|
796
|
+
ctx.paging.totalResults = totalResults
|
|
797
|
+
const d = Math.floor((Date.now() + 5 * 60 * 1000) / 1000)
|
|
798
|
+
this._serviceClient[baseEntity].index[ctx.paging.startIndex][optionsUrl].validTo = d
|
|
799
|
+
}
|
|
756
800
|
}
|
|
757
801
|
}
|
|
758
802
|
}
|
|
@@ -769,12 +813,6 @@ export class HelperRest {
|
|
|
769
813
|
if (!retryAfter) retryAfter = 60
|
|
770
814
|
}
|
|
771
815
|
|
|
772
|
-
if (retryAfter) {
|
|
773
|
-
this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} throttle/ratelimit error - awaiting ${retryAfter} seconds before automatic retry`)
|
|
774
|
-
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (!retryCount) retryCount = 0
|
|
778
816
|
let urlObj
|
|
779
817
|
try { urlObj = new URL(path) } catch (err) { void 0 }
|
|
780
818
|
let isServiceClient = !urlObj && this._serviceClient[baseEntity] && !this.lock.isLocked() // !isLocked to avoid retry ongoing doRequest with failing getAccessToken()
|
|
@@ -782,12 +820,25 @@ export class HelperRest {
|
|
|
782
820
|
|
|
783
821
|
if (isServiceClient && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ABORT_ERR' || err.code === 'ETIMEDOUT' || statusCode === 504 || oAuthTokeErr || retryAfter)) {
|
|
784
822
|
this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
823
|
+
|
|
824
|
+
let maxRetry = connectionObj.baseUrls.length
|
|
825
|
+
if (!retryCount) retryCount = 0
|
|
826
|
+
if (retryAfter) {
|
|
827
|
+
const delta = retryCount - maxRetry
|
|
828
|
+
if (delta >= 0 && delta < 2) maxRetry += delta + 1 // allow 3 retry for the same initial throttle
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (retryCount < maxRetry) {
|
|
832
|
+
if (retryAfter) {
|
|
833
|
+
this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} throttle/ratelimit error - awaiting ${retryAfter} seconds before automatic retry`)
|
|
834
|
+
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
|
|
790
835
|
}
|
|
836
|
+
const index = retryCount < connectionObj.baseUrls.length ? retryCount : connectionObj.baseUrls.length - 1
|
|
837
|
+
retryCount++
|
|
838
|
+
|
|
839
|
+
this.updateServiceClient(baseEntity, { baseUrl: connectionObj.baseUrls[index] })
|
|
840
|
+
this.scimgateway.logDebug(baseEntity, `${(connectionObj.baseUrls.length > 1) ? 'failover ' : ''}retry[${retryCount}] using baseUrl = ${this._serviceClient[baseEntity].baseUrl}`)
|
|
841
|
+
|
|
791
842
|
if (oAuthTokeErr) {
|
|
792
843
|
delete this._serviceClient[baseEntity] // ensure new getAccessToken request - token used should not have been expired, but rejected for other reason e.g. token server restart and no persistent token store?
|
|
793
844
|
}
|
|
@@ -868,7 +919,7 @@ export class HelperRest {
|
|
|
868
919
|
* {
|
|
869
920
|
* "type": "oauth",
|
|
870
921
|
* "options": {
|
|
871
|
-
* "azureTenantId": "<Entra ID azureTenantId", // Entra ID authentication - if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/
|
|
922
|
+
* "azureTenantId": "<Entra ID azureTenantId", // Entra ID authentication - if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/beta]
|
|
872
923
|
* "tokenUrl": "<tokenUrl>", // must be set if not using azureTenantId
|
|
873
924
|
* "clientId": "<clientId>",
|
|
874
925
|
* "clientSecret": "<clientSecret>"
|
|
@@ -927,7 +978,7 @@ export class HelperRest {
|
|
|
927
978
|
* {
|
|
928
979
|
* "type": "oauthJwtBearer",
|
|
929
980
|
* "options": {
|
|
930
|
-
* "azureTenantId": "<Entra ID azureTenantId", // Entra ID authentication, if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/
|
|
981
|
+
* "azureTenantId": "<Entra ID azureTenantId", // Entra ID authentication, if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/beta]
|
|
931
982
|
* "clientId": "<clientId>",
|
|
932
983
|
* "tls": { // files located in ./config/certs
|
|
933
984
|
* "key": "key.pem",
|
package/lib/plugin-entra-id.ts
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
import path from 'node:path'
|
|
77
77
|
|
|
78
78
|
// start - mandatory plugin initialization
|
|
79
|
-
import { ScimGateway, HelperRest } from 'scimgateway'
|
|
79
|
+
import { ScimGateway, HelperRest, LicenseData } from 'scimgateway'
|
|
80
80
|
const scimgateway = new ScimGateway()
|
|
81
81
|
const helper = new HelperRest(scimgateway)
|
|
82
82
|
const config = scimgateway.getConfig()
|
|
@@ -84,7 +84,6 @@ scimgateway.authPassThroughAllowed = false
|
|
|
84
84
|
scimgateway.pluginAndOrFilterEnabled = true
|
|
85
85
|
// end - mandatory plugin initialization
|
|
86
86
|
|
|
87
|
-
const newHelper = new HelperRest(scimgateway)
|
|
88
87
|
const entitlementsByValues: Record<string, any> = {}
|
|
89
88
|
const rolesByValues: Record<string, any> = {}
|
|
90
89
|
const rolesAssignments: Record<string, any> = {}
|
|
@@ -94,18 +93,14 @@ const permission: Record<string, any> = {}
|
|
|
94
93
|
|
|
95
94
|
// load Azure license mapping JSON-file having skuPartNumber and corresponding user-friendly name
|
|
96
95
|
let fs: typeof import('fs')
|
|
97
|
-
let licenseMapping: Record<string, any> =
|
|
96
|
+
let licenseMapping: Record<string, any> = LicenseData
|
|
98
97
|
async function loadLicenseMapping() {
|
|
99
98
|
try {
|
|
100
99
|
if (!fs) fs = (await import('fs'))
|
|
100
|
+
// runtime override: if file exists in pluginDir, use it instead of the embedded default
|
|
101
101
|
let mappingPath = path.join(scimgateway.pluginDir, 'azure-license-mapping.json')
|
|
102
102
|
if (fs.existsSync(mappingPath)) {
|
|
103
103
|
licenseMapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'))
|
|
104
|
-
} else {
|
|
105
|
-
mappingPath = path.join(scimgateway.gwDir, 'azure-license-mapping.json')
|
|
106
|
-
if (fs.existsSync(mappingPath)) {
|
|
107
|
-
licenseMapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'))
|
|
108
|
-
}
|
|
109
104
|
}
|
|
110
105
|
} catch (err) {
|
|
111
106
|
scimgateway.logDebug('plugin-entra-id', `Error loading license mapping: ${err}`)
|
|
@@ -252,7 +247,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
252
247
|
}
|
|
253
248
|
} else selectAttributes = userSelectAttributes
|
|
254
249
|
|
|
255
|
-
if (
|
|
250
|
+
if (permission[baseEntity]?.signInActivity === false) { // remove signInActivity
|
|
256
251
|
const index = selectAttributes.indexOf('signInActivity')
|
|
257
252
|
if (index > -1) {
|
|
258
253
|
selectAttributes.splice(index, 1)
|
|
@@ -941,15 +936,14 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
941
936
|
if (getObj.operator === 'eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
|
|
942
937
|
// mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
|
|
943
938
|
if (getObj.attribute === 'id') {
|
|
944
|
-
|
|
945
|
-
else path = `/groups/${getObj.value}?$select=${attrs.join()}`
|
|
939
|
+
path = `/groups/${getObj.value}?$select=${attrs.join()}`
|
|
946
940
|
} else {
|
|
947
|
-
|
|
948
|
-
else path = `/groups?$filter=${getObj.attribute} eq '${getObj.value}'&$select=${attrs.join()}`
|
|
941
|
+
path = `/groups?$filter=${getObj.attribute} eq '${getObj.value}'&$select=${attrs.join()}`
|
|
949
942
|
}
|
|
950
943
|
} else if (isUserMemberOf) {
|
|
951
944
|
// mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
|
|
952
945
|
// Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
|
|
946
|
+
includeMembers = false
|
|
953
947
|
path = `/users/${getObj.value}/transitiveMemberOf/microsoft.graph.group?$top=${getObj.count}&$count=true&$select=id,displayName`
|
|
954
948
|
} else {
|
|
955
949
|
// optional - simpel filtering
|
|
@@ -961,8 +955,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
961
955
|
throw new Error(`${action} error: not supporting advanced filtering: ${getObj.rawFilter}`)
|
|
962
956
|
} else {
|
|
963
957
|
// mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all groups to be returned - correspond to exploreGroups() in versions < 4.x.x
|
|
964
|
-
|
|
965
|
-
else path = `/groups?$top=${getObj.count}&$count=true&$select=${attrs.join()}`
|
|
958
|
+
path = `/groups?$top=${getObj.count}&$count=true&$select=${attrs.join()}`
|
|
966
959
|
}
|
|
967
960
|
if (getObj.and || getObj.or) {
|
|
968
961
|
// plugin have enabled 'scimgateway.pluginAndOrFilterEnabled' and the query includes an additonal and/or getObj that must to be handled and combined with the initial getObj
|
|
@@ -984,7 +977,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
984
977
|
if (!ctx) ctx = { paging }
|
|
985
978
|
else ctx.paging = paging
|
|
986
979
|
|
|
987
|
-
const newCtx = {
|
|
980
|
+
const newCtx: Record<string, any> = ctx?.headers ? { headers: ctx?.headers } : {}
|
|
988
981
|
newCtx.paging = { startIndex: 1 }
|
|
989
982
|
|
|
990
983
|
try {
|
|
@@ -993,11 +986,11 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
993
986
|
if (!isUserMemberOf) response = await helper.doRequest(baseEntity, method, path, body, ctx, options)
|
|
994
987
|
else {
|
|
995
988
|
// request both the default transitiveMemberOf (includes nested groups) and memberOf because we want to distinguish SCIM type=direct/indirect
|
|
996
|
-
|
|
989
|
+
let pathMemberOf = `/users/${getObj.value}/memberOf/microsoft.graph.group?$count=true&$select=id,displayName`
|
|
997
990
|
const allErrors: string[] = []
|
|
998
991
|
const results = await Promise.allSettled([
|
|
999
992
|
helper.doRequest(baseEntity, method, path, body, ctx, options),
|
|
1000
|
-
|
|
993
|
+
helper.doRequest(baseEntity, method, pathMemberOf, body, newCtx, options),
|
|
1001
994
|
])
|
|
1002
995
|
const errors = results
|
|
1003
996
|
.filter(r => r.status === 'rejected')
|
|
@@ -1010,30 +1003,23 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
1010
1003
|
}
|
|
1011
1004
|
|
|
1012
1005
|
response = (results[0] as PromiseFulfilledResult<any>).value // includes all groups (also nested)
|
|
1013
|
-
responseMemberOf = (results[1] as PromiseFulfilledResult<any>).value //
|
|
1014
|
-
|
|
1015
|
-
let nextStartIndex = scimgateway.getNextStartIndex(responseMemberOf.body.value.length * 2, newCtx.paging.startIndex, responseMemberOf.body.value.length)
|
|
1016
|
-
if (nextStartIndex > newCtx.paging.startIndex && responseMemberOf && responseMemberOf.body.value && Array.isArray(responseMemberOf.body.value)) {
|
|
1017
|
-
// use paging to ensure responseMemberOf is complete
|
|
1018
|
-
let totalResults = responseMemberOf.body.value.length
|
|
1019
|
-
let startIndex = 1
|
|
1006
|
+
responseMemberOf = (results[1] as PromiseFulfilledResult<any>).value // does not include nested groups
|
|
1007
|
+
while (newCtx.paging.nextLink) {
|
|
1020
1008
|
let res: any
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
responseMemberOf.body.value.push(res.body.value[i])
|
|
1034
|
-
}
|
|
1009
|
+
pathMemberOf = newCtx.paging.nextLink
|
|
1010
|
+
delete newCtx.paging.nextLink
|
|
1011
|
+
try {
|
|
1012
|
+
res = await helper.doRequest(baseEntity, method, pathMemberOf, body, newCtx, options)
|
|
1013
|
+
} catch (err: any) {
|
|
1014
|
+
delete newCtx.paging.nextLink
|
|
1015
|
+
break
|
|
1016
|
+
}
|
|
1017
|
+
if (res?.body && res.body.value && Array.isArray(res.body.value) && res.body.value.length > 0) {
|
|
1018
|
+
for (let i = 0; i < res.body.value.length; i++) {
|
|
1019
|
+
if (!res.body.value[i].id) continue
|
|
1020
|
+
responseMemberOf.body.value.push(res.body.value[i])
|
|
1035
1021
|
}
|
|
1036
|
-
}
|
|
1022
|
+
}
|
|
1037
1023
|
}
|
|
1038
1024
|
|
|
1039
1025
|
if (response.body && response.body.value && Array.isArray(response.body.value)) {
|
|
@@ -1047,6 +1033,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
1047
1033
|
})
|
|
1048
1034
|
}
|
|
1049
1035
|
}
|
|
1036
|
+
|
|
1050
1037
|
if (!response.body) {
|
|
1051
1038
|
throw new Error(`invalid response: ${JSON.stringify(response)}`)
|
|
1052
1039
|
}
|
|
@@ -1056,6 +1043,22 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
1056
1043
|
}
|
|
1057
1044
|
|
|
1058
1045
|
for (let i = 0; i < response.body.value.length; ++i) {
|
|
1046
|
+
if (includeMembers) {
|
|
1047
|
+
const id = response.body.value[i].id
|
|
1048
|
+
if (!id) break
|
|
1049
|
+
let path = `/groups/${id}/members?$select=id,displayName`
|
|
1050
|
+
const newCtx: Record<string, any> = ctx?.headers ? { headers: ctx?.headers } : {}
|
|
1051
|
+
newCtx.paging = { startIndex: 1 }
|
|
1052
|
+
const r = await helper.doRequest(baseEntity, 'GET', path, null, newCtx, options)
|
|
1053
|
+
response.body.value[i].members = r.body?.value || []
|
|
1054
|
+
while (newCtx.paging.nextLink) {
|
|
1055
|
+
const path = newCtx.paging.nextLink
|
|
1056
|
+
delete newCtx.paging.nextLink
|
|
1057
|
+
const r = await helper.doRequest(baseEntity, 'GET', path, null, newCtx, options)
|
|
1058
|
+
response.body.value[i].members.push(...r.body?.value || [])
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1059
1062
|
let members: any
|
|
1060
1063
|
if (response.body.value[i].members) {
|
|
1061
1064
|
members = response.body.value[i].members.reduce((acc: any[], el: Record<string, any>) => {
|
package/lib/saml/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Auth0, Inc. <support@auth0.com> (http://auth0.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Local `saml` Package
|
|
2
|
+
|
|
3
|
+
This folder contains a local copy of the [node-saml](https://github.com/auth0/node-saml) package (v4.0.0), originally authored by Auth0.
|
|
4
|
+
Licensed under the MIT License — see [LICENSE](./LICENSE).
|
|
5
|
+
|
|
6
|
+
## Why This Exists
|
|
7
|
+
|
|
8
|
+
When compiling scimgateway plugins to standalone binaries using `bun build --compile`, the original `saml` npm package fails at runtime on any machine other than the build machine.
|
|
9
|
+
|
|
10
|
+
**Root cause:** The original `saml` package uses `fs.readFileSync` with `__dirname`-resolved paths to load XML template files (`saml11.template` and `saml20.template`) at module initialization:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
// Original saml/lib/saml11.js
|
|
14
|
+
var newSaml11Document = utils.factoryForNode(path.join(__dirname, 'saml11.template'));
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// Original saml/lib/utils.js
|
|
19
|
+
exports.factoryForNode = function factoryForNode(pathToTemplate) {
|
|
20
|
+
const template = fs.readFileSync(pathToTemplate); // fails in compiled binary
|
|
21
|
+
...
|
|
22
|
+
};
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When Bun compiles the binary, `__dirname` gets baked in as the **absolute path from the build machine** (e.g. `/Users/xxx/.../node_modules/saml/lib/`). The `.template` files are not embedded in the binary, so the `readFileSync` call fails with `ENOENT` when the binary runs elsewhere.
|
|
26
|
+
|
|
27
|
+
## What Was Changed
|
|
28
|
+
|
|
29
|
+
Only one functional change was made — **templates are inlined as string constants** instead of being read from disk at runtime.
|
|
30
|
+
|
|
31
|
+
### `utils.js`
|
|
32
|
+
- Removed `const fs = require('fs')` — no longer needed
|
|
33
|
+
- Changed `factoryForNode(pathToTemplate)` to `factoryForNode(templateString)` — accepts an XML string directly instead of a file path
|
|
34
|
+
|
|
35
|
+
### `saml11.js` and `saml20.js`
|
|
36
|
+
- Removed `path.join(__dirname, '*.template')` file references
|
|
37
|
+
- Template XML is inlined as a string constant passed to `factoryForNode()`
|
|
38
|
+
- Removed `var path = require('path')` (no longer needed)
|
|
39
|
+
|
|
40
|
+
### `xml/encrypt.js` and `xml/sign.js`
|
|
41
|
+
- Copied unchanged from the original package
|
|
42
|
+
|
|
43
|
+
### `index.js`
|
|
44
|
+
- Copied unchanged from the original package
|
|
45
|
+
|
|
46
|
+
## Import Change
|
|
47
|
+
|
|
48
|
+
In `lib/samlAssertion.ts`:
|
|
49
|
+
```diff
|
|
50
|
+
-import { Saml20 as saml } from 'saml'
|
|
51
|
+
+import { Saml20 as saml } from './saml/index.js'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Dependencies
|
|
55
|
+
|
|
56
|
+
The sub-dependencies used by this code remain as npm packages (not copied):
|
|
57
|
+
- `@xmldom/xmldom`
|
|
58
|
+
- `moment`
|
|
59
|
+
- `xml-crypto`
|
|
60
|
+
- `xml-encryption`
|
|
61
|
+
- `valid-url`
|
|
62
|
+
- `xml-name-validator`
|
|
63
|
+
- `async`
|
|
64
|
+
- `xpath`
|
|
65
|
+
|
|
66
|
+
The `saml` entry in `package.json` must be kept — it provides the sub-dependencies (@xmldom/xmldom, moment, xml-crypto, etc.) that this local copy requires.
|