Skip to content

Commit 8b49e52

Browse files
authored
Merge pull request #623 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 09735ec + b2b3b7b commit 8b49e52

File tree

6 files changed

+254
-21
lines changed

6 files changed

+254
-21
lines changed

.github/workflows/dev_api.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,67 @@ jobs:
2525
uses: actions/checkout@v4
2626
with:
2727
persist-credentials: false
28+
29+
- name: Setup PowerShell module cache
30+
id: cacher
31+
uses: actions/cache@v3
32+
with:
33+
path: "~/.local/share/powershell/Modules"
34+
key: ${{ runner.os }}-ModuleBuilder
35+
36+
- name: Install ModuleBuilder
37+
if: steps.cacher.outputs.cache-hit != 'true'
38+
shell: pwsh
39+
run: |
40+
Set-PSRepository PSGallery -InstallationPolicy Trusted
41+
Install-Module ModuleBuilder -AllowClobber -Force
42+
43+
- name: Build CIPPCore Module
44+
shell: pwsh
45+
run: |
46+
$ModulePath = Join-Path $env:GITHUB_WORKSPACE "Modules/CIPPCore"
47+
$OutputPath = Join-Path $env:GITHUB_WORKSPACE "Output"
48+
49+
Write-Host "Building module from: $ModulePath"
50+
Write-Host "Output directory: $OutputPath"
51+
52+
# Generate function permissions before replacing the source module
53+
$ToolsPath = Join-Path $env:GITHUB_WORKSPACE "Tools"
54+
$ScriptPath = Join-Path $ToolsPath "Build-FunctionPermissions.ps1"
55+
pwsh -File $ScriptPath -ModulePath $ModulePath
56+
57+
# Build the module using ModuleBuilder
58+
Build-Module -SourcePath $ModulePath -OutputDirectory $OutputPath -Verbose
59+
60+
# Replace the source module with the built module
61+
Remove-Item -Path $ModulePath -Recurse -Force
62+
Copy-Item -Path (Join-Path $OutputPath "CIPPCore") -Destination $ModulePath -Recurse -Force
63+
64+
Write-Host "Module built and replaced successfully"
65+
66+
# Clean up output directory
67+
Remove-Item -Path $OutputPath -Recurse -Force
68+
69+
- name: Build CippExtensions Module
70+
shell: pwsh
71+
run: |
72+
$ModulePath = Join-Path $env:GITHUB_WORKSPACE "Modules/CippExtensions"
73+
$OutputPath = Join-Path $env:GITHUB_WORKSPACE "Output"
74+
75+
Write-Host "Building module from: $ModulePath"
76+
Write-Host "Output directory: $OutputPath"
77+
78+
# Build the module using ModuleBuilder
79+
Build-Module -SourcePath $ModulePath -OutputDirectory $OutputPath -Verbose
80+
81+
# Replace the source module with the built module
82+
Remove-Item -Path $ModulePath -Recurse -Force
83+
Copy-Item -Path (Join-Path $OutputPath "CippExtensions") -Destination $ModulePath -Recurse -Force
84+
85+
Write-Host "Module built and replaced successfully"
86+
87+
# Clean up output directory
88+
Remove-Item -Path $OutputPath -Recurse -Force
2889
2990
- name: Login to Azure
3091
uses: azure/login@v2

