Skip to content

Commit 521547c

Browse files
authored
Merge pull request #104 from KelvinTegelaar/master
[pull] master from KelvinTegelaar:master
2 parents 349f3e6 + 1434616 commit 521547c

29 files changed

Lines changed: 1022 additions & 639 deletions

Config/CIPPTimers.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,16 @@
237237
"TZOffset": true,
238238
"IsSystem": true
239239
},
240+
{
241+
"Id": "5e8a9b4c-2d6f-4a3e-b7c1-9d0e5f3a8b2c",
242+
"Command": "Start-IntuneReportExportOrchestrator",
243+
"Description": "Submit Intune report-export jobs ahead of nightly DB cache run",
244+
"Cron": "0 0 2 * * *",
245+
"Priority": 22,
246+
"RunOnProcessor": true,
247+
"TZOffset": true,
248+
"IsSystem": true
249+
},
240250
{
241251
"Id": "9a7f8e6d-5c4b-3a2d-1e0f-9b8c7d6e5f4a",
242252
"Command": "Start-CIPPDBCacheOrchestrator",

Config/version_latest.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ function Push-ExecScheduledCommand {
179179
return
180180
}
181181

182-
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) {
182+
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
183183
$State = 'Failed'
184184
Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$($Item.Command)" -Sev 'Warning'
185185
$Results = "Task blocked: The command '$($Item.Command)' is not permitted to run as a scheduled task."
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
function Push-IntuneReportExportSubmit {
2+
<#
3+
.SYNOPSIS
4+
Submits an Intune report export job for a tenant and stores the job id.
5+
.FUNCTIONALITY
6+
Entrypoint
7+
#>
8+
[CmdletBinding()]
9+
param($Item)
10+
11+
$TenantFilter = $Item.TenantFilter
12+
$ReportName = $Item.ReportName
13+
14+
if (-not $TenantFilter -or -not $ReportName) {
15+
Write-LogMessage -API 'IntuneReportExport' -message 'Missing TenantFilter or ReportName on activity item' -sev Error
16+
return @{ Status = 'Failed'; Reason = 'MissingInput' }
17+
}
18+
19+
try {
20+
$Select = switch ($ReportName) {
21+
'AppInvRawData' {
22+
@(
23+
'ApplicationKey', 'ApplicationName', 'ApplicationPublisher', 'ApplicationVersion',
24+
'DeviceId', 'DeviceName', 'OSDescription', 'OSVersion', 'Platform',
25+
'UserId', 'UserName', 'EmailAddress'
26+
)
27+
}
28+
default { throw "Unknown Intune report '$ReportName'" }
29+
}
30+
31+
$Body = @{
32+
reportName = $ReportName
33+
format = 'json'
34+
localizationType = 'replaceLocalizableValues'
35+
select = $Select
36+
} | ConvertTo-Json -Depth 5
37+
38+
$Job = New-GraphPOSTRequest `
39+
-uri 'https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs' `
40+
-tenantid $TenantFilter `
41+
-body $Body
42+
43+
if (-not $Job.id) { throw "Intune returned no job id for $ReportName" }
44+
45+
$JobsTable = Get-CIPPTable -tablename 'IntuneReportJobs'
46+
$Existing = Get-CIPPAzDataTableEntity @JobsTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$ReportName'"
47+
if ($Existing) {
48+
Remove-AzDataTableEntity @JobsTable -Entity $Existing -Force -ErrorAction SilentlyContinue
49+
}
50+
51+
Add-CIPPAzDataTableEntity @JobsTable -Entity @{
52+
PartitionKey = $TenantFilter
53+
RowKey = $ReportName
54+
JobId = $Job.id
55+
ReportName = $ReportName
56+
SubmittedAt = ([DateTime]::UtcNow).ToString('o')
57+
} -Force
58+
59+
Write-LogMessage -API 'IntuneReportExport' -tenant $TenantFilter -message "Submitted $ReportName export job $($Job.id)" -sev Info
60+
return @{ Status = 'Submitted'; JobId = $Job.id; ReportName = $ReportName; TenantFilter = $TenantFilter }
61+
} catch {
62+
$ErrorMessage = Get-CippException -Exception $_
63+
Write-LogMessage -API 'IntuneReportExport' -tenant $TenantFilter -message "Failed to submit $ReportName export: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
64+
return @{ Status = 'Failed'; ReportName = $ReportName; TenantFilter = $TenantFilter; Error = $ErrorMessage.NormalizedError }
65+
}
66+
}

Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -45,54 +45,37 @@ function Add-CIPPGroupMember {
4545
}
4646
$Users = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter
4747

48-
$SuccessfulUsers = [System.Collections.Generic.List[string]]::new()
49-
$FailedUsers = [System.Collections.Generic.List[string]]::new()
50-
51-
# Accept both human-readable labels (from Invoke-EditGroup / older callers) and
52-
# camelCase calculatedGroupType values (from the user template / add-edit-user form)
53-
$ExoGroupTypes = @('Distribution list', 'Distribution List', 'Mail-Enabled Security', 'distributionList', 'security')
54-
55-
if ($GroupType -in $ExoGroupTypes) {
48+
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
5649
$ExoBulkRequests = [System.Collections.Generic.List[object]]::new()
57-
$GuidToUpn = @{}
50+
$ExoLogs = [System.Collections.Generic.List[object]]::new()
5851

5952
foreach ($User in $Users) {
60-
$UserUpn = $User.body.userPrincipalName
61-
if (-not $UserUpn) { continue }
62-
$OpGuid = [guid]::NewGuid().ToString()
63-
$GuidToUpn[$OpGuid] = $UserUpn
64-
$Params = @{ Identity = $GroupId; Member = $UserUpn; BypassSecurityGroupManagerCheck = $true }
53+
$Params = @{ Identity = $GroupId; Member = $User.body.userPrincipalName; BypassSecurityGroupManagerCheck = $true }
6554
$ExoBulkRequests.Add(@{
66-
OperationGuid = $OpGuid
67-
CmdletInput = @{
55+
CmdletInput = @{
6856
CmdletName = 'Add-DistributionGroupMember'
6957
Parameters = $Params
7058
}
7159
})
60+
$ExoLogs.Add(@{
61+
message = "Added member $($User.body.userPrincipalName) to $($GroupId) group"
62+
target = $User.body.userPrincipalName
63+
})
7264
}
7365

7466
if ($ExoBulkRequests.Count -gt 0) {
75-
$RawExoRequest = @(New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests))
67+
$RawExoRequest = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests)
68+
$LastError = $RawExoRequest | Select-Object -Last 1
7669

77-
# Index responses by OperationGuid so each user is correlated by position, not by error.target
78-
$ResponseByGuid = @{}
79-
foreach ($Response in $RawExoRequest) {
80-
if ($Response.OperationGuid) {
81-
$ResponseByGuid[$Response.OperationGuid] = $Response
82-
}
70+
foreach ($ExoError in $LastError.error) {
71+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoError -Sev 'Error'
72+
throw $ExoError
8373
}
8474

85-
foreach ($OpGuid in $GuidToUpn.Keys) {
86-
$UserUpn = $GuidToUpn[$OpGuid]
87-
$Response = $ResponseByGuid[$OpGuid]
88-
89-
if ($Response -and $Response.error) {
90-
$ErrorText = if ($Response.error -is [string]) { $Response.error } else { ($Response.error | Out-String).Trim() }
91-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($UserUpn) to $($GroupId): $ErrorText" -Sev 'Error'
92-
$FailedUsers.Add($UserUpn)
93-
} else {
94-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Added member $($UserUpn) to $($GroupId) group" -Sev 'Info'
95-
$SuccessfulUsers.Add($UserUpn)
75+
foreach ($ExoLog in $ExoLogs) {
76+
$ExoError = $LastError | Where-Object { $ExoLog.target -in $_.target -and $_.error }
77+
if (!$LastError -or ($LastError.error -and $LastError.target -notcontains $ExoLog.target)) {
78+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoLog.message -Sev 'Info'
9679
}
9780
}
9881
}
@@ -108,26 +91,19 @@ function Add-CIPPGroupMember {
10891
}
10992
}
11093
$AddResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($AddRequests)
94+
$SuccessfulUsers = [system.collections.generic.list[string]]::new()
11195
foreach ($Result in $AddResults) {
112-
$UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName
11396
if ($Result.status -lt 200 -or $Result.status -gt 299) {
114-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($UserPrincipalName): $($Result.body.error.message)" -Sev 'Error'
115-
$FailedUsers.Add($UserPrincipalName)
97+
$FailedUsername = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName
98+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($FailedUsername): $($Result.body.error.message)" -Sev 'Error'
11699
} else {
100+
$UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName
117101
$SuccessfulUsers.Add($UserPrincipalName)
118102
}
119103
}
120104
}
121-
122-
if ($SuccessfulUsers.Count -eq 0 -and $FailedUsers.Count -gt 0) {
123-
$Results = "Failed to add user $($FailedUsers -join ', ') to $($GroupId)."
124-
throw $Results
125-
}
126-
127-
$Results = "Successfully added user $($SuccessfulUsers -join ', ') to $($GroupId)."
128-
if ($FailedUsers.Count -gt 0) {
129-
$Results = "$Results Failed to add: $($FailedUsers -join ', ')."
130-
}
105+
$UserList = ($SuccessfulUsers -join ', ')
106+
$Results = "Successfully added user $UserList to $($GroupId)."
131107
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info'
132108
return $Results
133109
} catch {

Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function Add-CIPPScheduledTask {
7070
$ImportedModules = [System.Collections.Generic.List[string]]::new()
7171
if (-not $Command) {
7272
try {
73-
foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB')) {
73+
foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
7474
if (-not (Get-Module -Name $SiblingModule)) {
7575
Import-Module $SiblingModule -ErrorAction SilentlyContinue
7676
if (Get-Module -Name $SiblingModule) {
@@ -91,7 +91,7 @@ function Add-CIPPScheduledTask {
9191
return "Error - The command '$RequestedCommand' does not exist and cannot be scheduled."
9292
}
9393

94-
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) {
94+
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
9595
Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$RequestedCommand" -Sev 'Warning'
9696
return "Error - The command '$RequestedCommand' is not permitted to run as a scheduled task."
9797
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
function Add-CIPPSSOAppSecret {
2+
<#
3+
.SYNOPSIS
4+
Creates a client secret on the CIPP-SSO app registration with retry.
5+
.DESCRIPTION
6+
Adds a new password credential to the given app object via Graph. Retries up to
7+
MaxRetries times with backoff because Entra propagation can take a few seconds
8+
after the app is freshly created or its app-management-policy exemption is set.
9+
Throws on final failure so callers can persist Status=error + LastError.
10+
.PARAMETER ObjectId
11+
Graph object ID of the application (NOT the appId/clientId).
12+
.PARAMETER DisplayName
13+
Display name to set on the password credential. Defaults to 'CIPP-SSO-Secret'.
14+
.PARAMETER MaxRetries
15+
Number of secret-creation attempts before giving up. Defaults to 5.
16+
#>
17+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
18+
[CmdletBinding()]
19+
param(
20+
[Parameter(Mandatory = $true)]
21+
[string]$ObjectId,
22+
23+
[Parameter(Mandatory = $false)]
24+
[string]$DisplayName = 'CIPP-SSO-Secret',
25+
26+
[Parameter(Mandatory = $false)]
27+
[int]$MaxRetries = 5
28+
)
29+
30+
$SecretText = $null
31+
$SecretAttempt = 0
32+
$BackoffSchedule = @(2, 5, 10, 15, 30)
33+
$LastException = $null
34+
35+
while ($SecretAttempt -lt $MaxRetries -and -not $SecretText) {
36+
try {
37+
$PasswordBody = @{ passwordCredential = @{ displayName = $DisplayName } } | ConvertTo-Json -Compress
38+
$PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true
39+
$SecretText = $PasswordResult.secretText
40+
Write-Information "[SSO-Secret] Client secret created on objectId $ObjectId"
41+
} catch {
42+
$SecretAttempt++
43+
$LastException = $_
44+
Write-Warning "[SSO-Secret] Secret creation attempt $SecretAttempt/$MaxRetries failed: $($_.Exception.Message)"
45+
if ($SecretAttempt -lt $MaxRetries) {
46+
$Delay = $BackoffSchedule[[Math]::Min($SecretAttempt - 1, $BackoffSchedule.Count - 1)]
47+
Start-Sleep -Seconds $Delay
48+
}
49+
}
50+
}
51+
52+
if (-not $SecretText) {
53+
$InnerMessage = if ($LastException) { $LastException.Exception.Message } else { 'unknown error' }
54+
throw "Failed to create client secret for CIPP-SSO after $MaxRetries attempts: $InnerMessage"
55+
}
56+
57+
return $SecretText
58+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function Get-CippAllowedPermissions {
2323

2424
# Get all available permissions and base roles configuration
2525

26-
$Version = (Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\version_latest.txt')).trim()
26+
$Version = (Get-Content -Path (Join-Path $env:CIPPRootPath 'version_latest.txt')).trim()
2727
$BaseRoles = Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\cipp-roles.json') | ConvertFrom-Json
2828
$DefaultRoles = @('superadmin', 'admin', 'editor', 'readonly', 'anonymous', 'authenticated')
2929

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

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ function New-CIPPSSOApp {
66
Creates a new or updates an existing Entra ID app registration for CIPP-SSO with
77
openid, profile, and email delegated permissions. If ExistingAppId is provided,
88
looks up that specific app by clientId. If the app no longer exists in the tenant,
9-
creates a new one. Generates a client secret and returns the details needed to
10-
configure EasyAuth.
9+
creates a new one. Does NOT create a client secret — call Add-CIPPSSOAppSecret
10+
for that as a separate step so the AppId can be persisted before the (sometimes
11+
flaky) secret creation runs.
1112
#>
1213
[CmdletBinding()]
1314
param(
@@ -120,37 +121,12 @@ function New-CIPPSSOApp {
120121
Write-Warning "[SSO-App] App management policy update failed (secret creation may still work): $($_.Exception.Message)"
121122
}
122123

123-
# Create client secret with retry
124-
$SecretText = $null
125-
$SecretAttempt = 0
126-
$MaxSecretRetries = 5
127-
while ($SecretAttempt -lt $MaxSecretRetries -and -not $SecretText) {
128-
try {
129-
$PasswordBody = '{"passwordCredential":{"displayName":"CIPP-SSO-Secret"}}'
130-
$PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$AppObjectId/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true
131-
$SecretText = $PasswordResult.secretText
132-
Write-Information "[SSO-App] Client secret created"
133-
} catch {
134-
$SecretAttempt++
135-
Write-Warning "[SSO-App] Secret creation attempt $SecretAttempt/$MaxSecretRetries failed: $($_.Exception.Message)"
136-
if ($SecretAttempt -lt $MaxSecretRetries) {
137-
$Delay = @(2, 5, 10, 15, 30)[$SecretAttempt - 1]
138-
Start-Sleep -Seconds $Delay
139-
}
140-
}
141-
}
142-
143-
if (-not $SecretText) {
144-
throw "Failed to create client secret for $AppDisplayName after $MaxSecretRetries attempts"
145-
}
146-
147124
return [PSCustomObject]@{
148-
AppId = $AppClientId
149-
ObjectId = $AppObjectId
150-
ClientSecret = $SecretText
151-
TenantId = $env:TenantID
152-
DisplayName = $AppDisplayName
153-
State = $State
154-
MultiTenant = $MultiTenant
125+
AppId = $AppClientId
126+
ObjectId = $AppObjectId
127+
TenantId = $env:TenantID
128+
DisplayName = $AppDisplayName
129+
State = $State
130+
MultiTenant = $MultiTenant
155131
}
156132
}

0 commit comments

Comments
 (0)