Skip to content

Commit eb1d452

Browse files
authored
Merge branch 'dev' into dev
2 parents 874ddfd + 0e1cd79 commit eb1d452

File tree

52 files changed

+10848
-332
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+10848
-332
lines changed

Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ function New-CippAuditLogSearch {
147147
if ($IPAddressFilters) {
148148
$SearchParams.ipAddressFilters = @($IPAddressFilters)
149149
}
150-
if ($ObjectIdFilterss) {
150+
if ($ObjectIdFilters) {
151151
$SearchParams.objectIdFilters = @($ObjectIdFilters)
152152
}
153153
if ($AdministrativeUnitFilters) {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
function Get-CippAllowedPermissions {
2+
<#
3+
.SYNOPSIS
4+
Retrieves the allowed permissions for the current user.
5+
6+
.DESCRIPTION
7+
This function retrieves the allowed permissions for the current user based on their role and the configured permissions in the CIPP system.
8+
For admin/superadmin users, permissions are computed from base role include/exclude rules.
9+
For editor/readonly users, permissions start from base role and are restricted by custom roles.
10+
11+
.PARAMETER UserRoles
12+
Array of user roles to compute permissions for.
13+
14+
.OUTPUTS
15+
Returns a list of allowed permissions for the current user.
16+
#>
17+
18+
[CmdletBinding()]
19+
param(
20+
[Parameter(Mandatory = $true)]
21+
[string[]]$UserRoles
22+
)
23+
24+
# Get all available permissions and base roles configuration
25+
26+
$CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase
27+
$CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent
28+
$Version = (Get-Content -Path $CIPPRoot\version_latest.txt).trim()
29+
$BaseRoles = Get-Content -Path $CIPPRoot\Config\cipp-roles.json | ConvertFrom-Json
30+
$DefaultRoles = @('superadmin', 'admin', 'editor', 'readonly', 'anonymous', 'authenticated')
31+
32+
$AllPermissionCacheTable = Get-CIPPTable -tablename 'cachehttppermissions'
33+
$AllPermissionsRow = Get-CIPPAzDataTableEntity @AllPermissionCacheTable -Filter "PartitionKey eq 'HttpFunctions' and RowKey eq 'HttpFunctions' and Version eq '$($Version)'"
34+
35+
if (-not $AllPermissionsRow) {
36+
$AllPermissions = Get-CIPPHttpFunctions -ByRole | Select-Object -ExpandProperty Permission
37+
$Entity = @{
38+
PartitionKey = 'HttpFunctions'
39+
RowKey = 'HttpFunctions'
40+
Version = [string]$Version
41+
Permissions = [string]($AllPermissions | ConvertTo-Json -Compress)
42+
}
43+
Add-CIPPAzDataTableEntity @AllPermissionCacheTable -Entity $Entity -Force
44+
} else {
45+
$AllPermissions = $AllPermissionsRow.Permissions | ConvertFrom-Json
46+
}
47+
48+
$AllowedPermissions = [System.Collections.Generic.List[string]]::new()
49+
50+
# Determine user's primary base role (highest priority first)
51+
$BaseRole = $null
52+
$PrimaryRole = $null
53+
54+
if ($UserRoles -contains 'superadmin') {
55+
$PrimaryRole = 'superadmin'
56+
} elseif ($UserRoles -contains 'admin') {
57+
$PrimaryRole = 'admin'
58+
} elseif ($UserRoles -contains 'editor') {
59+
$PrimaryRole = 'editor'
60+
} elseif ($UserRoles -contains 'readonly') {
61+
$PrimaryRole = 'readonly'
62+
}
63+
64+
if ($PrimaryRole) {
65+
$BaseRole = $BaseRoles.PSObject.Properties | Where-Object { $_.Name -eq $PrimaryRole } | Select-Object -First 1
66+
}
67+
68+
# Get custom roles (non-default roles)
69+
$CustomRoles = $UserRoles | Where-Object { $DefaultRoles -notcontains $_ }
70+
71+
# For admin and superadmin: Compute permissions from base role include/exclude rules
72+
if ($PrimaryRole -in @('admin', 'superadmin')) {
73+
Write-Information "Computing permissions for $PrimaryRole using base role rules"
74+
75+
if ($BaseRole) {
76+
# Start with all permissions and apply include/exclude rules
77+
$BasePermissions = [System.Collections.Generic.List[string]]::new()
78+
79+
# Apply include rules
80+
foreach ($Include in $BaseRole.Value.include) {
81+
$MatchingPermissions = $AllPermissions | Where-Object { $_ -like $Include }
82+
foreach ($Permission in $MatchingPermissions) {
83+
if ($BasePermissions -notcontains $Permission) {
84+
$BasePermissions.Add($Permission)
85+
}
86+
}
87+
}
88+
89+
# Apply exclude rules
90+
foreach ($Exclude in $BaseRole.Value.exclude) {
91+
$ExcludedPermissions = $BasePermissions | Where-Object { $_ -like $Exclude }
92+
foreach ($Permission in $ExcludedPermissions) {
93+
$BasePermissions.Remove($Permission) | Out-Null
94+
}
95+
}
96+
97+
foreach ($Permission in $BasePermissions) {
98+
$AllowedPermissions.Add($Permission)
99+
}
100+
}
101+
}
102+
# For editor and readonly: Start with base role permissions and restrict with custom roles
103+
elseif ($PrimaryRole -in @('editor', 'readonly')) {
104+
Write-Information "Computing permissions for $PrimaryRole with custom role restrictions"
105+
106+
if ($BaseRole) {
107+
# Get base role permissions first
108+
$BasePermissions = [System.Collections.Generic.List[string]]::new()
109+
110+
# Apply include rules from base role
111+
foreach ($Include in $BaseRole.Value.include) {
112+
$MatchingPermissions = $AllPermissions | Where-Object { $_ -like $Include }
113+
foreach ($Permission in $MatchingPermissions) {
114+
if ($BasePermissions -notcontains $Permission) {
115+
$BasePermissions.Add($Permission)
116+
}
117+
}
118+
}
119+
120+
# Apply exclude rules from base role
121+
foreach ($Exclude in $BaseRole.Value.exclude) {
122+
$ExcludedPermissions = $BasePermissions | Where-Object { $_ -like $Exclude }
123+
foreach ($Permission in $ExcludedPermissions) {
124+
$BasePermissions.Remove($Permission) | Out-Null
125+
}
126+
}
127+
128+
# If custom roles exist, intersect with custom role permissions (restriction)
129+
if ($CustomRoles.Count -gt 0) {
130+
$CustomRolePermissions = [System.Collections.Generic.List[string]]::new()
131+
132+
foreach ($CustomRole in $CustomRoles) {
133+
try {
134+
$RolePermissions = Get-CIPPRolePermissions -RoleName $CustomRole
135+
foreach ($Permission in $RolePermissions.Permissions) {
136+
if ($null -ne $Permission -and $Permission -is [string] -and $CustomRolePermissions -notcontains $Permission) {
137+
$CustomRolePermissions.Add($Permission)
138+
}
139+
}
140+
} catch {
141+
Write-Warning "Failed to get permissions for custom role '$CustomRole': $($_.Exception.Message)"
142+
}
143+
}
144+
145+
# Restrict base permissions to only those allowed by custom roles
146+
$RestrictedPermissions = $BasePermissions | Where-Object { $CustomRolePermissions -contains $_ }
147+
foreach ($Permission in $RestrictedPermissions) {
148+
if ($null -ne $Permission -and $Permission -is [string]) {
149+
$AllowedPermissions.Add($Permission)
150+
}
151+
}
152+
} else {
153+
# No custom roles, use base role permissions
154+
foreach ($Permission in $BasePermissions) {
155+
if ($null -ne $Permission -and $Permission -is [string]) {
156+
$AllowedPermissions.Add($Permission)
157+
}
158+
}
159+
}
160+
}
161+
}
162+
# Handle users with only custom roles (no base role)
163+
elseif ($CustomRoles.Count -gt 0) {
164+
Write-Information 'Computing permissions for custom roles only'
165+
166+
foreach ($CustomRole in $CustomRoles) {
167+
try {
168+
$RolePermissions = Get-CIPPRolePermissions -RoleName $CustomRole
169+
foreach ($Permission in $RolePermissions.Permissions) {
170+
if ($null -ne $Permission -and $Permission -is [string] -and $AllowedPermissions -notcontains $Permission) {
171+
$AllowedPermissions.Add($Permission)
172+
}
173+
}
174+
} catch {
175+
Write-Warning "Failed to get permissions for custom role '$CustomRole': $($_.Exception.Message)"
176+
}
177+
}
178+
}
179+
180+
# Return sorted unique permissions
181+
return ($AllowedPermissions | Sort-Object -Unique)
182+
}

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

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,6 @@ function Test-CIPPAccess {
1515
# Check help for role
1616
$APIRole = $Help.Role
1717

18-
if ($APIRole -eq 'Public') {
19-
return $true
20-
}
21-
22-
# Get default roles from config
23-
$CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase
24-
$CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent
25-
$BaseRoles = Get-Content -Path $CIPPRoot\Config\cipp-roles.json | ConvertFrom-Json
26-
27-
if ($APIRole -eq 'Public') {
28-
return $true
29-
}
30-
3118
# Get default roles from config
3219
$CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase
3320
$CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent
@@ -38,11 +25,6 @@ function Test-CIPPAccess {
3825
return $true
3926
}
4027

41-
# Get default roles from config
42-
$CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase
43-
$CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent
44-
$BaseRoles = Get-Content -Path $CIPPRoot\Config\cipp-roles.json | ConvertFrom-Json
45-
4628
if ($Request.Headers.'x-ms-client-principal-idp' -eq 'aad' -and $Request.Headers.'x-ms-client-principal-name' -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') {
4729
$Type = 'APIClient'
4830
# Direct API Access
@@ -91,6 +73,22 @@ function Test-CIPPAccess {
9173
$CustomRoles = @('cipp-api')
9274
Write-Information "API Access: AppId=$($Request.Headers.'x-ms-client-principal-name'), IP=$IPAddress"
9375
}
76+
if ($Request.Params.CIPPEndpoint -eq 'me') {
77+
$Permissions = Get-CippAllowedPermissions -UserRoles $CustomRoles
78+
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
79+
StatusCode = [HttpStatusCode]::OK
80+
Body = (
81+
@{
82+
'clientPrincipal' = @{
83+
appId = $Request.Headers.'x-ms-client-principal-name'
84+
appRole = $CustomRoles
85+
}
86+
'permissions' = $Permissions
87+
} | ConvertTo-Json -Depth 5)
88+
})
89+
return
90+
}
91+
9492
} else {
9593
$Type = 'User'
9694
$User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json
@@ -103,9 +101,14 @@ function Test-CIPPAccess {
103101
#Write-Information ($User | ConvertTo-Json -Depth 5)
104102
# Return user permissions
105103
if ($Request.Params.CIPPEndpoint -eq 'me') {
104+
$Permissions = Get-CippAllowedPermissions -UserRoles $User.userRoles
106105
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
107106
StatusCode = [HttpStatusCode]::OK
108-
Body = (@{ 'clientPrincipal' = $User } | ConvertTo-Json -Depth 5)
107+
Body = (
108+
@{
109+
'clientPrincipal' = $User
110+
'permissions' = $Permissions
111+
} | ConvertTo-Json -Depth 5)
109112
})
110113
return
111114
}

Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function Push-DomainAnalyserDomain {
8383
# Setup Score Explanation
8484
$ScoreExplanation = [System.Collections.Generic.List[string]]::new()
8585

86-
# Check MX Record
86+
#Region MX Check
8787
$MXRecord = Read-MXRecord -Domain $Domain -ErrorAction Stop
8888

8989
$Result.ExpectedSPFRecord = $MXRecord.ExpectedInclude
@@ -106,8 +106,9 @@ function Push-DomainAnalyserDomain {
106106
} else {
107107
$Result.MailProvider = $MXRecord.MailProvider.Name
108108
}
109+
#EndRegion MX Check
109110

110-
# Get SPF Record
111+
#Region SPF Check
111112
try {
112113
$SPFRecord = Read-SpfRecord -Domain $Domain -ErrorAction Stop
113114
if ($SPFRecord.RecordCount -gt 0) {
@@ -126,21 +127,21 @@ function Push-DomainAnalyserDomain {
126127
Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message $Message -LogData (Get-CippException -Exception $_) -sev Error
127128
}
128129

129-
# Check SPF Record
130-
$Result.SPFPassAll = $false
131130

132131
# Check warning + fail counts to ensure all tests pass
133132
#$SPFWarnCount = $SPFRecord.ValidationWarns | Measure-Object | Select-Object -ExpandProperty Count
134133
$SPFFailCount = $SPFRecord.ValidationFails | Measure-Object | Select-Object -ExpandProperty Count
134+
$Result.SPFPassAll = $false
135135

136136
if ($SPFFailCount -eq 0) {
137137
$ScoreDomain += $Scores.SPFCorrectAll
138138
$Result.SPFPassAll = $true
139139
} else {
140140
$ScoreExplanation.Add('SPF record did not pass validation') | Out-Null
141141
}
142+
#EndRegion SPF Check
142143

143-
# Get DMARC Record
144+
#Region DMARC Check
144145
try {
145146
$DMARCPolicy = Read-DmarcPolicy -Domain $Domain -ErrorAction Stop
146147

@@ -188,8 +189,9 @@ function Push-DomainAnalyserDomain {
188189
Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message $Message -LogData (Get-CippException -Exception $_) -sev Error
189190
#return $Message
190191
}
192+
#EndRegion DMARC Check
191193

192-
# DNS Sec Check
194+
#Region DNS Sec Check
193195
try {
194196
$DNSSECResult = Test-DNSSEC -Domain $Domain -ErrorAction Stop
195197
$DNSSECFailCount = $DNSSECResult.ValidationFails | Measure-Object | Select-Object -ExpandProperty Count
@@ -206,8 +208,9 @@ function Push-DomainAnalyserDomain {
206208
Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message $Message -LogData (Get-CippException -Exception $_) -sev Error
207209
#return $Message
208210
}
211+
#EndRegion DNS Sec Check
209212

210-
# DKIM Check
213+
#Region DKIM Check
211214
try {
212215
$DkimParams = @{
213216
Domain = $Domain
@@ -241,7 +244,9 @@ function Push-DomainAnalyserDomain {
241244
Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message $Message -LogData (Get-CippException -Exception $_) -sev Error
242245
#return $Message
243246
}
247+
#EndRegion DKIM Check
244248

249+
#Region MSCNAME DKIM Records
245250
# Get Microsoft DKIM CNAME selector Records
246251
# Ugly, but i needed to create a scope/loop i could break out of without breaking the rest of the function
247252
foreach ($d in $Domain) {
@@ -250,7 +255,7 @@ function Push-DomainAnalyserDomain {
250255
if ($Result.DKIMEnabled -eq $true) {
251256
continue
252257
}
253-
# Test if its a onmicrosft.com domain, skip domain if it is
258+
# Test if its a onmicrosoft.com domain, skip domain if it is
254259
if ($Domain -match 'onmicrosoft.com') {
255260
continue
256261
}
@@ -264,28 +269,21 @@ function Push-DomainAnalyserDomain {
264269
}
265270
}
266271

272+
# Get the DKIM record from EXO. This is the only way to get the correct values for the MSCNAME records since the new format was introduced in May 2025.
273+
$DKIM = (New-ExoRequest -tenantid $Tenant.Tenant -cmdlet 'Get-DkimSigningConfig' -Select 'Domain,Selector1CNAME,Selector2CNAME') | Where-Object { $_.Domain -eq $Domain }
267274

268-
# Compute the DKIM CNAME records from $Tenant.InitialDomainName according to this logic: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-dkim-configure#syntax-for-dkim-cname-records
269-
# Test if it has a - in the domain name
270-
if ($Domain -like '*-*') {
271-
Write-Information 'Domain has a - in it. Got to query EXO for the right values'
272-
$DKIM = (New-ExoRequest -tenantid $Tenant.Tenant -cmdlet 'Get-DkimSigningConfig') | Where-Object { $_.Domain -eq $Domain } | Select-Object Domain, Selector1CNAME, Selector2CNAME
273-
274-
# If no DKIM signing record is found, create a new disabled one
275-
if ($null -eq $DKIM) {
276-
Write-Information 'No DKIM record found in EXO - Creating new signing'
277-
$NewDKIMSigningRequest = New-ExoRequest -tenantid $Tenant.Tenant -cmdlet 'New-DkimSigningConfig' -cmdParams @{ KeySize = 2048; DomainName = $Domain; Enabled = $false }
278-
$Selector1Value = $NewDKIMSigningRequest.Selector1CNAME
279-
$Selector2Value = $NewDKIMSigningRequest.Selector2CNAME
280-
} else {
281-
$Selector1Value = $DKIM.Selector1CNAME
282-
$Selector2Value = $DKIM.Selector2CNAME
283-
}
275+
# If no DKIM signing record is found, create a new disabled one
276+
if ($null -eq $DKIM) {
277+
Write-Information 'No DKIM record found in EXO - Creating new signing'
278+
$NewDKIMSigningRequest = New-ExoRequest -tenantid $Tenant.Tenant -cmdlet 'New-DkimSigningConfig' -cmdParams @{ KeySize = 2048; DomainName = $Domain; Enabled = $false }
279+
$Selector1Value = $NewDKIMSigningRequest.Selector1CNAME
280+
$Selector2Value = $NewDKIMSigningRequest.Selector2CNAME
284281
} else {
285-
$Selector1Value = "selector1-$($Domain -replace '\.', '-' )._domainkey.$($Tenant.InitialDomainName)"
286-
$Selector2Value = "selector2-$($Domain -replace '\.', '-' )._domainkey.$($Tenant.InitialDomainName)"
282+
$Selector1Value = $DKIM.Selector1CNAME
283+
$Selector2Value = $DKIM.Selector2CNAME
287284
}
288285

286+
289287
# Create the MSCNAME object
290288
$MSCNAMERecords = [PSCustomObject]@{
291289
Domain = $Domain
@@ -304,7 +302,7 @@ function Push-DomainAnalyserDomain {
304302
Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message "MS CNAME DKIM error: $($ErrorMessage.NormalizedError)" -LogData $ErrorMessage -sev Error
305303
}
306304
}
307-
305+
#EndRegion MSCNAME DKIM Records
308306
# Final Score
309307
$Result.Score = $ScoreDomain
310308
$Result.ScorePercentage = [int](($Result.Score / $Result.MaximumScore) * 100)

0 commit comments

Comments
 (0)