Modules/CIPPCore/Public/Authentication/Get-CIPPAzIdentityToken.ps1

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ function Get-CIPPAzIdentityToken {
33
.SYNOPSIS
44
Get the Azure Identity token for Managed Identity
55
.DESCRIPTION
6-
This function retrieves the Azure Identity token using the Managed Identity endpoint for the specified resource
6+
This function retrieves the Azure Identity token using the Managed Identity endpoint for the specified resource.
7+
Tokens are cached per resource URL until expiration to reduce redundant API calls.
78
.PARAMETER ResourceUrl
89
The Azure resource URL to get a token for. Defaults to 'https://management.azure.com/' for Azure Resource Manager.
910
@@ -12,6 +13,8 @@ function Get-CIPPAzIdentityToken {
1213
- https://vault.azure.net (Azure Key Vault)
1314
- https://api.loganalytics.io (Log Analytics / Application Insights)
1415
- https://storage.azure.com/ (Azure Storage)
16+
.PARAMETER SkipCache
17+
Force a new token to be fetched, bypassing the cache.
1518
.EXAMPLE
1619
Get-CIPPAzIdentityToken
1720
Gets a token for Azure Resource Manager
@@ -25,7 +28,8 @@ function Get-CIPPAzIdentityToken {
2528
[CmdletBinding()]
2629
param(
2730
[Parameter(Mandatory = $false)]
28-
[string]$ResourceUrl = 'https://management.azure.com/'
31+
[string]$ResourceUrl = 'https://management.azure.com/',
32+
[switch]$SkipCache
2933
)
3034

3135
$Endpoint = $env:IDENTITY_ENDPOINT
@@ -35,12 +39,40 @@ function Get-CIPPAzIdentityToken {
3539
throw 'Managed Identity environment variables (IDENTITY_ENDPOINT/IDENTITY_HEADER) not found. Is Managed Identity enabled on the Function App?'
3640
}
3741

38-
$EncodedResource = [System.Uri]::EscapeDataString($ResourceUrl)
39-
$TokenUri = "$($Endpoint)?resource=$EncodedResource&api-version=2019-08-01"
40-
$Headers = @{
41-
'X-IDENTITY-HEADER' = $Secret
42-
}
42+
# Build cache key from resource URL
43+
$TokenKey = "ManagedIdentity-$ResourceUrl"
44+
45+
try {
46+
# Check if cached token exists and is still valid
47+
if ($script:ManagedIdentityTokens.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:ManagedIdentityTokens.$TokenKey.expires_on -and $SkipCache -ne $true) {
48+
return $script:ManagedIdentityTokens.$TokenKey.access_token
49+
}
50+
51+
# Get new token
52+
$EncodedResource = [System.Uri]::EscapeDataString($ResourceUrl)
53+
$TokenUri = "$($Endpoint)?resource=$EncodedResource&api-version=2019-08-01"
54+
$Headers = @{
55+
'X-IDENTITY-HEADER' = $Secret
56+
}
57+
58+
$TokenResponse = Invoke-RestMethod -Method Get -Headers $Headers -Uri $TokenUri -ErrorAction Stop
59+
60+
# Calculate expiration time
61+
$ExpiresOn = [int](Get-Date -UFormat %s -Millisecond 0) + $TokenResponse.expires_in
4362

44-
$TokenResponse = Invoke-RestMethod -Method Get -Headers $Headers -Uri $TokenUri
45-
return $TokenResponse.access_token
63+
# Store in cache (initialize synchronized hash table if needed)
64+
if (-not $script:ManagedIdentityTokens) {
65+
$script:ManagedIdentityTokens = [HashTable]::Synchronized(@{})
66+
}
67+
68+
# Add expires_on to token response for tracking
69+
Add-Member -InputObject $TokenResponse -NotePropertyName 'expires_on' -NotePropertyValue $ExpiresOn -Force
70+
71+
# Cache the token
72+
$script:ManagedIdentityTokens.$TokenKey = $TokenResponse
73+
74+
return $TokenResponse.access_token
75+
} catch {
76+
throw "Failed to get managed identity token for resource '$ResourceUrl': $($_.Exception.Message)"
77+
}
4678
}

Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ function New-CIPPAPIConfig {
1919

2020
try {
2121
if ($AppId) {
22-
$APIApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appid='$($AppId)')" -NoAuthCheck $true
22+
$APIApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appid='$($AppId)')" -NoAuthCheck $true -AsApp $true
23+
Write-Information "Found existing app with AppId $AppId"
2324
} else {
2425
$CreateBody = @{
2526
api = @{

Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,50 @@ function Test-CIPPAccess {
1212
# Get function help
1313
$FunctionName = 'Invoke-{0}' -f $Request.Params.CIPPEndpoint
1414

15+
$SwPermissions = [System.Diagnostics.Stopwatch]::StartNew()
16+
if (-not $global:CIPPFunctionPermissions) {
17+
$CIPPCoreModule = Get-Module -Name CIPPCore
18+
if ($CIPPCoreModule) {
19+
$PermissionsFileJson = Join-Path $CIPPCoreModule.ModuleBase 'lib' 'data' 'function-permissions.json'
20+
21+
if (Test-Path $PermissionsFileJson) {
22+
try {
23+
$jsonData = Get-Content -Path $PermissionsFileJson -Raw | ConvertFrom-Json -AsHashtable
24+
$global:CIPPFunctionPermissions = [System.Collections.Hashtable]::new([StringComparer]::OrdinalIgnoreCase)
25+
foreach ($key in $jsonData.Keys) {
26+
$global:CIPPFunctionPermissions[$key] = $jsonData[$key]
27+
}
28+
Write-Information "Loaded $($global:CIPPFunctionPermissions.Count) function permissions from JSON cache"
29+
} catch {
30+
Write-Warning "Failed to load function permissions from JSON: $($_.Exception.Message)"
31+
}
32+
}
33+
}
34+
}
35+
$SwPermissions.Stop()
36+
$AccessTimings['FunctionPermissions'] = $SwPermissions.Elapsed.TotalMilliseconds
37+
1538
if ($FunctionName -ne 'Invoke-me') {
1639
$swHelp = [System.Diagnostics.Stopwatch]::StartNew()
17-
try {
18-
$Help = Get-Help $FunctionName -ErrorAction Stop
19-
} catch {
20-
Write-Warning "Function '$FunctionName' not found"
40+
if ($global:CIPPFunctionPermissions -and $global:CIPPFunctionPermissions.ContainsKey($FunctionName)) {
41+
$PermissionData = $global:CIPPFunctionPermissions[$FunctionName]
42+
$APIRole = $PermissionData['Role']
43+
$Functionality = $PermissionData['Functionality']
44+
Write-Information "Loaded function permission data from cache for '$FunctionName': Role='$APIRole', Functionality='$Functionality'"
45+
} else {
46+
try {
47+
$Help = Get-Help $FunctionName -ErrorAction Stop
48+
$APIRole = $Help.Role
49+
$Functionality = $Help.Functionality
50+
Write-Information "Loaded function permission data via Get-Help for '$FunctionName': Role='$APIRole', Functionality='$Functionality'"
51+
} catch {
52+
Write-Warning "Function '$FunctionName' not found"
53+
}
2154
}
2255
$swHelp.Stop()
2356
$AccessTimings['GetHelp'] = $swHelp.Elapsed.TotalMilliseconds
2457
}
2558

26-
# Check help for role
27-
$APIRole = $Help.Role
28-
2959
# Get default roles from config
3060
$swRolesLoad = [System.Diagnostics.Stopwatch]::StartNew()
3161
$CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase
@@ -367,7 +397,7 @@ function Test-CIPPAccess {
367397
if (!$APIAllowed) {
368398
throw "Access to this CIPP API endpoint is not allowed, you do not have the required permission: $APIRole"
369399
}
370-
if (!$TenantAllowed -and $Help.Functionality -notmatch 'AnyTenant') {
400+
if (!$TenantAllowed -and $Functionality -notmatch 'AnyTenant') {
371401
throw 'Access to this tenant is not allowed'
372402
} else {
373403
return $true
@@ -405,12 +435,12 @@ function Test-CIPPAccess {
405435
}
406436
}
407437

408-
if (!$TenantAllowed -and $Help.Functionality -notmatch 'AnyTenant') {
438+
if (!$TenantAllowed -and $Functionality -notmatch 'AnyTenant') {
409439

410440
if (!$APIAllowed) {
411441
throw "Access to this CIPP API endpoint is not allowed, you do not have the required permission: $APIRole"
412442
}
413-
if (!$TenantAllowed -and $Help.Functionality -notmatch 'AnyTenant') {
443+
if (!$TenantAllowed -and $Functionality -notmatch 'AnyTenant') {
414444
Write-Information "Tenant not allowed: $TenantFilter"
415445

416446
throw 'Access to this tenant is not allowed'

Modules/CIPPCore/Public/Send-CIPPAlert.ps1

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ function Send-CIPPAlert {
100100

101101
if ($Type -eq 'webhook') {
102102
Write-Information 'Trying to send webhook'
103+
104+
$ExtensionTable = Get-CIPPTable -TableName Extensionsconfig
105+
$Configuration = ((Get-CIPPAzDataTableEntity @ExtensionTable).config | ConvertFrom-Json)
106+
107+
if ($Configuration.CFZTNA.WebhookEnabled -eq $true -and $Configuration.CFZTNA.Enabled -eq $true) {
108+
$CFAPIKey = Get-ExtensionAPIKey -Extension 'CFZTNA'
109+
$Headers = @{'CF-Access-Client-Id' = $Configuration.CFZTNA.ClientId; 'CF-Access-Client-Secret' = "$CFAPIKey" }
110+
Write-Information 'CF-Access-Client-Id and CF-Access-Client-Secret headers added to webhook API request'
111+
} else {
112+
$Headers = $null
113+
}
114+
103115
$JSONBody = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $JSONContent -EscapeForJson
104116
try {
105117
if (![string]::IsNullOrWhiteSpace($Config.webhook) -or ![string]::IsNullOrWhiteSpace($AltWebhook)) {
@@ -124,7 +136,16 @@ function Send-CIPPAlert {
124136
Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $JSONBody
125137
}
126138
default {
127-
Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $JSONContent
139+
$RestMethod = @{
140+
Uri = $webhook
141+
Method = 'POST'
142+
ContentType = 'application/json'
143+
Body = $JSONContent
144+
}
145+
if ($Headers) {
146+
$RestMethod['Headers'] = $Headers
147+
}
148+
Invoke-RestMethod @RestMethod
128149
}
129150
}
130151
}
@@ -137,6 +158,7 @@ function Send-CIPPAlert {
137158
$ErrorMessage = Get-CippException -Exception $_
138159
Write-Information "Could not send alerts to webhook: $($ErrorMessage.NormalizedError)"
139160
Write-LogMessage -API 'Webhook Alerts' -message "Could not send alerts to webhook: $($ErrorMessage.NormalizedError)" -tenant $TenantFilter -sev error -LogData $ErrorMessage
161+
return "Could not send alerts to webhook: $($ErrorMessage.NormalizedError)"
140162
}
141163
}
142164

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
param(
2+
[string]$ModulePath = (Join-Path $PSScriptRoot '..' 'Modules' 'CIPPCore'),
3+
[string]$OutputPath,
4+
[string]$ModuleName
5+
)
6+
7+
$ErrorActionPreference = 'Stop'
8+
9+
function Resolve-ModuleImportPath {
10+
param(
11+
[Parameter(Mandatory = $true)][string]$Root,
12+
[Parameter(Mandatory = $true)][string]$Name
13+
)
14+
15+
$psd1 = Join-Path $Root "$Name.psd1"
16+
if (Test-Path $psd1) { return $psd1 }
17+
18+
$psm1 = Join-Path $Root "$Name.psm1"
19+
if (Test-Path $psm1) { return $psm1 }
20+
21+
throw "Module files not found for '$Name' in '$Root'. Expected $Name.psd1 or $Name.psm1."
22+
}
23+
24+
function Get-HelpProperty {
25+
param(
26+
[Parameter(Mandatory = $true)]$HelpObject,
27+
[Parameter(Mandatory = $true)][string]$PropertyName
28+
)
29+
30+
$property = $HelpObject.PSObject.Properties[$PropertyName]
31+
if ($property) { return $property.Value }
32+
return ''
33+
}
34+
35+
# Resolve defaults
36+
if (-not (Test-Path -Path $ModulePath)) {
37+
throw "ModulePath '$ModulePath' not found. Provide -ModulePath to the module root."
38+
}
39+
$ModulePath = (Resolve-Path -Path $ModulePath).ProviderPath
40+
if (-not $ModuleName) { $ModuleName = (Split-Path -Path $ModulePath -Leaf) }
41+
if (-not $OutputPath) {
42+
$defaultLibData = Join-Path $ModulePath 'lib' 'data' 'function-permissions.json'
43+
$OutputPath = if (Test-Path (Split-Path -Parent $defaultLibData)) { $defaultLibData } else { Join-Path $ModulePath 'function-permissions.json' }
44+
}
45+
46+
# Ensure destination directory exists
47+
$null = New-Item -ItemType Directory -Path (Split-Path -Parent $OutputPath) -Force
48+
49+
# Import target module so Get-Help can read Role/Functionality metadata
50+
$ModuleImportPath = Resolve-ModuleImportPath -Root $ModulePath -Name $ModuleName
51+
$normalizedImportPath = [System.IO.Path]::GetFullPath($ModuleImportPath)
52+
$loaded = Get-Module -Name $ModuleName | Where-Object { [System.IO.Path]::GetFullPath($_.Path) -eq $normalizedImportPath }
53+
if (-not $loaded) {
54+
Write-Host "Importing module '$ModuleName' from '$ModuleImportPath'"
55+
Import-Module -Name $ModuleImportPath -Force -ErrorAction Stop
56+
} else {
57+
Write-Host "Module '$ModuleName' already loaded from '$ModuleImportPath'; reusing existing session copy."
58+
}
59+
60+
$commands = Get-Command -Module $ModuleName -CommandType Function
61+
$permissions = [ordered]@{}
62+
63+
foreach ($command in $commands | Sort-Object -Property Name | Select-Object -Unique) {
64+
$help = Get-Help -Name $command.Name -ErrorAction SilentlyContinue
65+
if ($help) {
66+
$role = Get-HelpProperty -HelpObject $help -PropertyName 'Role'
67+
$functionality = Get-HelpProperty -HelpObject $help -PropertyName 'Functionality'
68+
} else {
69+
$role = ''
70+
$functionality = ''
71+
}
72+
73+
if ($role -or $functionality) {
74+
$permissions[$command.Name] = @{
75+
Role = $role
76+
Functionality = $functionality
77+
}
78+
} else {
79+
Write-Host "Skipping $($command.Name): no Role or Functionality metadata found."
80+
}
81+
}
82+
83+
# Depth 3 is sufficient for the flat hashtable of functions -> (Role, Functionality)
84+
$json = $permissions | ConvertTo-Json -Depth 3
85+
Set-Content -Path $OutputPath -Value $json -Encoding UTF8
86+
87+
Write-Host "Wrote permissions for $($permissions.Count) functions to $OutputPath"

0 commit comments

Comments
 (0)