Public/Add-PurviewCollectionRoleMember.ps1

function Add-PurviewCollectionRoleMember {
    <#
    .SYNOPSIS
    Adds a principal to a role in a Microsoft Purview collection's metadata policy.
 
    .DESCRIPTION
    Retrieves the current metadata policy for the specified collection, adds the principal to
    the designated role's attribute rule, then commits the updated policy back to Purview via PUT.
 
    Accepted principal types:
    - User (default): users and service principals, stored under 'principal.microsoft.id'
    - Group: Entra ID security groups with transitive membership, stored under
      'principal.microsoft.groups.id'
 
    This function is idempotent. If the principal already holds the role, no API write is made
    and the function completes silently. Use -Verbose to observe the no-op.
 
    .PARAMETER AccountName
    The name of the Microsoft Purview account (the subdomain portion of
    https://<AccountName>.purview.azure.com).
 
    .PARAMETER CollectionName
    The collection to assign the role on. Accepts either the 6-character system name
    (e.g. 'abc123') or the friendly display name (e.g. 'Finance Team'). Friendly names
    are resolved automatically via the account/collections API. Pass the system name
    directly to avoid that extra API call in performance-sensitive loops.
 
    .PARAMETER RoleId
    The fully-qualified Purview metadata role ID. Use Get-PurviewMetadataRole to list
    available roles. Built-in role IDs follow the pattern:
    'purviewmetadatarole_builtin_<role-name>'
 
    .PARAMETER PrincipalId
    The Entra ID Object ID (GUID) of the user, service principal, or group to assign.
 
    .PARAMETER PrincipalType
    Whether the principal is a 'User' (covers both users and service principals) or a
    'Group' (Entra ID security group). Defaults to 'User'. This controls which attribute
    condition in the policy JSON receives the principal ID.
 
    .PARAMETER SkipRoleValidation
    Skips the pre-flight API call that validates the RoleId exists. Useful in batch
    operations where the same validated role ID is reused across many collections.
 
    .OUTPUTS
    None. The function writes no output on success. Use -Verbose for operational detail.
 
    .NOTES
    Idempotent: safe to call multiple times with the same arguments.
    Requires an active Az.Accounts session (Connect-AzAccount or a managed identity context)
    with the Purview Collection Administrator role on the target collection.
 
    .EXAMPLE
    Add-PurviewCollectionRoleMember `
        -AccountName 'contoso-purview' `
        -CollectionName 'abc123' `
        -RoleId 'purviewmetadatarole_builtin_data-curator' `
        -PrincipalId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
 
    Adds a user or service principal to the Data Curator role on collection 'abc123'
    using the 6-character system name.
 
    .EXAMPLE
    Add-PurviewCollectionRoleMember `
        -AccountName 'contoso-purview' `
        -CollectionName 'Finance Team' `
        -RoleId 'purviewmetadatarole_builtin_data-curator' `
        -PrincipalId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
 
    Same assignment using the collection's friendly display name. The system name is
    resolved automatically before the policy is fetched.
 
    .EXAMPLE
    Add-PurviewCollectionRoleMember `
        -AccountName 'contoso-purview' `
        -CollectionName 'Finance Team' `
        -RoleId 'purviewmetadatarole_builtin_purview-reader' `
        -PrincipalId 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' `
        -PrincipalType Group
 
    Assigns an Entra ID security group as Purview Reader. Group membership is evaluated
    transitively by Purview at access time.
 
    .EXAMPLE
    $assignments = @(
        @{ Collection = 'Finance Team'; Role = 'purviewmetadatarole_builtin_data-curator'; Principal = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }
        @{ Collection = 'Engineering'; Role = 'purviewmetadatarole_builtin_purview-reader'; Principal = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' }
    )
    foreach ($a in $assignments) {
        Add-PurviewCollectionRoleMember `
            -AccountName 'contoso-purview' `
            -CollectionName $a.Collection `
            -RoleId $a.Role `
            -PrincipalId $a.Principal `
            -SkipRoleValidation
    }
 
    Batch IaC pattern. SkipRoleValidation avoids a redundant GET /metadataroles call on
    every iteration when the role IDs are known-good constants.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$AccountName,

        [Parameter(Mandatory = $true)]
        [string]$CollectionName,

        [Parameter(Mandatory = $true)]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

            $builtInRoles = @(
                'purviewmetadatarole_builtin_collection-administrator'
                'purviewmetadatarole_builtin_data-source-administrator'
                'purviewmetadatarole_builtin_data-curator'
                'purviewmetadatarole_builtin_purview-reader'
                'purviewmetadatarole_builtin_data-share-contributor'
                'purviewmetadatarole_builtin_policy-author'
                'purviewmetadatarole_builtin_workflow-administrator'
                'purviewmetadatarole_builtin_insights-reader'
            )

            $builtInRoles | Where-Object { $_ -like "$wordToComplete*" }
        })]
        [string]$RoleId,

        [Parameter(Mandatory = $true)]
        [string]$PrincipalId,

        [Parameter(Mandatory = $false)]
        [ValidateSet('User', 'Group')]
        [string]$PrincipalType = 'User',

        [Parameter(Mandatory = $false)]
        [switch]$SkipRoleValidation
    )

    # Validate role exists (unless skipped for performance)
    if (-not $SkipRoleValidation) {
        Write-Verbose "Validating role '$RoleId' exists..."
        if (-not (Test-PurviewMetadataRoleExists -AccountName $AccountName -RoleId $RoleId)) {
            $validRoles = (Get-PurviewMetadataRoleIds -AccountName $AccountName) -join "`n "
            throw "Role '$RoleId' does not exist in account '$AccountName'. Valid roles:`n $validRoles"
        }
    }

    Write-Verbose "Fetching metadata policy for collection: $CollectionName"
    $Policy = Get-PurviewMetadataPolicy -AccountName $AccountName -CollectionName $CollectionName

    $UpdateResult = Update-PurviewPolicyRoleMemberInternal -Policy $Policy -RoleId $RoleId -PrincipalId $PrincipalId -Action Add -PrincipalType $PrincipalType

    if ($UpdateResult.Updated) {
        Write-Verbose "Policy modified. Pushing update to Purview."

        $PolicyId = $UpdateResult.Policy.id
        if ([string]::IsNullOrWhiteSpace($PolicyId)) {
            throw "Could not determine Policy ID from the retrieved policy object."
        }

        if ($PSCmdlet.ShouldProcess("$AccountName/$CollectionName", "Add principal '$PrincipalId' ($PrincipalType) to role '$RoleId'")) {
            Update-PurviewMetadataPolicy -AccountName $AccountName -PolicyId $PolicyId -PolicyObject $UpdateResult.Policy
        }
    } else {
        Write-Verbose "No changes needed. Principal '$PrincipalId' already has role '$RoleId'."
    }
}