Public/Set-CallerRBACAssignment.ps1

function Set-CallerRBACAssignment {
    <#
    .SYNOPSIS
        Determines, evaluates, and (optionally) applies the least-privilege RBAC
        a caller needs to run a command.
 
    .DESCRIPTION
        End-to-end PSAutoRBAC entry point. For a given platform command it:
          1. Resolves the minimum required role(s) from the knowledge base.
          2. Resolves the target Azure scope.
          3. Tests whether the caller already holds each required role.
          4. Generates idempotent grant / revoke snippets for any missing role.
          5. With -WhatIf it only reports; otherwise it grants missing roles
             (honouring -Confirm via ShouldProcess).
 
        Designed for Zero Trust / least-privilege provisioning: the framework
        runs as an identity holding only roleAssignments/write, and uses that to
        grant the caller exactly the roles needed for the operation -- and no more.
 
    .PARAMETER Tenant
        The Entra tenant id (recorded on output; not required for offline use).
 
    .PARAMETER SubscriptionId
        The Azure subscription id. Used to build the scope when -Scope is absent.
 
    .PARAMETER ResourceGroupName
        Optional resource group, used to build a resource-group scope.
 
    .PARAMETER CallerId
        The caller identity (UPN / sign-in name) the roles are evaluated for.
 
    .PARAMETER Scope
        Explicit Azure scope. Overrides scope built from subscription / group.
 
    .PARAMETER Platform
        The execution platform, e.g. 'Azure PowerShell'.
 
    .PARAMETER Command
        The command whose RBAC requirement is being evaluated.
 
    .PARAMETER ArgumentList
        Optional command arguments (recorded on output for traceability).
 
    .PARAMETER MapPath
        Optional alternate knowledge-base path (chiefly for testing).
 
    .PARAMETER RoleAssignment
        Optional pre-fetched role assignments for offline evaluation.
 
    .EXAMPLE
        Set-CallerRBACAssignment -Tenant $tid -SubscriptionId $sid `
            -CallerId 'patrick.gallucci@microsoft.com' -Platform 'Azure PowerShell' `
            -Command 'Connect-AzAccount' -WhatIf
 
        Reports the Reader requirement and whether the caller holds it, without
        making any change.
 
    .OUTPUTS
        PSCustomObject per required role with UPN, Role, State, scope, the command
        context, and AddScript / RemoveScript snippets.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType([pscustomobject])]
    param(
        [Parameter()]
        [string]$Tenant,

        [Parameter()]
        [string]$SubscriptionId,

        [Parameter()]
        [string]$ResourceGroupName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$CallerId,

        [Parameter()]
        [string]$Scope,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Platform,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Command,

        [Parameter()]
        [Alias('Args')]
        [object[]]$ArgumentList,

        [Parameter()]
        [string]$MapPath,

        [Parameter()]
        [object[]]$RoleAssignment
    )

    # 1. Minimum requirement.
    $reqParams = @{ Platform = $Platform; Command = $Command }
    if ($MapPath) { $reqParams['MapPath'] = $MapPath }
    $requirement = Get-CommandRBACRequirement @reqParams

    # 2. Scope.
    $resolvedScope = Resolve-RBACScope -Scope $Scope -SubscriptionId $SubscriptionId -ResourceGroupName $ResourceGroupName

    # 3. Current state.
    $testParams = @{
        CallerId     = $CallerId
        RequiredRole = $requirement.Roles
        Scope        = $resolvedScope
    }
    if ($PSBoundParameters.ContainsKey('RoleAssignment')) {
        $testParams['RoleAssignment'] = $RoleAssignment
    }
    $states = Test-CallerRBACAssignment @testParams

    foreach ($state in $states) {
        $scripts = New-RBACAssignmentScript -CallerId $CallerId -Role $state.Role -Scope $resolvedScope

        # 4/5. Apply when missing and not in WhatIf mode.
        $applied = $false
        if (-not $state.State) {
            if ($PSCmdlet.ShouldProcess("$CallerId at $resolvedScope", "Grant role '$($state.Role)'")) {
                if (Get-Command -Name 'New-AzRoleAssignment' -ErrorAction SilentlyContinue) {
                    New-AzRoleAssignment -SignInName $CallerId -RoleDefinitionName $state.Role -Scope $resolvedScope -ErrorAction Stop | Out-Null
                    $applied = $true
                }
                else {
                    Write-Warning "New-AzRoleAssignment unavailable; emitting AddScript only. Run it from an identity with roleAssignments/write."
                }
            }
        }

        [pscustomobject]@{
            PSTypeName   = 'PSAutoRBAC.CallerAssignment'
            Tenant       = $Tenant
            UPN          = $CallerId
            Platform     = $requirement.Platform
            Command      = $Command
            ArgumentList = $ArgumentList
            Scope        = $resolvedScope
            Role         = $state.Role
            State        = $state.State
            IsKnown      = $requirement.IsKnown
            Applied      = $applied
            Notes        = $requirement.Notes
            AddScript    = $scripts.AddScript
            RemoveScript = $scripts.RemoveScript
        }
    }
}