internal/functions/Build-AssignmentPlan.ps1
function Write-AssignmentDetails { [CmdletBinding()] param ( $displayName, $scope, $prefix, $identityStatus ) $shortScope = $scope -replace "/providers/Microsoft.Management", "" if ($prefix -ne "") { Write-Information " $($prefix) '$($displayName)' at $($shortScope)" } else { Write-Information " '$($displayName)' at $($shortScope)" } if ($identityStatus.requiresRoleChanges) { foreach ($role in $identityStatus.added) { $roleScope = $role.scope $roleShortScope = $roleScope -replace "/providers/Microsoft.Management", "" Write-Information " add role $($role.roleDisplayName) at $($roleShortScope)" } foreach ($role in $identityStatus.removed) { $roleScope = $role.scope $roleShortScope = $roleScope -replace "/providers/Microsoft.Management", "" Write-Information " remove role $($role.roleDisplayName) at $($roleShortScope)" } } } function Build-AssignmentPlan { [CmdletBinding()] param ( [string] $assignmentsRootFolder, [hashtable] $pacEnvironment, [hashtable] $scopeTable, [hashtable] $deployedPolicyResources, [hashtable] $assignments, [hashtable] $roleAssignments, [hashtable] $allDefinitions, [hashtable] $allAssignments, [hashtable] $replaceDefinitions, [hashtable] $policyRoleIds ) Write-Information "===================================================================================================" Write-Information "Processing Policy Assignments JSON files in folder '$assignmentsRootFolder'" Write-Information "===================================================================================================" $assignmentFiles = @() $assignmentFiles += Get-ChildItem -Path $assignmentsRootFolder -Recurse -File -Filter "*.json" $assignmentFiles += Get-ChildItem -Path $assignmentsRootFolder -Recurse -File -Filter "*.jsonc" $csvFiles = Get-ChildItem -Path $assignmentsRootFolder -Recurse -File -Filter "*.csv" $parameterFilesCsv = @{} if ($assignmentFiles.Length -gt 0) { Write-Information "Number of Policy Assignment files = $($assignmentFiles.Length)" foreach ($csvFile in $csvFiles) { $parameterFilesCsv.Add($csvFile.Name, $csvFile.FullName) } } else { Write-Information "There aren't any Policy Assignment files in the folder provided!" } # Cache role definitions $roleDefinitionList = Get-AzRoleDefinition [hashtable] $roleDefinitions = @{} foreach ($roleDefinition in $roleDefinitionList) { if (!$roleDefinitions.ContainsKey($roleDefinition.Id)) { $null = $roleDefinitions.Add($roleDefinition.Id, $roleDefinition.Name) } } # Convert Policy and PolicySetDefinition to detailed Info $combinedPolicyDetails = Convert-PolicySetsToDetails ` -allPolicyDefinitions $allDefinitions.policydefinitions ` -allPolicySetDefinitions $allDefinitions.policysetdefinitions # Process files $deployedPolicyAssignments = $deployedPolicyResources.policyassignments.managed $deployedRoleAssignmentsByPrincipalId = $deployedPolicyResources.roleAssignmentsByPrincipalId $deleteCandidates = Get-HashtableShallowClone $deployedPolicyAssignments foreach ($id in $deployedPolicyAssignments.Keys) { $allAssignments[$id] = $deployedPolicyAssignments.$id } foreach ($assignmentFile in $assignmentFiles) { $Json = Get-Content -Path $assignmentFile.FullName -Raw -ErrorAction Stop Write-Information "" if ((Test-Json $Json)) { Write-Information "Processing file '$($assignmentFile.FullName)'" } else { Write-Error "Assignment JSON file '$($assignmentFile.FullName)' is not valid." -ErrorAction Stop } $assignmentObject = $Json | ConvertFrom-Json -AsHashtable # Remove-NullFields $assignmentObject # Collect all assignment definitions (values) $rootAssignmentDefinition = @{ nodeName = "/" assignment = @{ append = $false name = "" displayName = "" description = "" } enforcementMode = "Default" parameters = @{} additionalRoleAssignments = @() nonComplianceMessages = @() overrides = @() resourceSelectors = @() hasErrors = $false hasOnlyNotSelectedEnvironments = $false ignoreBranch = $false managedIdentityLocation = $pacEnvironment.managedIdentityLocation notScope = $pacEnvironment.globalNotScopes csvRowsValidated = $false } $hasErrors, $assignmentsList = Build-AssignmentDefinitionNode ` -pacEnvironment $pacEnvironment ` -scopeTable $scopeTable ` -parameterFilesCsv $parameterFilesCsv ` -definitionNode $assignmentObject ` -assignmentDefinition $rootAssignmentDefinition ` -combinedPolicyDetails $combinedPolicyDetails ` -policyRoleIds $policyRoleIds if ($hasErrors) { Write-Error "Assignment definitions content errors" -ErrorAction Stop } $isUserAssignedAny = $false foreach ($assignment in $assignmentsList) { # Remove-NullFields $assignment $id = $assignment.id $allAssignments[$id] = $assignment $displayName = $assignment.displayName $description = $assignment.description $metadata = $assignment.metadata $parameters = $assignment.parameters $policyDefinitionId = $assignment.policyDefinitionId $scope = $assignment.scope $notScopes = $assignment.notScopes $enforcementMode = $assignment.enforcementMode if ($assignment.nonComplianceMessages.ContainsKey("Key")) { $obj = @{ message = $assignment.nonComplianceMessages.Value policyDefinitionReferenceId = $assignment.nonComplianceMessages.policyDefinitionReferenceId } $assignment.nonComplianceMessages = @($obj) } $nonComplianceMessages = $assignment.nonComplianceMessages $overrides = $assignment.overrides $resourceSelectors = $assignment.resourceSelectors if ($deployedPolicyAssignments.ContainsKey($id)) { # Update and replace scenarios $deployedPolicyAssignment = $deployedPolicyAssignments[$id] $deployedPolicyAssignmentProperties = Get-PolicyResourceProperties $deployedPolicyAssignment $deleteCandidates.Remove($id) # do not delete $replacedDefinition = $replaceDefinitions.ContainsKey($policyDefinitionId) $changedPolicyDefinitionId = $policyDefinitionId -ne $deployedPolicyAssignmentProperties.policyDefinitionId $displayNameMatches = $displayName -eq $deployedPolicyAssignmentProperties.displayName $descriptionMatches = $description -eq $deployedPolicyAssignmentProperties.description $notScopesMatch = Confirm-ObjectValueEqualityDeep ` $deployedPolicyAssignmentProperties.notScopes ` $notScopes $parametersMatch = Confirm-AssignmentParametersMatch ` -existingParametersObj $deployedPolicyAssignmentProperties.parameters ` -definedParametersObj $parameters $metadataMatches, $changePacOwnerId = Confirm-MetadataMatches ` -existingMetadataObj $deployedPolicyAssignmentProperties.metadata ` -definedMetadataObj $metadata $enforcementModeMatches = $enforcementMode -eq $deployedPolicyAssignmentProperties.EnforcementMode # Rebuild the non-compliance object $nonComplianceObject = @() if ($nonComplianceMessages.length -gt 1) { foreach ($nc in $nonComplianceMessages) { $obj = @{ message = $nc.message policyDefinitionReferenceId = $nc.policyDefinitionReferenceId } $nonComplianceObject += $obj } } else { if ($null -ne $nonComplianceMessages) { if ($null -ne $nonComplianceMessages[0].Keys) { foreach ($nc in $nonComplianceMessages) { if ($nc.ContainsKey("Key")) { $obj = @{ message = $nc.Value policyDefinitionReferenceId = $nc.policyDefinitionReferenceId } $nonComplianceObject += $obj } else { $obj = @{ message = $nc.message policyDefinitionReferenceId = $nc.policyDefinitionReferenceId } $nonComplianceObject += $obj } } } else { foreach ($nc in $nonComplianceMessages.Keys) { $obj = @{ message = $nonComplianceMessages[0][$nc] policyDefinitionReferenceId = $nc.policyDefinitionReferenceId } $nonComplianceObject += $obj } } } } $nonComplianceMessagesMatches = Confirm-ObjectValueEqualityDeep ` $deployedPolicyAssignmentProperties.nonComplianceMessages ` $nonComplianceObject $overridesMatch = Confirm-ObjectValueEqualityDeep ` $deployedPolicyAssignmentProperties.overrides ` $overrides $resourceSelectorsMatch = Confirm-ObjectValueEqualityDeep ` $deployedPolicyAssignmentProperties.resourceSelectors ` $resourceSelectors $identityStatus = Build-AssignmentIdentityChanges ` -existing $deployedPolicyAssignment ` -assignment $assignment ` -replacedAssignment ($replacedDefinition -or $changedPolicyDefinitionId) ` -deployedRoleAssignmentsByPrincipalId $deployedRoleAssignmentsByPrincipalId if ($identityStatus.requiresRoleChanges) { $roleAssignments.added += ($identityStatus.added) $roleAssignments.removed += ($identityStatus.removed) $roleAssignments.numberOfChanges += ($identityStatus.numberOfChanges) } if ($identityStatus.isUserAssigned) { $isUserAssignedAny = $true } # Check if Policy assignment in Azure is the same as in the JSON file $changesStrings = @() $match = $displayNameMatches -and $descriptionMatches -and $parametersMatch -and $metadataMatches -and !$changePacOwnerId ` -and $enforcementModeMatches -and $notScopesMatch -and $nonComplianceMessagesMatches -and $overridesMatch -and $resourceSelectorsMatch -and !$identityStatus.replaced if ($match) { # no Assignment properties changed $assignments.numberUnchanged++ if ($identityStatus.requiresRoleChanges) { # role assignments for Managed Identity changed - caused by a mangedIdentityLocation changed or a previously failed role assignment failure Write-AssignmentDetails -displayName $displayName -scope $scope -prefix "Update($($identityStatus.changedIdentityStrings))" -identityStatus $identityStatus } else { Write-AssignmentDetails -displayName $displayName -scope $scope -prefix "Unchanged" -identityStatus $identityStatus } } else { # One or more properties have changed if ($identityStatus.replaced) { # Assignment must be deleted and recreated (new) if ($changedPolicyDefinitionId) { $changesStrings += "definitionId" } if ($replacedDefinition) { $changesStrings += "replacedDefinition" } $changesStrings += ($identityStatus.changedIdentityStrings) } if (!$displayNameMatches) { $changesStrings += "displayName" } if (!$descriptionMatches) { $changesStrings += "description" } if ($changePacOwnerId) { $changesStrings += "owner" } if (!$metadataMatches) { $changesStrings += "metadata" } if (!$parametersMatch) { $changesStrings += "parameters" } if (!$enforcementModeMatches) { $changesStrings += "enforcementMode" } if (!$notScopesMatch) { $changesStrings += "notScopes" } if (!$nonComplianceMessagesMatches) { $changesStrings += "nonComplianceMessages" } if (!$overridesMatch) { $changesStrings += "overrides" } if (!$resourceSelectorsMatch) { $changesStrings += "resourceSelectors" } $changesString = $changesStrings -join "," if ($identityStatus.replaced) { # Assignment must be deleted and recreated (new) $null = $assignments.replace.Add($id, $assignment) Write-AssignmentDetails -displayName $displayName -scope $scope -prefix "Replace($changesString)" -identityStatus $identityStatus } else { $null = $assignments.update.Add($id, $assignment) Write-AssignmentDetails -displayName $displayName -scope $scope -prefix "Update($changesString)" -identityStatus $identityStatus } $assignments.numberOfChanges++ } } else { # New Assignment $null = $assignments.new.Add($id, $assignment) $assignments.numberOfChanges++ $identityStatus = Build-AssignmentIdentityChanges ` -existing $null ` -assignment $assignment ` -replacedAssignment $false ` -deployedRoleAssignmentsByPrincipalId $deployedRoleAssignmentsByPrincipalId if ($identityStatus.requiresRoleChanges) { $roleAssignments.added += ($identityStatus.added) $roleAssignments.numberOfChanges += ($identityStatus.numberOfChanges) } if ($identityStatus.isUserAssigned) { $isUserAssignedAny = $true } Write-AssignmentDetails -displayName $displayName -scope $scope -prefix "New" -identityStatus $identityStatus } } } $strategy = $pacEnvironment.desiredState.strategy if ($deleteCandidates.psbase.Count -gt 0) { Write-Information "" Write-Information "Cleanup removed Policy resources (delete)" foreach ($id in $deleteCandidates.Keys) { $deleteCandidate = $deleteCandidates.$id $deleteCandidateProperties = Get-PolicyResourceProperties $deleteCandidate $name = $deleteCandidate.name $displayName = $deleteCandidateProperties.displayName $scope = $deleteCandidateProperties.scope $pacOwner = $deleteCandidate.pacOwner $shallDelete = Confirm-DeleteForStrategy -pacOwner $pacOwner -strategy $strategy if ($shallDelete) { # always delete if owned by this Policy as Code solution # never delete if owned by another Policy as Code solution # if strategy is "full", delete with unknown owner (missing pacOwnerId) Write-AssignmentDetails -displayName $displayName -scope $scope -prefix "" -identityStatus $identityStatus $splat = @{ id = $id name = $name scopeId = $scope displayName = $deleteCandidateProperties.displayName } $allAssignments.Remove($id) $assignments.delete.Add($id, $splat) $assignments.numberOfChanges++ $identityStatus = Build-AssignmentIdentityChanges ` -existing $deployedPolicyAssignment ` -assignment $null ` -replacedAssignment $false ` -deployedRoleAssignmentsByPrincipalId $deployedRoleAssignmentsByPrincipalId if ($identityStatus.requiresRoleChanges) { $roleAssignments.removed += ($identityStatus.removed) $roleAssignments.numberOfChanges += ($identityStatus.numberOfChanges) } if ($identityStatus.isUserAssigned) { $isUserAssignedAny = $true } } else { Write-AssignmentDetails -displayName $name -scope $scope -prefix "Desired State($pacOwner,$strategy) - no delete" -identityStatus $identityStatus } } } Write-Information "" if ($isUserAssignedAny) { Write-Warning "EPAC does not manage role assignments for Policy Assignments with user-assigned Managed Identities." } Write-Information "Number of unchanged Policy Assignments = $($assignments.numberUnchanged)" Write-Information "" } |