Skip to content

Commit 9288c87

Browse files
authored
Merge pull request #921 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 880e557 + 5f75629 commit 9288c87

4 files changed

Lines changed: 219 additions & 16 deletions

File tree

Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT
44
Internal
55
#>
66
if (!$scope) { $scope = 'https://graph.microsoft.com/.default' }
7+
if (!$tenantid) { $tenantid = $env:TenantID }
78

9+
# ── Fast path: return cached token immediately, skip all table lookups ──
10+
$TokenKey = '{0}-{1}-{2}' -f $tenantid, $scope, $asApp
11+
if ($SkipCache -ne $true -and $script:AccessTokens.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:AccessTokens.$TokenKey.expires_on) {
12+
$AccessToken = $script:AccessTokens.$TokenKey
13+
if ($ReturnRefresh) { return $AccessToken }
14+
return @{ Authorization = "Bearer $($AccessToken.access_token)" }
15+
}
16+
17+
# ── Slow path: need a new token — do table lookups + token acquisition ──
818
if (!$env:SetFromProfile) { $CIPPAuth = Get-CIPPAuthentication; Write-Host 'Could not get Refreshtoken from environment variable. Reloading token.' }
919
$ConfigTable = Get-CippTable -tablename 'Config'
1020
$Filter = "PartitionKey eq 'AppCache' and RowKey eq 'AppCache'"
@@ -15,7 +25,6 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT
1525
$CIPPAuth = Get-CIPPAuthentication
1626
}
1727
$refreshToken = $env:RefreshToken
18-
if (!$tenantid) { $tenantid = $env:TenantID }
1928
#Get list of tenants that have 'directTenant' set to true
2029
#get directtenants directly from table, avoid get-tenants due to performance issues
2130
$TenantsTable = Get-CippTable -tablename 'Tenants'
@@ -90,20 +99,26 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT
9099
}
91100

92101

