internal/functions/Build-AssignmentPlan.ps1
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 "===================================================================================================" # Cache role definitions $roleDefinitionList = Get-AzRoleDefinition [hashtable] $roleDefinitions = @{} foreach ($roleDefinition in $roleDefinitionList) { if (!$roleDefinitions.ContainsKey($roleDefinition.Id)) { $null = $roleDefinitions.Add($roleDefinition.Id, $roleDefinition.Name) } } # Populate allAssignments $deployedPolicyAssignments = $DeployedPolicyResources.policyassignments.managed $deployedRoleAssignmentsByPrincipalId = $DeployedPolicyResources.roleAssignmentsByPrincipalId $deleteCandidates = Get-HashtableShallowClone $deployedPolicyAssignments foreach ($id in $deployedPolicyAssignments.Keys) { $AllAssignments[$id] = $deployedPolicyAssignments.$id } if (!(Test-Path $AssignmentsRootFolder -PathType Container)) { Write-Warning "Policy Assignments folder 'policyAssignments' not found. Assignments not managed by this EPAC instance." } else { # Convert Policy and PolicySetDefinition to detailed Info $combinedPolicyDetails = Convert-PolicySetsToDetails ` -AllPolicyDefinitions $AllDefinitions.policydefinitions ` -AllPolicySetDefinitions $AllDefinitions.policysetdefinitions $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-Warning "No Policy Assignment files found! Deleting any Policy Assignments." } # Process each assignment file 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 $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 $nonComplianceMessagesMatches = Confirm-ObjectValueEqualityDeep ` $deployedPolicyAssignmentProperties.nonComplianceMessages ` $nonComplianceMessages $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 "Cleanup removed Policy Assignments (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) $IdentityStatus = Build-AssignmentIdentityChanges ` -Existing $deleteCandidate ` -Assignment $null ` -ReplacedAssignment $false ` -DeployedRoleAssignmentsByPrincipalId $deployedRoleAssignmentsByPrincipalId if ($IdentityStatus.requiresRoleChanges) { $RoleAssignments.removed += ($IdentityStatus.removed) $RoleAssignments.numberOfChanges += ($IdentityStatus.numberOfChanges) } if ($IdentityStatus.isUserAssigned) { $isUserAssignedAny = $true } Write-AssignmentDetails -DisplayName $DisplayName -Scope $Scope -Prefix "" -IdentityStatus $IdentityStatus $splat = @{ id = $id name = $name scopeId = $Scope displayName = $DisplayName } $AllAssignments.Remove($id) $Assignments.delete.Add($id, $splat) $Assignments.numberOfChanges++ } 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 "" } |