src/Types/New-XrmRolePrivilege.ps1

<#
    .SYNOPSIS
    Create a RolePrivilege object.

    .DESCRIPTION
    Instantiate a RolePrivilege object used to define a privilege with its depth for security role operations.
    The privilege is given by PrivilegeId, by PrivilegeName, or by EntityLogicalName + AccessRight (e.g. account + Write => prvWriteAccount).
    With ClampDepth, a depth the privilege does not support is replaced by the closest supported one: the highest supported depth below the requested one, else the lowest above it (e.g. Global only for organization-owned tables).

    .PARAMETER PrivilegeId
    Unique identifier of the privilege.

    .PARAMETER PrivilegeName
    Name of the privilege (e.g. "prvReadAccount"). Used to resolve the PrivilegeId automatically if PrivilegeId is not provided.

    .PARAMETER Depth
    Depth of the privilege (Basic, Local, Deep, Global).

    .PARAMETER BusinessUnitId
    Business unit unique identifier. Optional, defaults to Guid.Empty.

    .PARAMETER XrmClient
    Xrm connector initialized to target instance, used to resolve PrivilegeName. Use latest one by default. (Dataverse ServiceClient)
    Declared last to keep the existing positional parameters.

    .PARAMETER EntityLogicalName
    Table of the privilege, with AccessRight, instead of PrivilegeId or PrivilegeName.

    .PARAMETER AccessRight
    Access right of the privilege, with EntityLogicalName: Read, Write, Create, Delete, Append, AppendTo, Assign, Share.

    .PARAMETER ClampDepth
    Replace a depth the privilege does not support by the closest supported one, instead of keeping it as given.

    .OUTPUTS
    Microsoft.Crm.Sdk.Messages.RolePrivilege. The constructed RolePrivilege object.

    .EXAMPLE
    $priv = New-XrmRolePrivilege -PrivilegeName "prvReadAccount" -Depth Global;

    .EXAMPLE
    $priv = New-XrmRolePrivilege -PrivilegeId $privilegeId -Depth Local;

    .EXAMPLE
    # Local where the table supports it, Global for organization-owned tables
    $privileges = "account", "businessunit" | ForEach-Object { New-XrmRolePrivilege -XrmClient $xrmClient -EntityLogicalName $_ -AccessRight Read -Depth Local -ClampDepth };

    .LINK
    https://github.com/AymericM78/PowerDataOps/blob/main/documentation/commands/New-XrmRolePrivilege.md
#>

function New-XrmRolePrivilege {
    [CmdletBinding()]
    [OutputType([Microsoft.Crm.Sdk.Messages.RolePrivilege])]
    param
    (
        [Parameter(Mandatory = $false)]
        [Guid]
        $PrivilegeId,

        [Parameter(Mandatory = $false)]
        [string]
        $PrivilegeName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [Microsoft.Crm.Sdk.Messages.PrivilegeDepth]
        $Depth,

        [Parameter(Mandatory = $false)]
        [Guid]
        $BusinessUnitId = [Guid]::Empty,

        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $EntityLogicalName,

        [Parameter(Mandatory = $false)]
        [ValidateSet("Read", "Write", "Create", "Delete", "Append", "AppendTo", "Assign", "Share")]
        [string]
        $AccessRight,

        [Parameter(Mandatory = $false)]
        [switch]
        $ClampDepth
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        $byId = $PSBoundParameters.ContainsKey('PrivilegeId');
        $byName = $PSBoundParameters.ContainsKey('PrivilegeName');
        $byTable = $PSBoundParameters.ContainsKey('EntityLogicalName') -or $PSBoundParameters.ContainsKey('AccessRight');
        if (-not $byId -and -not $byName -and -not $byTable) {
            throw "You must provide either PrivilegeId, PrivilegeName, or EntityLogicalName and AccessRight.";
        }
        if ($byTable -and -not ($PSBoundParameters.ContainsKey('EntityLogicalName') -and $PSBoundParameters.ContainsKey('AccessRight'))) {
            throw "EntityLogicalName and AccessRight go together.";
        }

        $resolvedId = $PrivilegeId;
        $resolvedName = $PrivilegeName;
        $resolvedDepth = $Depth;

        # The privilege definition is read for a table + access right, or to know the supported depths
        $needsDefinition = $ClampDepth -or (-not $byId -and -not $byName);
        if (-not $needsDefinition -and -not $byId) {
            # Resolve PrivilegeId from PrivilegeName
            $query = New-XrmQueryExpression -LogicalName "privilege" -Columns "privilegeid", "name" -TopCount 1;
            $query = $query | Add-XrmQueryCondition -Field "name" -Condition Equal -Values @($PrivilegeName);
            $results = $XrmClient | Get-XrmMultipleRecords -Query $query;
            $privRecord = $results | Select-Object -First 1;
            if (-not $privRecord) {
                throw "Privilege '$PrivilegeName' not found.";
            }
            $resolvedId = $privRecord.privilegeid;
        }
        elseif ($needsDefinition) {
            if ($byId) {
                $candidates = @(Get-XrmPrivileges -XrmClient $XrmClient -Id $PrivilegeId);
                $description = "Privilege '$PrivilegeId'";
            }
            elseif ($byName) {
                $candidates = @(Get-XrmPrivileges -XrmClient $XrmClient -Name $PrivilegeName);
                $description = "Privilege '$PrivilegeName'";
            }
            else {
                $candidates = @(Get-XrmPrivileges -XrmClient $XrmClient -EntityLogicalName $EntityLogicalName -AccessRight $AccessRight);
                $description = "$AccessRight privilege of table '$EntityLogicalName'";
            }
            if ($candidates.Count -eq 0) {
                throw "$description not found.";
            }
            if ($candidates.Count -gt 1) {
                throw "$description is ambiguous: $(($candidates | ForEach-Object { $_.Name }) -join ', ').";
            }
            $privilege = $candidates[0];
            $resolvedId = $privilege.Id;
            $resolvedName = $privilege.Name;

            if ($ClampDepth -and $privilege.SupportedDepths.Count -gt 0 -and $privilege.SupportedDepths -notcontains $Depth) {
                $lower = @($privilege.SupportedDepths | Where-Object { [int]$_ -lt [int]$Depth } | Sort-Object { [int]$_ });
                $upper = @($privilege.SupportedDepths | Where-Object { [int]$_ -gt [int]$Depth } | Sort-Object { [int]$_ });
                $resolvedDepth = $(if ($lower.Count -gt 0) { $lower[-1] } else { $upper[0] });
                Write-Verbose "$($privilege.Name): depth $Depth is not supported, $resolvedDepth used.";
            }
        }

        $rolePrivilege = [Microsoft.Crm.Sdk.Messages.RolePrivilege]::new($resolvedDepth, $resolvedId, $BusinessUnitId);
        if ($resolvedName) {
            $rolePrivilege.PrivilegeName = $resolvedName;
        }
        $rolePrivilege;
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function New-XrmRolePrivilege -Alias *;

Register-ArgumentCompleter -CommandName New-XrmRolePrivilege -ParameterName "EntityLogicalName" -ScriptBlock {
    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)
    $validLogicalNames = Get-XrmEntitiesLogicalName;
    return $validLogicalNames | Where-Object { $_ -like "$wordToComplete*" };
}