Private/Update-PurviewPolicyRoleMemberInternal.ps1
|
function Find-PrincipalConditionEntry { <# .SYNOPSIS Finds the condition entry in a rule's dnfCondition that matches the given attributeName. .DESCRIPTION Each attributeRule has a dnfCondition which is an array of OR clauses. Each OR clause is an array of AND conditions (AttributeMatchers). Common attributeNames in Purview metadata policies: - principal.microsoft.id : User or Service Principal Object IDs - principal.microsoft.groups.id : Entra ID Group Object IDs (transitive membership) - derived.purview.role : The role binding (e.g., purviewmetadatarole_builtin_collection-administrator) - derived.purview.permission : Inherited permissions from parent collections Returns the first matching condition term and its clause index, or $null if not found. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$RuleConfig, [Parameter(Mandatory = $true)] [string]$AttributeName ) if ($null -ne $RuleConfig.dnfCondition) { for ($clauseIdx = 0; $clauseIdx -lt $RuleConfig.dnfCondition.Count; $clauseIdx++) { $clause = $RuleConfig.dnfCondition[$clauseIdx] foreach ($term in $clause) { if ($term.attributeName -eq $AttributeName) { return @{ Term = $term; ClauseIndex = $clauseIdx } } } } } return $null } function Update-PurviewPolicyRoleMemberInternal { <# .SYNOPSIS Internal helper to add or remove a principal from a role's attributeRule in a Purview metadata policy. .DESCRIPTION Updates the dnfCondition array structure within the matching attributeRule. dnfCondition structure for a role rule (e.g., collection-administrator): [ [ # First OR clause - explicit principal assignment { attributeName: "principal.microsoft.id", attributeValueIncludedIn: ["guid1", "guid2"] }, { attributeName: "derived.purview.role", attributeValueIncludes: "purviewmetadatarole_builtin_..." } ], [ # Second OR clause - inherited from parent (read-only, managed by Purview) { attributeName: "derived.purview.permission", attributeValueIncludes: "..." } ] ] For groups, use attributeName "principal.microsoft.groups.id" instead of "principal.microsoft.id". #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$Policy, [Parameter(Mandatory = $true)] [string]$RoleId, [Parameter(Mandatory = $true)] [string]$PrincipalId, [Parameter(Mandatory = $true)] [ValidateSet('Add', 'Remove')] [string]$Action, [Parameter(Mandatory = $false)] [ValidateSet('User', 'Group')] [string]$PrincipalType = 'User' ) $updated = $false # Determine the attribute name based on principal type $principalAttributeName = switch ($PrincipalType) { 'User' { 'principal.microsoft.id' } 'Group' { 'principal.microsoft.groups.id' } } # Depending on API version, the rules might be on the root or under properties.attributeRules $rulesArray = $null if ($null -ne $Policy.properties -and $null -ne $Policy.properties.attributeRules) { $rulesArray = $Policy.properties.attributeRules } elseif ($null -ne $Policy.attributeRules) { $rulesArray = $Policy.attributeRules } if ($null -eq $rulesArray) { throw "Could not find 'attributeRules' in the policy object. The policy structure may be unexpected." } # Find the role rule $roleRule = $null foreach ($rule in $rulesArray) { $matched = $false if ($rule.id -eq $RoleId -or $rule.name -eq $RoleId) { $matched = $true } elseif ($null -ne $rule.dnfCondition) { foreach ($clause in $rule.dnfCondition) { foreach ($term in $clause) { if ($term.attributeName -eq 'derived.purview.role' -and $term.attributeValueIncludes -eq $RoleId) { $matched = $true break } } if ($matched) { break } } } if ($matched) { $roleRule = $rule break } } if ($null -eq $roleRule) { if ($Action -eq 'Add') { Write-Warning "Role ID '$RoleId' not found in policy. Cannot add." } return @{ Policy = $Policy; Updated = $false } } $principalConditionResult = Find-PrincipalConditionEntry -RuleConfig $roleRule -AttributeName $principalAttributeName if ($null -eq $principalConditionResult) { if ($Action -eq 'Add') { Write-Verbose "Could not find '$principalAttributeName' condition block for role '$RoleId'. Creating a new one..." # Create a new condition format exactly as expected by the payload. # dnfCondition is an array of OR clauses. Each OR clause is an array of AND conditions. # For a new principal assignment, we need: # - The principal ID condition # - The derived.purview.role binding condition $newPrincipalCondition = [ordered]@{ attributeName = $principalAttributeName attributeValueIncludedIn = @($PrincipalId) } $newRoleCondition = [ordered]@{ attributeName = 'derived.purview.role' attributeValueIncludes = $RoleId } if ($null -eq $roleRule.dnfCondition) { # Add entirely new dnfCondition structure with both conditions in the same AND clause $roleRule | Add-Member -MemberType NoteProperty -Name dnfCondition -Value @( ,@( $newPrincipalCondition, $newRoleCondition ) ) } else { # Find or create the first OR clause that has the role binding, then add the principal condition there # If there's already a clause with derived.purview.role, add to it; otherwise create a new clause $foundRoleClause = $false for ($i = 0; $i -lt $roleRule.dnfCondition.Count; $i++) { $clause = $roleRule.dnfCondition[$i] foreach ($term in $clause) { if ($term.attributeName -eq 'derived.purview.role') { # Found the role binding clause, add principal condition here $roleRule.dnfCondition[$i] = @($clause) + @($newPrincipalCondition) $foundRoleClause = $true break } } if ($foundRoleClause) { break } } if (-not $foundRoleClause) { # No existing role clause found, prepend a new OR clause $roleRule.dnfCondition = @( ,@( $newPrincipalCondition, $newRoleCondition ) ) + $roleRule.dnfCondition } } $updated = $true return @{ Policy = $Policy; Updated = $updated } } else { Write-Verbose "Principal condition block '$principalAttributeName' does not exist, nothing to remove." return @{ Policy = $Policy; Updated = $false } } } # Extract the actual condition term from the result $principalCondition = $principalConditionResult.Term # Ensure attributeValueIncludedIn is an array if ($null -eq $principalCondition.attributeValueIncludedIn) { $principalCondition.attributeValueIncludedIn = @() } # It could be a PSObject array, we work with it as an array list $currentMembers = @($principalCondition.attributeValueIncludedIn) if ($Action -eq 'Add') { if ($PrincipalId -notin $currentMembers) { $currentMembers += $PrincipalId $updated = $true } else { Write-Verbose "Principal '$PrincipalId' already in '$RoleId'." } } else { # Remove if ($PrincipalId -in $currentMembers) { $currentMembers = $currentMembers | Where-Object { $_ -ne $PrincipalId } $updated = $true } else { Write-Verbose "Principal '$PrincipalId' not found in '$RoleId'." } } if ($updated) { $principalCondition.attributeValueIncludedIn = @($currentMembers) Write-Verbose "Successfully updated members array." } return @{ Policy = $Policy; Updated = $updated } } |