|
| 1 | +function Sync-ITGlueConditionalAccessPolicies { |
| 2 | + <# |
| 3 | + .FUNCTIONALITY |
| 4 | + Internal |
| 5 | + .SYNOPSIS |
| 6 | + Syncs Conditional Access Policies to ITGlue Flexible Assets. |
| 7 | + #> |
| 8 | + param( |
| 9 | + $CAPTypeId, |
| 10 | + $OrgId, |
| 11 | + $Conn, |
| 12 | + $ConditionalAccessPolicies, |
| 13 | + $ITGlueAssetCache, |
| 14 | + $TenantFilter, |
| 15 | + $CIPPURL, |
| 16 | + $Tenant |
| 17 | + ) |
| 18 | + |
| 19 | + $Result = @{ |
| 20 | + UpdatedCount = 0 |
| 21 | + SkippedCount = 0 |
| 22 | + Errors = [System.Collections.Generic.List[string]]@() |
| 23 | + Logs = [System.Collections.Generic.List[string]]@() |
| 24 | + } |
| 25 | + |
| 26 | + try { |
| 27 | + Add-ITGlueFlexibleAssetFields -TypeId $CAPTypeId -FieldsToAdd @( |
| 28 | + @{ Name = 'Policy Name'; Kind = 'Text'; ShowInList = $true } |
| 29 | + @{ Name = 'Policy ID'; Kind = 'Text'; ShowInList = $false } |
| 30 | + @{ Name = 'State'; Kind = 'Text'; ShowInList = $true } |
| 31 | + @{ Name = 'Policy Details'; Kind = 'Textbox'; ShowInList = $false } |
| 32 | + @{ Name = 'Raw JSON'; Kind = 'Textbox'; ShowInList = $false } |
| 33 | + ) -Conn $Conn |
| 34 | + |
| 35 | + $ExistingCAPAssets = Invoke-ITGlueRequest -Method GET -Endpoint '/flexible_assets' -Headers $Conn.Headers -BaseUrl $Conn.BaseUrl -QueryParams @{ |
| 36 | + 'filter[flexible_asset_type_id]' = $CAPTypeId |
| 37 | + 'filter[organization_id]' = $OrgId |
| 38 | + } |
| 39 | + |
| 40 | + foreach ($CAP in $ConditionalAccessPolicies) { |
| 41 | + try { |
| 42 | + $StateIcon = switch ($CAP.state) { |
| 43 | + 'enabled' { '✓ Enabled' } |
| 44 | + 'disabled' { '✗ Disabled' } |
| 45 | + 'enabledForReportingButNotEnforced' { '⚠ Report-Only' } |
| 46 | + default { $CAP.state } |
| 47 | + } |
| 48 | + |
| 49 | + # Build content for hash - ONLY actual policy settings (exclude dates/timestamps) |
| 50 | + $ContentForHash = @" |
| 51 | +State: $StateIcon |
| 52 | +Client App Types: $($CAP.clientAppTypes) |
| 53 | +Platforms (Include): $($CAP.includePlatforms) |
| 54 | +Platforms (Exclude): $($CAP.excludePlatforms) |
| 55 | +Locations (Include): $($CAP.includeLocations) |
| 56 | +Locations (Exclude): $($CAP.excludeLocations) |
| 57 | +Applications (Include): $($CAP.includeApplications) |
| 58 | +Applications (Exclude): $($CAP.excludeApplications) |
| 59 | +User Actions: $(Format-ITGlueCAPValue $CAP.includeUserActions) |
| 60 | +Auth Context: $(Format-ITGlueCAPValue $CAP.includeAuthenticationContextClassReferences) |
| 61 | +Users (Include): $(Format-ITGlueCAPValue $CAP.includeUsers) |
| 62 | +Users (Exclude): $(Format-ITGlueCAPValue $CAP.excludeUsers) |
| 63 | +Groups (Include): $(Format-ITGlueCAPValue $CAP.includeGroups) |
| 64 | +Groups (Exclude): $(Format-ITGlueCAPValue $CAP.excludeGroups) |
| 65 | +Roles (Include): $(Format-ITGlueCAPValue $CAP.includeRoles) |
| 66 | +Roles (Exclude): $(Format-ITGlueCAPValue $CAP.excludeRoles) |
| 67 | +Operator: $($CAP.grantControlsOperator) |
| 68 | +Built-in Controls: $($CAP.builtInControls) |
| 69 | +Custom Auth Factors: $($CAP.customAuthenticationFactors) |
| 70 | +Terms of Use: $($CAP.termsOfUse) |
| 71 | +"@ |
| 72 | + |
| 73 | + # Hash-based change detection - hash ONLY policy content (not dates or display timestamps) |
| 74 | + $ContentToHash = "$($CAP.displayName)|$($CAP.state)|$ContentForHash" |
| 75 | + $NewHash = Get-StringHash -String $ContentToHash |
| 76 | + |
| 77 | + # Build full HTML with dates for display (dates NOT in hash) |
| 78 | + $DetailsHtml = @" |
| 79 | +<h4>State: $StateIcon</h4> |
| 80 | +<p><strong>Created:</strong> $($CAP.createdDateTime)<br/> |
| 81 | +<strong>Modified:</strong> $($CAP.modifiedDateTime)</p> |
| 82 | +
|
| 83 | +<h4>Conditions</h4> |
| 84 | +<table> |
| 85 | +<tr><td><strong>Client App Types</strong></td><td>$($CAP.clientAppTypes)</td></tr> |
| 86 | +<tr><td><strong>Platforms (Include)</strong></td><td>$($CAP.includePlatforms)</td></tr> |
| 87 | +<tr><td><strong>Platforms (Exclude)</strong></td><td>$($CAP.excludePlatforms)</td></tr> |
| 88 | +<tr><td><strong>Locations (Include)</strong></td><td>$($CAP.includeLocations)</td></tr> |
| 89 | +<tr><td><strong>Locations (Exclude)</strong></td><td>$($CAP.excludeLocations)</td></tr> |
| 90 | +<tr><td><strong>Applications (Include)</strong></td><td>$($CAP.includeApplications)</td></tr> |
| 91 | +<tr><td><strong>Applications (Exclude)</strong></td><td>$($CAP.excludeApplications)</td></tr> |
| 92 | +<tr><td><strong>User Actions</strong></td><td>$(Format-ITGlueCAPValue $CAP.includeUserActions)</td></tr> |
| 93 | +<tr><td><strong>Auth Context</strong></td><td>$(Format-ITGlueCAPValue $CAP.includeAuthenticationContextClassReferences)</td></tr> |
| 94 | +</table> |
| 95 | +
|
| 96 | +<h4>Users & Groups</h4> |
| 97 | +<table> |
| 98 | +<tr><td><strong>Users (Include)</strong></td><td>$(Format-ITGlueCAPValue $CAP.includeUsers)</td></tr> |
| 99 | +<tr><td><strong>Users (Exclude)</strong></td><td>$(Format-ITGlueCAPValue $CAP.excludeUsers)</td></tr> |
| 100 | +<tr><td><strong>Groups (Include)</strong></td><td>$(Format-ITGlueCAPValue $CAP.includeGroups)</td></tr> |
| 101 | +<tr><td><strong>Groups (Exclude)</strong></td><td>$(Format-ITGlueCAPValue $CAP.excludeGroups)</td></tr> |
| 102 | +<tr><td><strong>Roles (Include)</strong></td><td>$(Format-ITGlueCAPValue $CAP.includeRoles)</td></tr> |
| 103 | +<tr><td><strong>Roles (Exclude)</strong></td><td>$(Format-ITGlueCAPValue $CAP.excludeRoles)</td></tr> |
| 104 | +</table> |
| 105 | +
|
| 106 | +<h4>Grant Controls</h4> |
| 107 | +<table> |
| 108 | +<tr><td><strong>Operator</strong></td><td>$($CAP.grantControlsOperator)</td></tr> |
| 109 | +<tr><td><strong>Built-in Controls</strong></td><td>$($CAP.builtInControls)</td></tr> |
| 110 | +<tr><td><strong>Custom Auth Factors</strong></td><td>$($CAP.customAuthenticationFactors)</td></tr> |
| 111 | +<tr><td><strong>Terms of Use</strong></td><td>$($CAP.termsOfUse)</td></tr> |
| 112 | +</table> |
| 113 | +
|
| 114 | +<p><em>Last updated: $(Get-Date -Format 'yyyy-MM-dd HH:mm') UTC</em></p> |
| 115 | +"@ |
| 116 | + |
| 117 | + $CAPTraits = @{ |
| 118 | + 'policy-name' = $CAP.displayName |
| 119 | + 'policy-id' = $CAP.id |
| 120 | + 'state' = $CAP.state |
| 121 | + 'policy-details' = $DetailsHtml |
| 122 | + 'raw-json' = $CAP.rawjson |
| 123 | + } |
| 124 | + |
| 125 | + $ExistingAsset = $ExistingCAPAssets | Where-Object { $_.traits.'policy-id' -eq $CAP.id } | Select-Object -First 1 |
| 126 | + |
| 127 | + # Check if content has changed by comparing hashes |
| 128 | + $NeedsUpdate = $true |
| 129 | + if ($ExistingAsset) { |
| 130 | + $CachedAsset = Get-CIPPAzDataTableEntity @ITGlueAssetCache -Filter "PartitionKey eq 'ITGlueCAP' and RowKey eq '$($ExistingAsset.id)'" |
| 131 | + if ($CachedAsset -and $CachedAsset.Hash -eq $NewHash) { |
| 132 | + $NeedsUpdate = $false |
| 133 | + $Result.SkippedCount++ |
| 134 | + } else { |
| 135 | + # Debug: Log why hash changed |
| 136 | + if ($CachedAsset) { |
| 137 | + Write-LogMessage -API 'ITGlueSync' -tenant $TenantFilter -message "CAP hash mismatch for $($CAP.displayName): Cached=$($CachedAsset.Hash.Substring(0,8))... New=$($NewHash.Substring(0,8))..." -sev Debug |
| 138 | + } else { |
| 139 | + Write-LogMessage -API 'ITGlueSync' -tenant $TenantFilter -message "CAP no cache found for $($CAP.displayName) (AssetID: $($ExistingAsset.id))" -sev Debug |
| 140 | + } |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + if ($NeedsUpdate) { |
| 145 | + $AssetAttribs = @{ |
| 146 | + 'organization-id' = $OrgId |
| 147 | + 'flexible-asset-type-id' = $CAPTypeId |
| 148 | + traits = $CAPTraits |
| 149 | + } |
| 150 | + |
| 151 | + if ($ExistingAsset) { |
| 152 | + $null = Invoke-ITGlueRequest -Method PATCH -Endpoint "/flexible_assets/$($ExistingAsset.id)" -Headers $Conn.Headers -BaseUrl $Conn.BaseUrl -ResourceType 'flexible-assets' -ResourceId $ExistingAsset.id -Attributes $AssetAttribs |
| 153 | + $AssetId = $ExistingAsset.id |
| 154 | + } else { |
| 155 | + $CreatedAsset = Invoke-ITGlueRequest -Method POST -Endpoint '/flexible_assets' -Headers $Conn.Headers -BaseUrl $Conn.BaseUrl -ResourceType 'flexible-assets' -Attributes $AssetAttribs |
| 156 | + $AssetId = $CreatedAsset[0].id |
| 157 | + } |
| 158 | + |
| 159 | + # Cache the hash to avoid unnecessary updates on next sync |
| 160 | + $CacheEntry = @{ |
| 161 | + PartitionKey = 'ITGlueCAP' |
| 162 | + RowKey = [string]$AssetId |
| 163 | + OrgId = [string]$OrgId |
| 164 | + PolicyId = $CAP.id |
| 165 | + Hash = $NewHash |
| 166 | + } |
| 167 | + Add-CIPPAzDataTableEntity @ITGlueAssetCache -Entity $CacheEntry -Force |
| 168 | + |
| 169 | + $Result.UpdatedCount++ |
| 170 | + } |
| 171 | + } catch { |
| 172 | + $Result.Errors.Add("CAP FA [$($CAP.displayName)]: $_") |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + # Delete CAP assets that no longer exist in M365 |
| 177 | + $CurrentCAPIds = $ConditionalAccessPolicies | ForEach-Object { $_.id } |
| 178 | + $OrphanedAssets = $ExistingCAPAssets | Where-Object { $_.traits.'policy-id' -notin $CurrentCAPIds } |
| 179 | + foreach ($Orphan in $OrphanedAssets) { |
| 180 | + try { |
| 181 | + $PolicyName = if ($Orphan.traits.'policy-name') { $Orphan.traits.'policy-name' } else { "ID: $($Orphan.traits.'policy-id')" } |
| 182 | + $null = Invoke-ITGlueRequest -Method DELETE -Endpoint "/flexible_assets/$($Orphan.id)" -Headers $Conn.Headers -BaseUrl $Conn.BaseUrl |
| 183 | + $Result.Logs.Add("Deleted orphaned CAP: $PolicyName") |
| 184 | + |
| 185 | + # Remove from cache |
| 186 | + $CachedAsset = Get-CIPPAzDataTableEntity @ITGlueAssetCache -Filter "PartitionKey eq 'ITGlueCAP' and RowKey eq '$($Orphan.id)'" |
| 187 | + if ($CachedAsset) { |
| 188 | + Remove-AzDataTableEntity @ITGlueAssetCache -Entity $CachedAsset -Force |
| 189 | + } |
| 190 | + } catch { |
| 191 | + $PolicyName = if ($Orphan.traits.'policy-name') { $Orphan.traits.'policy-name' } else { "ID: $($Orphan.traits.'policy-id')" } |
| 192 | + $Result.Errors.Add("Failed to delete orphaned CAP [$PolicyName]: $_") |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + $Result.Logs.Add("Conditional Access Policies: $($Result.UpdatedCount) updated, $($Result.SkippedCount) unchanged") |
| 197 | + } catch { |
| 198 | + $Result.Errors.Add("Conditional Access Policies block failed: $_") |
| 199 | + } |
| 200 | + |
| 201 | + return $Result |
| 202 | +} |
0 commit comments