93-
$TokenKey = '{0}-{1}-{2}' -f $tenantid, $scope, $asApp
94-
95102
try {
96-
if ($script:AccessTokens.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:AccessTokens.$TokenKey.expires_on -and $SkipCache -ne $true) {
97-
#Write-Host 'Graph: cached token'
98-
$AccessToken = $script:AccessTokens.$TokenKey
103+
$TokenRequest = @{
104+
Method = 'POST'
105+
Uri = "https://login.microsoftonline.com/$($tenantid)/oauth2/v2.0/token"
106+
Body = $Authbody
107+
ErrorAction = 'Stop'
108+
}
109+
if ($script:LoginWebSession) {
110+
$TokenRequest.WebSession = $script:LoginWebSession
99111
} else {
100-
#Write-Host 'Graph: new token'
101-
$AccessToken = (Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($tenantid)/oauth2/v2.0/token" -Body $Authbody -ErrorAction Stop)
102-
$ExpiresOn = [int](Get-Date -UFormat %s -Millisecond 0) + $AccessToken.expires_in
103-
Add-Member -InputObject $AccessToken -NotePropertyName 'expires_on' -NotePropertyValue $ExpiresOn
104-
if (!$script:AccessTokens) { $script:AccessTokens = [HashTable]::Synchronized(@{}) }
105-
$script:AccessTokens.$TokenKey = $AccessToken
112+
$TokenRequest.SessionVariable = 'NewLoginSession'
113+
}
114+
$AccessToken = (Invoke-RestMethod @TokenRequest)
115+
if (!$script:LoginWebSession -and $NewLoginSession) {
116+
$script:LoginWebSession = $NewLoginSession
106117
}
118+
$ExpiresOn = [int](Get-Date -UFormat %s -Millisecond 0) + $AccessToken.expires_in
119+
Add-Member -InputObject $AccessToken -NotePropertyName 'expires_on' -NotePropertyValue $ExpiresOn
120+
if (!$script:AccessTokens) { $script:AccessTokens = [HashTable]::Synchronized(@{}) }
121+
$script:AccessTokens.$TokenKey = $AccessToken
107122

108123
if ($ReturnRefresh) { $header = $AccessToken } else { $header = @{ Authorization = "Bearer $($AccessToken.access_token)" } }
109124
return $header

Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ function New-GraphGetRequest {
7171
ContentType = 'application/json; charset=utf-8'
7272
}
7373

74+
# Reuse WebSession for TCP/TLS connection pooling (PS 7.4+)
75+
if ($script:GraphWebSession) {
76+
$GraphRequest.WebSession = $script:GraphWebSession
77+
} else {
78+
$GraphRequest.SessionVariable = 'NewGraphSession'
79+
}
80+
7481
if ($ReturnRawResponse) {
7582
$GraphRequest.SkipHttpErrorCheck = $true
7683
$Data = Invoke-WebRequest @GraphRequest
@@ -80,6 +87,11 @@ function New-GraphGetRequest {
8087
$script:LastGraphResponseHeaders = $ResponseHeaders
8188
}
8289

90+
# Store the WebSession for future calls in this runspace
91+
if (!$script:GraphWebSession -and $NewGraphSession) {
92+
$script:GraphWebSession = $NewGraphSession
93+
}
94+
8395
# If we reach here, the request was successful
8496
$RequestSuccessful = $true
8597

Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ function Get-GraphRequestList {
104104
foreach ($Key in $Keys) {
105105
if ($Parameters[$Key] -is [string]) {
106106
$Parameters[$Key] = [regex]::Replace($Parameters[$Key], '\{DaysAgo:(\d+)\}', {
107-
param($m)
108-
(Get-Date).ToUniversalTime().AddDays(-[int]$m.Groups[1].Value).ToString('yyyy-MM-dd')
109-
})
107+
param($m)
108+
(Get-Date).ToUniversalTime().AddDays( - [int]$m.Groups[1].Value).ToString('yyyy-MM-dd')
109+
})
110110
}
111111
}
112112

@@ -179,10 +179,13 @@ function Get-GraphRequestList {
179179
$GraphRequest.uri = $GraphQuery.ToString()
180180
}
181181

182-
if ($Parameters.'$count' -and !$SkipCache.IsPresent -and !$NoPagination.IsPresent) {
182+
if ($Parameters.'$count' -and -not $ManualPagination.IsPresent) {
183183
$Count = New-GraphGetRequest @GraphRequest -CountOnly -ErrorAction Stop
184184
if ($CountOnly.IsPresent) { return $Count }
185185
Write-Information "Total results (`$count): $Count"
186+
} elseif ($CountOnly.IsPresent) {
187+
$Count = New-GraphGetRequest @GraphRequest -CountOnly -ErrorAction Stop
188+
return $Count
186189
}
187190
}
188191
#Write-Information ( 'GET [ {0} ]' -f $GraphQuery.ToString())
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
function Invoke-ExecGraphRequestProfile {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
.ROLE
6+
CIPP.Core.Read
7+
#>
8+
[CmdletBinding()]
9+
param($Request, $TriggerMetadata)
10+
11+
$TenantFilter = $Request.Query.tenantFilter
12+
$Endpoint = $Request.Query.Endpoint
13+
if (!$TenantFilter -or !$Endpoint) {
14+
return [HttpResponseContext]@{
15+
StatusCode = 400
16+
Body = @{ error = 'tenantFilter and Endpoint are required' } | ConvertTo-Json
17+
}
18+
}
19+
20+
$Timings = [System.Collections.Generic.List[object]]::new()
21+
$OverallSw = [System.Diagnostics.Stopwatch]::StartNew()
22+
23+
function Add-Timing($Step, $Detail, $ElapsedMs) {
24+
$Timings.Add([PSCustomObject]@{
25+
Step = $Step
26+
Detail = $Detail
27+
Ms = [math]::Round($ElapsedMs, 1)
28+
WallClock = [math]::Round($OverallSw.Elapsed.TotalMilliseconds, 1)
29+
})
30+
}
31+
32+
# ── 1. Parameter setup ──────────────────────────────────────────────
33+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
34+
$Parameters = @{}
35+
foreach ($key in @('$filter', 'graphFilter', '$select', '$expand', 'expand', '$top', '$count', '$orderby', '$search', '$format')) {
36+
if ($Request.Query.$key) {
37+
if ($key -eq 'graphFilter') { $Parameters.'$filter' = $Request.Query.$key }
38+
elseif ($key -eq '$count') { $Parameters.$key = ([string]([System.Boolean]$Request.Query.$key)).ToLower() }
39+
else { $Parameters.$key = $Request.Query.$key }
40+
}
41+
}
42+
$sw.Stop()
43+
Add-Timing 'ParameterSetup' 'Extracted query params' $sw.Elapsed.TotalMilliseconds
44+
45+
# ── 2. Graph URL build ──────────────────────────────────────────────
46+
$sw.Restart()
47+
$Version = if ($Request.Query.Version) { $Request.Query.Version } else { 'beta' }
48+
$Endpoint = $Endpoint -replace '^/', ''
49+
$GraphQuery = [System.UriBuilder]('https://graph.microsoft.com/{0}/{1}' -f $Version, $Endpoint)
50+
$ParamCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty)
51+
foreach ($Item in ($Parameters.GetEnumerator() | Sort-Object -CaseSensitive -Property Key)) {
52+
$val = $Item.Value
53+
if ($val -is [System.Boolean]) { $val = $val.ToString().ToLower() }
54+
if ($val) { $ParamCollection.Add($Item.Key, $val) }
55+
}
56+
$GraphQuery.Query = $ParamCollection.ToString()
57+
$GraphUrl = $GraphQuery.ToString()
58+
$sw.Stop()
59+
Add-Timing 'UrlBuild' $GraphUrl $sw.Elapsed.TotalMilliseconds
60+
61+
# ── 3. Get-CIPPAuthentication (env check) ───────────────────────────
62+
$sw.Restart()
63+
$envPresent = [bool]$env:ApplicationID -and [bool]$env:ApplicationSecret -and [bool]$env:RefreshToken
64+
if (!$env:SetFromProfile) {
65+
Get-CIPPAuthentication | Out-Null
66+
}
67+
$sw.Stop()
68+
Add-Timing 'GetCIPPAuthentication' "EnvPresent=$envPresent SetFromProfile=$($env:SetFromProfile)" $sw.Elapsed.TotalMilliseconds
69+
70+
# ── 4. CIPPCore module state ────────────────────────────────────────
71+
$sw.Restart()
72+
$scope = 'https://graph.microsoft.com/.default'
73+
# Match Get-GraphToken's key format: $asApp is $null when not passed, so key ends with empty string
74+
$TokenKeyNull = '{0}-{1}-{2}' -f $TenantFilter, $scope, $null
75+
$TokenKeyFalse = '{0}-{1}-{2}' -f $TenantFilter, $scope, $false
76+
$coreState = & (Get-Module CIPPCore) {
77+
$keyNull = $args[0]
78+
$keyFalse = $args[1]
79+
$cachedNull = $script:AccessTokens.$keyNull
80+
$cachedFalse = $script:AccessTokens.$keyFalse
81+
$now = [int](Get-Date -UFormat %s -Millisecond 0)
82+
@{
83+
TokenCachedNull = [bool]($cachedNull -and $now -lt $cachedNull.expires_on)
84+
TokenCachedFalse = [bool]($cachedFalse -and $now -lt $cachedFalse.expires_on)
85+
CacheKeys = if ($script:AccessTokens) { @($script:AccessTokens.Keys) } else { @() }
86+
CacheType = if ($script:AccessTokens) { $script:AccessTokens.GetType().Name } else { 'null' }
87+
CacheCount = if ($script:AccessTokens) { $script:AccessTokens.Count } else { 0 }
88+
LoginSession = [bool]$script:LoginWebSession
89+
GraphSession = [bool]$script:GraphWebSession
90+
}
91+
} $TokenKeyNull $TokenKeyFalse
92+
$sw.Stop()
93+
Add-Timing 'CoreModuleState' "CacheCount=$($coreState.CacheCount) Keys=$($coreState.CacheKeys -join ';') LoginSession=$($coreState.LoginSession) GraphSession=$($coreState.GraphSession)" $sw.Elapsed.TotalMilliseconds
94+
95+
# ── 5. Get-AuthorisedRequest ────────────────────────────────────────
96+
$sw.Restart()
97+
$isAuth = Get-AuthorisedRequest -Uri $GraphUrl -TenantID $TenantFilter
98+
$sw.Stop()
99+
Add-Timing 'GetAuthorisedRequest' "Authorised=$isAuth" $sw.Elapsed.TotalMilliseconds
100+
101+
# ── 6. Get-GraphToken ───────────────────────────────────────────────
102+
$sw.Restart()
103+
$headers = Get-GraphToken -tenantid $TenantFilter -scope $scope
104+
$sw.Stop()
105+
Add-Timing 'GetGraphToken' "HasAuth=$([bool]$headers.Authorization)" $sw.Elapsed.TotalMilliseconds
106+
107+
# ── 7. Raw Invoke-RestMethod (baseline — no wrapper overhead) ──────
108+
$sw.Restart()
109+
$directRequest = @{
110+
Uri = $GraphUrl
111+
Method = 'GET'
112+
Headers = $headers
113+
ContentType = 'application/json; charset=utf-8'
114+
}
115+
if ($coreState.GraphSession) {
116+
$graphSess = & (Get-Module CIPPCore) { $script:GraphWebSession }
117+
if ($graphSess) { $directRequest.WebSession = $graphSess }
118+
}
119+
$directResult = Invoke-RestMethod @directRequest
120+
$directCount = if ($directResult.value) { $directResult.value.Count } else { 1 }
121+
$sw.Stop()
122+
Add-Timing 'DirectInvokeRestMethod' "ResultCount=$directCount" $sw.Elapsed.TotalMilliseconds
123+
124+
# ── 8. Get-GraphRequestList (full wrapper) ──────────────────────────
125+
$sw.Restart()
126+
$ManualPagination = [System.Boolean]$Request.Query.manualPagination
127+
$listParams = @{
128+
TenantFilter = $TenantFilter
129+
Endpoint = $Request.Query.Endpoint
130+
Parameters = ($Parameters.Clone())
131+
ManualPagination = $ManualPagination
132+
SkipCache = $true
133+
}
134+
$listParams.Parameters.Remove('$count')
135+
$listResult = Get-GraphRequestList @listParams
136+
$listCount = if ($listResult -is [array]) { $listResult.Count } else { 1 }
137+
$sw.Stop()
138+
Add-Timing 'GetGraphRequestList' "ResultCount=$listCount" $sw.Elapsed.TotalMilliseconds
139+
140+
# ── 9. CIPPCore state after calls ───────────────────────────────────
141+
$sw.Restart()
142+
$coreStateAfter = & (Get-Module CIPPCore) {
143+
$keyNull = $args[0]
144+
$now = [int](Get-Date -UFormat %s -Millisecond 0)
145+
$cached = $script:AccessTokens.$keyNull
146+
@{
147+
TokenCached = [bool]($cached -and $now -lt $cached.expires_on)
148+
CacheCount = if ($script:AccessTokens) { $script:AccessTokens.Count } else { 0 }
149+
CacheKeys = if ($script:AccessTokens) { @($script:AccessTokens.Keys) } else { @() }
150+
LoginSession = [bool]$script:LoginWebSession
151+
GraphSession = [bool]$script:GraphWebSession
152+
}
153+
} $TokenKeyNull
154+
$sw.Stop()
155+
Add-Timing 'CoreStateAfter' "TokenCached=$($coreStateAfter.TokenCached) CacheCount=$($coreStateAfter.CacheCount) LoginSession=$($coreStateAfter.LoginSession) GraphSession=$($coreStateAfter.GraphSession)" $sw.Elapsed.TotalMilliseconds
156+
157+
$OverallSw.Stop()
158+
Add-Timing 'Total' 'End-to-end' $OverallSw.Elapsed.TotalMilliseconds
159+
160+
$ProfileData = [PSCustomObject]@{
161+
Tenant = $TenantFilter
162+
Endpoint = $Request.Query.Endpoint
163+
GraphUrl = $GraphUrl
164+
CoreStateBefore = $coreState
165+
CoreStateAfter = $coreStateAfter
166+
Timings = @($Timings)
167+
}
168+
169+
return [HttpResponseContext]@{
170+
StatusCode = 200
171+
Body = $ProfileData
172+
}
173+
}

0 commit comments

Comments
 (0)