internal/functions/Build-AssignmentDefinitionAtLeaf.ps1
#Requires -PSEdition Core function Build-AssignmentDefinitionAtLeaf { # Recursive Function param( $pacEnvironment, [hashtable] $assignmentDefinition, [hashtable] $combinedPolicyDetails, [hashtable] $policyRoleIds # Returns a list of completed assignment definitions (each a hashtable) ) # Must contain a definitionEntry or definitionEntryList $definitionEntryList = $assignmentDefinition.definitionEntryList $hasErrors = $assignmentDefinition.hasErrors $nodeName = $assignmentDefinition.nodeName if ($definitionEntryList.Count -eq 0) { Write-Error " Leaf Node $($nodeName): each tree branch must define either a definitionEntry or a non-empty definitionEntryList." $hasErrors = $true } # Must contain a scopeCollection $scopeCollection = $assignmentDefinition.scopeCollection if ($null -eq $scopeCollection) { Write-Error " Leaf Node $($nodeName): each tree branch requires exactly one scope definition resulting in a scope collection after notScope calculations." $hasErrors = $true } # Validate optional parameterFileName $parameterSuppressDefaultValues = $true if ($null -ne $assignmentDefinition.parameterSuppressDefaultValues) { $parameterSuppressDefaultValues = $assignmentDefinition.parameterSuppressDefaultValues } $parameterFileName = $assignmentDefinition.parameterFileName $parameterSelector = $assignmentDefinition.parameterSelector $useCsv = $false if ($null -ne $parameterFileName) { if ($null -ne $parameterSelector) { $useCsv = $true } else { Write-Error " Leaf Node $($nodeName): parameterFile ($parameterFileName) usage requires a parameterSelector string." $hasErrors = $true } } if (!$hasErrors) { # Assemble entries without scopes or parameters and prepare for parameter processing $assignmentInDefinition = $assignmentDefinition.assignment $assignmentsList = @() $assignmentEntryList = [System.Collections.ArrayList]::new() $itemArrayList = [System.Collections.ArrayList]::new() $thisPacOwnerId = $pacEnvironment.pacOwnerId $policyDefinitionsScopes = $pacEnvironment.policyDefinitionsScopes foreach ($definitionEntry in $definitionEntryList) { $assignmentInDefinitionEntry = $definitionEntry.assignment $name = "" $displayName = "" $description = "" if ($assignmentInDefinitionEntry.append) { $name = $assignmentInDefinition.name + $assignmentInDefinitionEntry.name $displayName = $assignmentInDefinition.displayName + $assignmentInDefinitionEntry.displayName $description = $assignmentInDefinition.description + $assignmentInDefinitionEntry.description } else { $name = $assignmentInDefinitionEntry.name + $assignmentInDefinition.name $displayName = $assignmentInDefinitionEntry.displayName + $assignmentInDefinition.displayName $description = $assignmentInDefinitionEntry.description + $assignmentInDefinition.description } if ($name.Length -eq 0 -or $displayName.Length -eq 0) { Write-Error " Leaf Node $($nodeName): each tree branch must define an Assignment name and displayName.`n name='$name'`n displayName='$displayName'`n description=$description" $hasErrors = $true continue } if ($definitionEntry.nonComplianceMessages) { $nonComplianceMessages = $definitionEntry.nonComplianceMessages } else { $nonComplianceMessages = $assignmentDefinition.nonComplianceMessages } $enforcementMode = $assignmentDefinition.enforcementMode $metadata = $assignmentDefinition.metadata if ($metadata) { if ($metadata.ContainsKey("pacOwnerId")) { $metadata.Remove("pacOwnerId") } $metadata.pacOwnerId = $thisPacOwnerId } else { $metadata = @{ pacOwnerId = $thisPacOwnerId } } $assignmentEntry = $null $policyDefinitionId = $definitionEntry.policyDefinitionId if ($definitionEntry.isPolicySet) { # Set Policy Set id $assignmentEntry = @{ isPolicySet = $true policyDefinitionId = $policyDefinitionId name = $name displayName = $displayName description = $description enforcementMode = $enforcementMode metadata = $metadata nonComplianceMessages = $nonComplianceMessages } if ($useCsv) { $itemEntry = @{ shortName = $policyDefinitionId itemId = $policyDefinitionId policySetId = $policyDefinitionId assignmentId = $null } $null = $itemArrayList.Add($itemEntry) } } else { # Set Policy id $assignmentEntry = @{ isPolicySet = $false policyDefinitionId = $policyDefinitionId name = $name displayName = $displayName description = $description enforcementMode = $enforcementMode metadata = $metadata nonComplianceMessages = $nonComplianceMessages } } $null = $assignmentEntryList.Add($assignmentEntry) } if ($hasErrors) { return $true, $null } $flatPolicyList = $null if ($useCsv) { if ($itemArrayList.Count -gt 0) { $flatPolicyList = Convert-PolicySetsToFlatList ` -itemList $itemArrayList.ToArray() ` -details $combinedPolicyDetails.policySets # Validate compatibility between spreadsheet and definition entry list $rowHashtable = @{} foreach ($row in $assignmentDefinition.csvParameterArray) { # Ignore empty lines with a warning $name = $row.name if ($null -eq $name -or $name -eq "") { Write-Warning " Node $($nodeName): CSV parameterFile '$parameterFileName' has an empty row." continue } # generate the key into the flatPolicyList $policyId = Confirm-PolicyDefinitionUsedExists -name $name -policyDefinitionsScopes $policyDefinitionsScopes -allDefinitions $combinedPolicyDetails.policies -suppressErrorMessage if ($null -eq $policyId) { Write-Error " Node $($nodeName): CSV parameterFile '$parameterFileName' has a row containing an unknown Policy name '$name'." $hasErrors = $true continue } $flatPolicyEntryKey = $policyId $flatPolicyReferencePath = $row.referencePath if ($null -ne $flatPolicyReferencePath -and $flatPolicyReferencePath -ne "") { $flatPolicyEntryKey = "$policyId\\$flatPolicyReferencePath" $null = $rowHashtable.Add($flatPolicyEntryKey, "$($row.displayName) ($name -- $flatPolicyReferencePath)") } else { $null = $rowHashtable.Add($flatPolicyEntryKey, "$($row.displayName) ($name)") } $row.policyId = $policyId $row.flatPolicyEntryKey = $flatPolicyEntryKey } $missingInCsv = [System.Collections.ArrayList]::new() foreach ($flatPolicyEntryKey in $flatPolicyList.Keys) { if ($rowHashtable.ContainsKey($flatPolicyEntryKey)) { $rowHashtable.Remove($flatPolicyEntryKey) } else { $flatPolicyEntry = $flatPolicyList.$flatPolicyEntryKey if ($flatPolicyEntry.isEffectParameterized) { # Complain only about Policies with parameterized effect value if ($flatPolicyEntry.referencePath) { $null = $missingInCsv.Add("$($flatPolicyEntry.displayName) ($($flatPolicyEntry.name) -- $($flatPolicyEntry.referencePath))") } else { $null = $missingInCsv.Add("$($flatPolicyEntry.displayName) ($($flatPolicyEntry.name))") } } } } if ($rowHashtable.Count -gt 0) { Write-Warning " Node $($nodeName): CSV parameterFile '$parameterFileName' contains rows for Policies not included in any of the Policy Sets:" foreach ($displayString in $rowHashtable.Values) { Write-Warning " $($displayString)" } } if ($missingInCsv.Count -gt 0) { Write-Warning " Node $($nodeName): CSV parameterFile '$parameterFileName' is missing rows for Policies included in the Policy Sets:" foreach ($missing in $missingInCsv) { Write-Warning " $($missing)" } } } else { $useCsv = $false } } $effectProcessedForPolicy = @{} foreach ($assignmentEntry in $assignmentEntryList) { # Finish processing definitions, parameters and compliance messages $parameterObject = $null $policyDefinitionId = $assignmentEntry.policyDefinitionId if ($assignmentEntry.isPolicySet) { $policySetsDetails = $combinedPolicyDetails.policySets $policySetDetails = $policySetsDetails.$policyDefinitionId if ($useCsv) { $finalParameters, $localHasErrors = Build-AssignmentCsvAndJsonParameters ` -nodeName $nodeName ` -policySetId $policyDefinitionId ` -policyDefinitionsScopes $policyDefinitionsScopes ` -assignmentDefinition $assignmentDefinition ` -flatPolicyList $flatPolicyList ` -combinedPolicyDetails $combinedPolicyDetails ` -effectProcessedForPolicy $effectProcessedForPolicy if ($localHasErrors) { $hasErrors = $true continue } $parameterObject = Build-AssignmentParameterObject ` -assignmentParameters $finalParameters ` -parametersInPolicyDefinition $policySetDetails.parameters ` -parameterSuppressDefaultValues:$parameterSuppressDefaultValues } else { $parameterObject = Build-AssignmentParameterObject ` -assignmentParameters $assignmentDefinition.parameters ` -parametersInPolicyDefinition $policySetDetails.parameters ` -parameterSuppressDefaultValues:$parameterSuppressDefaultValues } } else { $policiesDetails = $combinedPolicyDetails.policies $policyDetails = $policiesDetails.$policyDefinitionId $parameterObject = Build-AssignmentParameterObject ` -assignmentParameters $assignmentDefinition.parameters ` -parametersInPolicyDefinition $policyDetails.parameters ` -parameterSuppressDefaultValues:$parameterSuppressDefaultValues } if ($parameterObject.Count -gt 0) { $assignmentEntry.parameters = $parameterObject } else { $assignmentEntry.parameters = @{} } # Process scopeCollection foreach ($scopeEntry in $scopeCollection) { # Clone hashtable $scopedAssignment = Get-DeepClone $assignmentEntry -AsHashTable # Complete processing roleDefinitions and additionalRoleAssignments and add with metadata to hashtable $roleAssignmentSpecs = @() $roleDefinitionIds = $null if ($policyRoleIds.ContainsKey($policyDefinitionId)) { $scopedAssignment.identity = @{ type = "SystemAssigned" } $roleDefinitionIds = $policyRoleIds.$policyDefinitionId $scopedAssignment.identityRequired = $true if ($null -ne $assignmentDefinition.managedIdentityLocation) { $scopedAssignment.managedIdentityLocation = $assignmentDefinition.managedIdentityLocation } else { Write-Error "Assignment requires an identity and the definition does not define a managedIdentityLocation" -ErrorAction Stop } foreach ($roleDefinitionId in $roleDefinitionIds) { $roleDisplayName = "Unknown" $roleDefinitionName = ($roleDefinitionId.Split("/"))[-1] if ($roleDefinitions.ContainsKey($roleDefinitionName)) { $roleDisplayName = $roleDefinitions.$roleDefinitionName } $roleAssignmentSpecs += @{ scope = $scopeEntry.scope roleDefinitionId = $roleDefinitionId roleDisplayName = $roleDisplayName } } $additionalRoleAssignments = $assignmentDefinition.additionalRoleAssignments if ($additionalRoleAssignments -and $additionalRoleAssignments.Length -gt 0) { foreach ($additionalRoleAssignment in $additionalRoleAssignments) { $roleDefinitionId = $additionalRoleAssignment.roleDefinitionId $roleDisplayName = "Unknown" $roleDefinitionName = ($roleDefinitionId.Split("/"))[-1] if ($roleDefinitions.ContainsKey($roleDefinitionName)) { $roleDisplayName = $roleDefinitions.$roleDefinitionName } $roleAssignmentSpecs += @{ scope = $additionalRoleAssignment.scope roleDefinitionId = $roleDefinitionId roleDisplayName = $roleDisplayName } } } if ($scopedAssignment.metadata.ContainsKey("roles")) { $scopedAssignment.metadata.Remove("roles") } $null = $scopedAssignment.metadata.Add("roles", $roleAssignmentSpecs) } else { $scopedAssignment.identity = @{ type = "None" } } # Add scope and notScopes(if defined) $scope = $scopeEntry.scope $id = "$scope/providers/Microsoft.Authorization/policyAssignments/$($assignmentEntry.name)" $scopedAssignment.id = $id $scopedAssignment.scope = $scope if ($scopeEntry.notScope.Length -gt 0) { $scopedAssignment.notScopes = @() + $scopeEntry.notScope } else { $scopedAssignment.notScopes = @() } # Add completed hashtable to collection $assignmentsList += $scopedAssignment } } } return $hasErrors, $assignmentsList } |