Skip to content

Commit 93283d8

Browse files
authored
Merge pull request #983 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 3e4421b + fad627f commit 93283d8

8 files changed

Lines changed: 31993 additions & 7145 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action
2+
# More GitHub Actions for Azure: https://github.com/Azure/actions
3+
4+
name: Build and deploy Powershell project to Azure Function App - cippjta72
5+
6+
on:
7+
push:
8+
branches:
9+
- dev
10+
workflow_dispatch:
11+
12+
env:
13+
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root
14+
15+
jobs:
16+
deploy:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: 'Checkout GitHub Action'
21+
uses: actions/checkout@v4
22+
23+
- name: 'Run Azure Functions Action'
24+
uses: Azure/functions-action@v1
25+
id: fa
26+
with:
27+
app-name: 'cippjta72'
28+
slot-name: 'Production'
29+
package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}
30+
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_1EBE9D73F9EC4528BA666FC934055536 }}
31+
sku: 'flexconsumption'
32+

Modules/CIPPCore/Public/Authentication/Set-CIPPSSOEasyAuth.ps1

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ function Set-CIPPSSOEasyAuth {
9595

9696
# Safely navigate to AAD registration
9797
if (-not $Current.ContainsKey('identityProviders') -or $null -eq $Current.identityProviders) { $Current.identityProviders = @{} }
98-
if (-not $Current.identityProviders.ContainsKey('azureActiveDirectory') -or $null -eq $Current.identityProviders.azureActiveDirectory) { $Current.identityProviders.azureActiveDirectory = @{} }
98+
if (-not $Current.identityProviders.ContainsKey('azureActiveDirectory') -or $null -eq $Current.identityProviders.azureActiveDirectory) { $Current.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{} -Force }
9999
$AAD = $Current.identityProviders.azureActiveDirectory
100100

101101
if (-not $AAD.ContainsKey('registration') -or $null -eq $AAD.registration) { $AAD.registration = @{} }
@@ -131,10 +131,10 @@ function Set-CIPPSSOEasyAuth {
131131
# Full overwrite: initial setup — build the entire authsettingsV2 from scratch
132132
$AuthConfig = @{
133133
properties = @{
134-
platform = @{ enabled = $true }
135-
globalValidation = @{
134+
platform = @{ enabled = $true }
135+
globalValidation = @{
136136
unauthenticatedClientAction = 'RedirectToLoginPage'
137-
redirectToProvider = 'azureactivedirectory'
137+
redirectToProvider = 'azureactivedirectory'
138138
excludedPaths = @(
139139
'/api/Public*'
140140
'/api/setup/health'
@@ -144,17 +144,17 @@ function Set-CIPPSSOEasyAuth {
144144
azureActiveDirectory = @{
145145
enabled = $true
146146
registration = $(if ($ImplicitAuth) {
147-
@{
148-
clientId = $AppId
149-
openIdIssuer = $IssuerUrl
150-
}
151-
} else {
152-
@{
153-
clientId = $AppId
154-
clientSecretSettingName = 'AUTH_SECRET'
155-
openIdIssuer = $IssuerUrl
156-
}
157-
})
147+
@{
148+
clientId = $AppId
149+
openIdIssuer = $IssuerUrl
150+
}
151+
} else {
152+
@{
153+
clientId = $AppId
154+
clientSecretSettingName = 'AUTH_SECRET'
155+
openIdIssuer = $IssuerUrl
156+
}
157+
})
158158
validation = @{
159159
allowedAudiences = @("api://$AppId")
160160
defaultAuthorizationPolicy = @{
@@ -164,7 +164,7 @@ function Set-CIPPSSOEasyAuth {
164164
}
165165
}
166166
}
167-
login = @{
167+
login = @{
168168
tokenStore = @{
169169
enabled = $true
170170
tokenRefreshExtensionHours = 72

Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function Set-CippApiAuth {
4141
# The env var has the raw config (identityProviders at top level, no properties wrapper)
4242
# Safely navigate/create the full path — any level may be null
4343
if (-not $Current.ContainsKey('identityProviders') -or $null -eq $Current.identityProviders) { $Current.identityProviders = @{} }
44-
if (-not $Current.identityProviders.ContainsKey('azureActiveDirectory') -or $null -eq $Current.identityProviders.azureActiveDirectory) { $Current.identityProviders.azureActiveDirectory = @{} }
44+
if (-not $Current.identityProviders.ContainsKey('azureActiveDirectory') -or $null -eq $Current.identityProviders.azureActiveDirectory) { $Current.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{} -Force }
4545

4646
$AAD = $Current.identityProviders.azureActiveDirectory
4747
Write-Information "[ApiAuth] AAD keys: $($AAD.Keys -join ', ')"
@@ -89,7 +89,7 @@ function Set-CippApiAuth {
8989
$PutUri = "$BaseUri/config/authsettingsV2?api-version=2020-06-01"
9090
$PutResult = New-CIPPAzRestRequest -Uri $PutUri -Method PUT -Body $PutBody -ContentType 'application/json' -ErrorAction Stop
9191
Write-Information "[ApiAuth] PUT result: $($PutResult | ConvertTo-Json -Depth 10 -Compress)"
92-
Write-Information "[ApiAuth] Updated EasyAuth successfully"
92+
Write-Information '[ApiAuth] Updated EasyAuth successfully'
9393
}
9494
} else {
9595
# Full overwrite path (no SSO EasyAuth config to preserve)
@@ -106,7 +106,7 @@ function Set-CippApiAuth {
106106
if (!$ClientIds) { $ClientIds = @() }
107107

108108
if (($ClientIds | Measure-Object).Count -gt 0) {
109-
$AuthSettings.properties.identityProviders.azureActiveDirectory = @{
109+
$AuthSettings.properties.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{
110110
enabled = $true
111111
registration = @{
112112
clientId = $ClientIds[0] ?? $ClientIds
@@ -118,24 +118,25 @@ function Set-CippApiAuth {
118118
allowedApplications = @($ClientIds)
119119
}
120120
}
121-
}
121+
} -Force
122122
} else {
123-
$AuthSettings.properties.identityProviders.azureActiveDirectory = @{
123+
#Replaced with add-member -force
124+
$AuthSettings.properties.identityProviders | Add-Member -MemberType NoteProperty -Name 'azureActiveDirectory' -Value @{
124125
enabled = $false
125126
registration = @{}
126127
validation = @{}
127-
}
128+
} -Force
128129
}
129130

130-
$AuthSettings.properties.globalValidation = @{
131+
$AuthSettings.properties | Add-Member -MemberType NoteProperty -Name 'globalValidation' -Value @{
131132
unauthenticatedClientAction = 'Return401'
132-
}
133-
$AuthSettings.properties.login = @{
133+
} -Force
134+
$AuthSettings.properties | Add-Member -MemberType NoteProperty -Name 'login' -Value @{
134135
tokenStore = @{
135136
enabled = $true
136137
tokenRefreshExtensionHours = 72
137138
}
138-
}
139+
} -Force
139140

140141
if ($PSCmdlet.ShouldProcess('Update auth settings')) {
141142
$putUri = "$BaseUri/config/authsettingsV2?api-version=2020-06-01"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
function Get-CippMcpSpec {
2+
<#
3+
.SYNOPSIS
4+
Loads and caches the CIPP OpenAPI specification (openapi.json).
5+
.DESCRIPTION
6+
Returns the parsed OpenAPI document used to project the MCP tool list. The result
7+
is cached per worker runspace; pass -Force to reload (e.g. after the spec is
8+
regenerated). Not an HTTP entrypoint.
9+
.FUNCTIONALITY
10+
Internal
11+
#>
12+
[CmdletBinding()]
13+
param([switch]$Force)
14+
15+
if ($script:CippMcpSpec -and -not $Force) {
16+
return $script:CippMcpSpec
17+
}
18+
19+
$Root = $env:CIPPRootPath
20+
if (-not $Root -or -not (Test-Path (Join-Path $Root 'openapi.json'))) {
21+
# Fallback: walk up from this module until openapi.json is found.
22+
$Root = $PSScriptRoot
23+
while ($Root -and -not (Test-Path (Join-Path $Root 'openapi.json'))) {
24+
$Parent = Split-Path $Root -Parent
25+
if (-not $Parent -or $Parent -eq $Root) { $Root = $null; break }
26+
$Root = $Parent
27+
}
28+
}
29+
30+
$SpecPath = if ($Root) { Join-Path $Root 'openapi.json' } else { $null }
31+
if (-not $SpecPath -or -not (Test-Path $SpecPath)) {
32+
throw [pscustomobject]@{ code = -32603; message = 'OpenAPI spec (openapi.json) not found; cannot project MCP tools.' }
33+
}
34+
35+
# -AsHashtable is required: the spec contains objects with case-differing keys
36+
# (e.g. displayName / DisplayName) which a case-insensitive PSCustomObject cannot hold.
37+
$script:CippMcpSpec = [System.IO.File]::ReadAllText($SpecPath) | ConvertFrom-Json -AsHashtable -Depth 100
38+
return $script:CippMcpSpec
39+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
function Get-CippMcpToolList {
2+
<#
3+
.SYNOPSIS
4+
Projects the CIPP OpenAPI spec into the read-only MCP tool list.
5+
.DESCRIPTION
6+
Returns every operation whose x-cipp-role ends in '.Read' (never '.ReadWrite') as an
7+
MCP tool definition: name (the API endpoint), description, inputSchema (JSON Schema
8+
built from the operation's query parameters / request body with $ref inlined), and
9+
read-only annotations. Cached per worker; pass -Force to rebuild. Not an entrypoint.
10+
The spec is consumed as nested hashtables (Get-CippMcpSpec uses -AsHashtable).
11+
.FUNCTIONALITY
12+
Internal
13+
#>
14+
[CmdletBinding()]
15+
param([switch]$Force)
16+
17+
if ($script:CippMcpToolListCache -and -not $Force) {
18+
return $script:CippMcpToolListCache
19+
}
20+
21+
$Spec = Get-CippMcpSpec
22+
$Tools = [System.Collections.Generic.List[object]]::new()
23+
24+
foreach ($PathEntry in $Spec['paths'].GetEnumerator()) {
25+
$Endpoint = $PathEntry.Key -replace '^/api/', ''
26+
27+
# Never expose the MCP transport itself as a tool.
28+
if ($Endpoint -eq 'ExecMcp') { continue }
29+
30+
foreach ($MethodEntry in $PathEntry.Value.GetEnumerator()) {
31+
$Method = [string]$MethodEntry.Key
32+
if ($Method -notin @('get', 'post')) { continue }
33+
34+
$Op = $MethodEntry.Value
35+
$Role = $Op['x-cipp-role']
36+
37+
# Read-only surface only.
38+
if (-not $Role -or $Role -notmatch '\.Read$') { continue }
39+
40+
# Defensive backstop: never expose an endpoint whose name implies a mutation,
41+
# even if its x-cipp-role is mislabeled '.Read' (e.g. AddTestReport, EditIntunePolicy).
42+
if ($Endpoint -match '^(Add|Set|Remove|Delete|Edit|New|Update|Disable|Enable|Reset|Revoke|Push|Clear|Start|Stop|Rename|Move|Copy)') { continue }
43+
44+
$Properties = [ordered]@{}
45+
$RequiredList = [System.Collections.Generic.List[string]]::new()
46+
47+
# Query / path parameters.
48+
foreach ($ParamRaw in @($Op['parameters'])) {
49+
if (-not $ParamRaw) { continue }
50+
$Param = Resolve-CippMcpNode -Node $ParamRaw -Spec $Spec
51+
if ($Param['in'] -notin @('query', 'path')) { continue }
52+
$Schema = if ($Param['schema']) { $Param['schema'] } else { @{ type = 'string' } }
53+
$Properties[[string]$Param['name']] = $Schema
54+
if ($Param['required']) { $RequiredList.Add([string]$Param['name']) }
55+
}
56+
57+
# Request body (uncommon for reads; included for completeness).
58+
if ($Op['requestBody'] -and $Op['requestBody']['content'] -and $Op['requestBody']['content']['application/json']) {
59+
$BodySchema = Resolve-CippMcpNode -Node $Op['requestBody']['content']['application/json']['schema'] -Spec $Spec
60+
if ($BodySchema -and $BodySchema['properties']) {
61+
foreach ($BodyProp in $BodySchema['properties'].GetEnumerator()) {
62+
$Properties[[string]$BodyProp.Key] = $BodyProp.Value
63+
}
64+
foreach ($Req in @($BodySchema['required'])) { if ($Req) { $RequiredList.Add([string]$Req) } }
65+
}
66+
}
67+
68+
$InputSchema = [ordered]@{
69+
type = 'object'
70+
properties = $Properties
71+
}
72+
if ($RequiredList.Count -gt 0) {
73+
$InputSchema['required'] = @($RequiredList | Select-Object -Unique)
74+
}
75+
76+
$Tools.Add([ordered]@{
77+
name = $Endpoint
78+
description = Get-CippMcpDescription -Operation $Op
79+
inputSchema = $InputSchema
80+
annotations = [ordered]@{ title = $Endpoint; readOnlyHint = $true }
81+
})
82+
}
83+
}
84+
85+
$script:CippMcpToolListCache = $Tools
86+
return $Tools
87+
}
88+
89+
function Resolve-CippMcpNode {
90+
# Deep-resolves a parsed OpenAPI node (hashtable/array/scalar), inlining any $ref. Internal helper.
91+
param($Node, $Spec, [int]$Depth = 0, [string[]]$Seen = @())
92+
93+
if ($null -eq $Node) { return $null }
94+
if ($Depth -gt 15) { return @{ type = 'object' } }
95+
if ($Node -is [string] -or $Node -is [valuetype]) { return $Node }
96+
97+
if ($Node -is [System.Collections.IDictionary]) {
98+
if ($Node.Contains('$ref')) {
99+
$Ref = [string]$Node['$ref']
100+
if ($Seen -contains $Ref) { return [ordered]@{ type = 'object'; description = 'recursive reference omitted' } }
101+
$Target = Resolve-CippMcpRef -Ref $Ref -Spec $Spec
102+
return Resolve-CippMcpNode -Node $Target -Spec $Spec -Depth ($Depth + 1) -Seen ($Seen + $Ref)
103+
}
104+
$Out = [ordered]@{}
105+
foreach ($Entry in $Node.GetEnumerator()) {
106+
if ($Entry.Key -eq '$ref') { continue }
107+
$Out[[string]$Entry.Key] = Resolve-CippMcpNode -Node $Entry.Value -Spec $Spec -Depth ($Depth + 1) -Seen $Seen
108+
}
109+
return $Out
110+
}
111+
112+
if ($Node -is [System.Collections.IEnumerable]) {
113+
return @($Node | ForEach-Object { Resolve-CippMcpNode -Node $_ -Spec $Spec -Depth ($Depth + 1) -Seen $Seen })
114+
}
115+
116+
return $Node
117+
}
118+
119+
function Resolve-CippMcpRef {
120+
# Resolves a JSON pointer like '#/components/parameters/tenantFilter' against the spec. Internal helper.
121+
param([string]$Ref, $Spec)
122+
123+
$Segments = $Ref.TrimStart('#') -split '/' | Where-Object { $_ -ne '' }
124+
$Node = $Spec
125+
foreach ($Seg in $Segments) {
126+
$Key = $Seg -replace '~1', '/' -replace '~0', '~'
127+
if ($Node -is [System.Collections.IDictionary] -and $Node.Contains($Key)) {
128+
$Node = $Node[$Key]
129+
} else {
130+
return $null
131+
}
132+
}
133+
return $Node
134+
}
135+
136+
function Get-CippMcpDescription {
137+
# Cleans the operation description (strips leaked PowerShell help) and prefixes the tag. Internal helper.
138+
param($Operation)
139+
140+
$Desc = [string]$Operation['description']
141+
$Desc = $Desc -replace '(?s)\s*#>.*$', ''
142+
$Desc = $Desc -replace '(?s)\[CmdletBinding.*$', ''
143+
$Desc = $Desc.Trim()
144+
if ([string]::IsNullOrWhiteSpace($Desc)) { $Desc = [string]$Operation['summary'] }
145+
146+
$Tag = @($Operation['tags'])[0]
147+
if ($Tag -and $Tag -ne 'Uncategorized') { $Desc = "[$Tag] $Desc" }
148+
return $Desc
149+
}

0 commit comments

Comments
 (0)