PIM.Graph.psm1

function Invoke-PimGraphRequest {
    <#
    .SYNOPSIS
        Execute a graph request.
     
    .DESCRIPTION
        Execute a graph request.
        Wrapper command around Invoke-MgGraphRequest with better output processing.
     
    .PARAMETER Uri
        Relative link to call.
        Passed through to Invoke-MgGraphRequest.
        If no 'beta' or 'v1.0' prefix is used, it automatically injects 'v1.0'
     
    .PARAMETER Method
        What REST Method to call.
        Defaults to GET.
     
    .PARAMETER Body
        A body to pass to the request.
     
    .EXAMPLE
        PS C:\> Invoke-PimGraphRequest me
 
        Retrieves information about the current user.
    #>

    [CmdletBinding()]
    param (
        [string]
        $Uri,

        [string]
        $Method = 'GET',

        [hashtable]
        $Body
    )

    if ($Uri -notmatch '^v1.0/|^beta/') {
        $Uri = 'v1.0/{0}' -f $Uri
    }

    $param = @{
        Method = $Method
        ErrorAction = 'Stop'
    }
    if ($Body) { $param.Body = $Body }

    $nextLink = $Uri
    while ($nextLink) {
        $result = Invoke-MgGraphRequest @param -Uri $nextLink
        if ($result.value) {
            foreach ($entry in $result.value) {
                if ($entry -isnot [hashtable]) { $entry }
                else { [PSCustomObject]$entry }
            }
        }
        elseif ($result.Keys.Count -eq 2 -and $result.Keys -contains 'value') {
            # Do nothing, there are no results
        }
        else {
            if ($result -isnot [Hashtable]) { $result }
            else { [PSCustomObject]$result }
        }
        $nextLink = $result.'@odata.nextlink' -replace '^https://graph.microsoft.com/'
    }
}

function Resolve-User {
    <#
    .SYNOPSIS
        Resolves a user into an ID
     
    .DESCRIPTION
        Resolves a user into an ID
     
    .PARAMETER Identity
        ID or UPN or mail of the user to resolve.
     
    .PARAMETER Me
        Whether to retrieve the ID of the current user.
     
    .EXAMPLE
        PS C:\> Resolve-User -Me
 
        Retrieve the ID of the current user
 
    .EXAMPLE
        PS C:\> Resolve-User -Identity max.mustermann@contoso.com
 
        Retrieve the ID of max.mustermann@contoso.com
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Identity')]
        [string]
        $Identity,

        [Parameter(Mandatory = $true, ParameterSetName = 'Me')]
        [switch]
        $Me
    )

    process {
        if ($Me) {
            try { (Invoke-PimGraphRequest -Uri 'me' -ErrorAction Stop).Id }
            catch { $PSCmdlet.ThrowTerminatingError($_) }
            return
        }

        if ($Identity -as [guid]) {
            return $Identity
        }

        try { $user = Invoke-PimGraphRequest -Uri "users?`$select=id&`$filter=userPrincipalName eq '$Identity' or mail eq '$Identity'" -ErrorAction Stop }
        catch { $PSCmdlet.ThrowTerminatingError($_) }

        if (-not $user) { throw "User not found: $user" }
        $user.id
    }
}

function Enable-PIMRole
{
    <#
    .SYNOPSIS
        Activate a temporary Role membership.
     
    .DESCRIPTION
        Activate a temporary Role membership.
 
        Scopes Needed:
        RoleAssignmentSchedule.ReadWrite.Directory
     
    .PARAMETER Role
        The role to activate.
     
    .PARAMETER TicketNumber
        The ticket number associated with the privilege activation.
     
    .PARAMETER Reason
        The reason you require the role to be activated
     
    .PARAMETER Duration
        For how long the role should be active.
        Must be at least 5 minutes, maximum duration is defined in PIM.
        Defaults to 8 hours.
     
    .PARAMETER StartTime
        When the activation should start.
        Defaults to "now"
     
    .PARAMETER TicketSystem
        What ticket system is associated with the ticket number offered.
        Defaults to 'N/A'
     
    .PARAMETER DirectoryScope
        What scope the the activation applies to.
        Defaults to '/'.
     
    .EXAMPLE
        PS C:\> Enable-PIMRole 'Global Administrator' '#1234' 'Updating global tenant settings.'
 
        Enables the 'Global Administrator' role for 8 hours.
 
    .LINK
        https://learn.microsoft.com/en-us/graph/api/rbacapplication-post-roleassignmentschedulerequests?view=graph-rest-1.0&tabs=http
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Role,

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

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

        [timespan]
        $Duration = "08:00:00",

        [datetime]
        $StartTime = (Get-Date),

        [string]
        $TicketSystem = "N/A",

        [string]
        $DirectoryScope = "/"
    )
    
    process
    {
        $resolvedRole = Resolve-PIMRole -Identity $Role
        $body = @{
            action = "SelfActivate"
            principalId = (Invoke-MgGraphRequest -Uri "v1.0/me").id
            roleDefinitionId = $resolvedRole
            directoryScopeId = $DirectoryScope
            justification = $Reason
            scheduleInfo = @{
                startDateTime = $StartTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
                expiration = @{
                    type = "AfterDuration"
                    duration = "PT$($Duration.TotalMinutes)M"
                }
            }
            ticketInfo = @{
                ticketNumber = $TicketNumber
                ticketSystem = $TicketSystem
            }
        }
        try { Invoke-PimGraphRequest -Uri "v1.0/roleManagement/directory/roleAssignmentScheduleRequests" -Method POST -Body $body -ErrorAction Stop }
        catch { $PSCmdlet.ThrowTerminatingError($_) }
    }
}


function Get-PIMRole {
    <#
    .SYNOPSIS
        Search AAD for directory roles.
     
    .DESCRIPTION
        Search AAD for directory roles.
 
        Scopes:
        RoleManagement.Read.Directory, Directory.Read.All, RoleManagement.ReadWrite.Directory, Directory.ReadWrite.All
     
    .PARAMETER Name
        The name to filter the roles by.
     
    .EXAMPLE
        PS C:\> Get-PIMRole
 
        Retrieve all active roles.
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Name = '*'
    )
    
    process {
        Invoke-PimGraphRequest -Uri "v1.0/directoryRoles" | Where-Object displayName -Like $Name
    }
}


function Get-PIMRoleAssignment {
    <#
    .SYNOPSIS
        Retrieve permanent role assignments.
     
    .DESCRIPTION
        Retrieve permanent role assignments.
 
        Scopes Needed: RoleManagement.Read.Directory
     
    .PARAMETER Role
        Role for which to find assignees.
     
    .PARAMETER User
        User for which to retrieve assignments.
        Specify either "me" for the current user or UPN/mail of specific user.
     
    .EXAMPLE
        PS C:\> Get-PIMRoleAssignment
 
        Retrieve ALL role assignments.
 
    .EXAMPLE
        PS C:\> Get-PIMRoleAssignment -User me
 
        Retrieve all role assignments of the current user.
 
    .EXAMPLE
        PS C:\> Get-PIMRoleAssignment -Role 'Global Administrator'
 
        Retrieve all memberships in the 'Global Administrator' role.
 
    .LINK
        https://learn.microsoft.com/en-us/graph/api/rbacapplication-list-roleassignments?view=graph-rest-1.0&tabs=http
    #>

    [CmdletBinding()]
    param (
        [string]
        $Role,

        [string]
        $User
    )

    begin {
        $filterSegments = @()
        if ($Role) {
            $roleID = Resolve-PIMRole -Identity $Role
            $filterSegments += "roleDefinitionId eq '$roleID'"
        }
        if ($User) {
            if ('me' -eq $User) { $userID = Resolve-User -Me }
            else { $userID = Resolve-User -Identity $User }
            $filterSegments += "principalId eq '$userID'"
        }
        $filterString = ''
        if ($filterSegments) {
            $filterString = '&$filter={0}' -f ($filterSegments -join ' and ')
        }
    }
    process {
        $results = Invoke-PimGraphRequest -Uri "v1.0/roleManagement/directory/roleAssignments?`$expand=principal$filterString"
        foreach ($result in $results) {
            [PSCustomObject]@{
                # General Info
                RoleID         = $result.roleDefinitionId
                PrincipalID    = $result.principalId
                DirectoryScope = $result.directoryScopeId

                # Principal Details
                PrincipalName  = $result.principal.displayName
                PrincipalType  = $result.principal.'@odata.type' -replace '#microsoft\.graph\.'

                # Assignment data
                AssignmentID   = $result.id
                Principal      = $result.principal
            }
        }
    }
}

function Get-PIMRoleRequest {
    <#
    .SYNOPSIS
        Retrieve previously submitted role elevation requests.
     
    .DESCRIPTION
        Retrieve previously submitted role elevation requests.
        Returns both requests created in the Portal and those created by commandline.
 
        Scopes needed (least to most privileged):
        RoleEligibilitySchedule.Read.Directory, RoleManagement.Read.Directory, RoleManagement.Read.All, RoleEligibilitySchedule.ReadWrite.Directory, RoleManagement.ReadWrite.Directory
     
    .PARAMETER Role
        Role for which to retrieve elevation requests.
     
    .PARAMETER User
        User for which to retrieve elevation requests
     
    .EXAMPLE
        PS C:\> Get-PIMRoleRequest -User me
         
        Retrieve all requests for the current account.
 
    .EXAMPLE
        PS C:\> Get-PIMRoleRequest -Role 'Global Administrator'
 
        Retrieve all requests for Global Admin
 
    .LINK
        https://learn.microsoft.com/en-us/graph/api/rbacapplication-list-roleeligibilityschedulerequests?view=graph-rest-1.0&tabs=http
    #>

    [CmdletBinding()]
    param (
        [string]
        $Role,

        [string]
        $User
    )

    begin {
        function Get-ExpirationTime {
            [CmdletBinding()]
            param (
                $ScheduleInfo
            )

            if ($ScheduleInfo.expiration.endDateTime) {
                return $ScheduleInfo.expiration.endDateTime
            }

            $start = $ScheduleInfo.startDateTime
            $duration = $ScheduleInfo.expiration.duration -replace '^PT'
            $end = $start
            $minutes = $duration -replace '^.{0,}?(\d+)M.{0,}$', '$1'
            if ($minutes -and $minutes -ne $duration) { $end = $end.AddMinutes($minutes) }
            $hours = $duration -replace '^.{0,}?(\d+)H.{0,}$', '$1'
            if ($hours -and $hours -ne $duration) { $end = $end.AddHours($hours) }
            $end
        }

        $filterSegments = @()
        if ($Role) {
            $roleID = Resolve-PIMRole -Identity $Role
            $filterSegments += "roleDefinitionId eq '$roleID'"
        }
        if ($User) {
            if ('me' -eq $User) { $userID = Resolve-User -Me }
            else { $userID = Resolve-User -Identity $User }
            $filterSegments += "principalId eq '$userID'"
        }
        $filterString = ''
        if ($filterSegments) {
            $filterString = '&$filter={0}' -f ($filterSegments -join ' and ')
        }
    }
    process {
        $requests = Invoke-PimGraphRequest -Uri "roleManagement/directory/roleAssignmentScheduleRequests?`$expand=principal$($filterString)"
        foreach ($request in $requests) {
            [PSCustomObject]@{
                PSTypeName         = 'PIM.Graph.RoleRequest'

                # IDs
                RequestID          = $request.id
                PrincipalID        = $request.principalId
                RoleID             = $request.roleDefinitionId
                
                # State
                Action             = $request.action
                Status             = $request.status

                # Schedule
                Start              = $request.scheduleInfo.startDateTime
                End                = Get-ExpirationTime -ScheduleInfo $request.scheduleInfo
                ExpirationType     = $request.scheduleInfo.expiration.type
                ExpirationDuration = $request.scheduleInfo.expiration.duration
                ExpirationTime     = $request.scheduleInfo.expiration.endDateTime

                Created            = $request.createdDateTime
                Completed          = $request.completedDateTime

                # Metadata
                Reason             = $request.justification
                TicketNumber       = $request.ticketInfo.ticketNumber
                TicketSystem       = $request.ticketInfo.ticketSystem

                # Principal
                PrincipalType      = $request.principal.'@odata.type' -replace '#microsoft\.graph\.'
                PrincipalName      = $request.principal.displayName
                PrincipalUPN       = $request.principal.userPrincipalName

                # Role
                Role               = Resolve-PIMRole -Identity $request.roleDefinitionId -AsName -Lenient

                Data               = $request
            }
        }
    }
}

function Resolve-PIMRole
{
    <#
    .SYNOPSIS
        Resolve a role by ID or name.
     
    .DESCRIPTION
        Resolve a role by ID or name.
        Uses role providers to do the resolving with, some of which are provided out of the box:
        - builtin: Provides the default IDs for the builtin roles (such as Global Administrator)
        - manual: Allows manually mapping name to ID using Set-PIMRoleMapping.
        - Get-PIMRole: Uses Get-PIMRole to retrieve active roles from Azure AD.
          This requires having the correct scopes and permissions to retrieve them.
 
        For more details on Role Providers, see the following commands:
        - Get-PIMRoleProvider: List available Role Providers.
        - Set-PIMRoleProvider: Modify existing Role Providers (most notably: Disable or enable)
        - Register-PIMRoleProvider: Create a new Role Provider
        - Unregister-PIMRoleProvider: Remove an existing Role Provider
     
    .PARAMETER Identity
        Role to resolve.
     
    .PARAMETER AsName
        Resolve to name rather than ID.
     
    .PARAMETER Lenient
        In case of not finding anything, return the specified Identity, rather than throwing an exception.
     
    .EXAMPLE
        PS C:\> Resolve-PIMRole -Identity 'Global Administrator'
 
        Returns the ID of the Global Administrator role.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Identity,

        [switch]
        $AsName,

        [switch]
        $Lenient
    )
    
    process
    {
        # If no resolution is required and ID is provided, return ID
        if (-not $AsName -and $Identity -as [Guid]) { return $Identity }

        $providers = Get-PIMRoleProvider -Enabled | Sort-Object Priority
        foreach ($provider in $providers) {
            Write-Verbose "Resolving $Identity through $($provider.Name)"
            try {
                $result = & $provider.Conversion $Identity $AsName
                if ($result) { return $result }
            }
            catch {
                Write-Verbose "Error resolving $Identity through $($provider.Name): $_"
            }
        }
        if ($Lenient) { return $Identity }
        throw "Unable to resolve $Identity"
    }
}


function Stop-PIMRoleRequest {
    <#
    .SYNOPSIS
        Cancels a pending role request.
     
    .DESCRIPTION
        Cancels a pending role request.
 
        Scopes needed (least to most privileged):
        RoleEligibilitySchedule.ReadWrite.Directory, RoleManagement.ReadWrite.Directory
     
    .PARAMETER ID
        ID of the request to cancel.
     
    .EXAMPLE
        PS C:\> Get-PIMRoleRequest -User me | Where-Object Status -eq Granted | Stop-PIMRoleRequest
         
        Cancels all role requests still pending for the current user
 
    .LINK
        https://learn.microsoft.com/en-us/graph/api/unifiedroleeligibilityschedulerequest-cancel?view=graph-rest-1.0&tabs=http
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('RequestID')]
        [string[]]
        $ID
    )

    process {
        foreach ($requestID in $ID) {
            try { Invoke-PimGraphRequest -Method POST -Uri "v1.0/roleManagement/directory/roleAssignmentScheduleRequests/$requestID/cancel" -ErrorAction Stop }
            catch { $PSCmdlet.WriteError($_) }
        }
    }
}

function Get-PIMRoleProvider {
    <#
    .SYNOPSIS
        Lists all registered Role Providers.
     
    .DESCRIPTION
        Lists all registered Role Providers.
        Role Providers are plugins that allow resolving role names using the logic provided within.
     
    .PARAMETER Name
        Name of the Role Provider to retrieve.
        Defaults to '*'
     
    .PARAMETER Enabled
        Only return enabled Role Providers.
     
    .EXAMPLE
        PS C:\> Get-PIMRoleProvider
         
        Lists all registered Role Providers.
 
    .EXAMPLE
        PS C:\> Get-PIMRoleProvider -Enabled
         
        Lists all enabled Role Providers.
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Name = '*',

        [switch]
        $Enabled
    )

    process {
        $enabledSet = $PSBoundParameters.ContainsKey('Enabled')
        ($script:roleProviders.Values) | Where-Object {
            $_.Name -Like $Name -and
            (-not $enabledSet -or $_.Enabled -eq $Enabled)
        }
    }
}

function Register-PIMRoleProvider {
    <#
    .SYNOPSIS
        Register a new Role Provider.
     
    .DESCRIPTION
        Register a new Role Provider.
        Role Providers are plugins that allow resolving role names using the logic provided within.
     
    .PARAMETER Name
        Name of the provider to create.
        Must be unique, otherwise it will overwrite an existing Provider.
     
    .PARAMETER Conversion
        Logic that processes input into results.
        The scriptblock must accept two parameters:
        - Identity
        - AsName
        Identity is the string input to convert.
        AsName is a boolean, whether to return the displayname of a role.
        By default, this scriptblock should be returning the ID.
     
    .PARAMETER ListNames
        A logic that, without any input, should return a list of role names.
        This is used for tab completion and you may leave this empty.
        Try to avoid including long-running logic or implement caching.
     
    .PARAMETER Description
        Description of the Role Provider.
        Used to give the user some impression of what and how it does.
     
    .PARAMETER Priority
        The priority of the Role Provider.
        The lower the number, the earlier it is executed.
        The first successful role resolution wins, causing Role Providers with a higher number to be skipped.
        Slower Role Providers should usually have a higher number.
        Defaults to 50.
     
    .PARAMETER Enabled
        Whether the Role Provider should be enabled.
        Only enabled Providers are used when resolving a role.
        Defaults to $true
     
    .EXAMPLE
        PS C:\> Resolve-PIMRoleProvider -Name 'custom-DB' -Conversion $conversion -ListNames { } -Priority 40
         
        Registers the 'custom-DB' Role Provider with the specified conversion logic, an empty name listing logic and priority 40.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Conversion,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ListNames,

        [string]
        $Description,

        [int]
        $Priority = 50,

        [bool]
        $Enabled = $true
    )

    $script:roleProviders[$Name] = [PSCustomObject]@{
        PSTypeName  = 'PIM.Graph.RoleProvider'
        Name        = $Name
        Conversion  = $Conversion
        ListNames   = $ListNames
        Priority    = $Priority
        Enabled     = $Enabled
        Description = $Description
    }
}

function Set-PIMRoleMapping {
    <#
    .SYNOPSIS
        Maps a role name to a role ID.
     
    .DESCRIPTION
        Maps a role name to a role ID.
        This allows manually defining how a name should be resolved, enabling ...
        - Role resolution without any scopes / connection required.
        - Defining aliases / shortcuts for frequently resolved roles
     
    .PARAMETER Name
        Name of the role.
        May either be the full name or an abbreviation as desired.
     
    .PARAMETER ID
        ID the name maps to.
     
    .PARAMETER Register
        Whether the mapping should be remembered across sessions.
     
    .EXAMPLE
        PS C:\> Set-PIMRoleMapping -Name GA -ID 62e90394-69f5-4237-9190-012177145e10 -Register
         
        Creates a permanent role name alias for the Global Administrator
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

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

        [switch]
        $Register
    )
    
    process {
        $script:manuallyMappedRoles[$ID] = $Name

        if (-not $Register) { return }

        $folder = Join-Path $env:APPDATA 'PowerShell/PIM.Graph'
        if (-not (Test-Path -Path $folder)) {
            $null = New-Item -Path $folder -Force -ItemType Directory
        }

        $rolesPath = "$folder/roles.clixml"
        if (Test-Path -Path $rolesPath) {
            $roles = Import-Clixml -Path $rolesPath
        }
        else {
            $roles = @{ }
        }
        $roles[$ID] = $Name
        $roles | Export-Clixml -Path $rolesPath
    }
}


function Set-PIMRoleProvider {
    <#
    .SYNOPSIS
        Modifies an existing Role Provider.
     
    .DESCRIPTION
        Modifies an existing Role Provider.
        Role Providers are plugins that allow resolving role names using the logic provided within.
     
    .PARAMETER Name
        Name of the Provider to modify.
     
    .PARAMETER Conversion
        The conversion logic that reslves names to ID.
     
    .PARAMETER ListNames
        The logic listing all available names for tab completion purposes.
     
    .PARAMETER Priority
        The priority of the Role Provider.
        The lower the number, the earlier it is executed.
        The first successful role resolution wins, causing Role Providers with a higher number to be skipped.
        Slower Role Providers should usually have a higher number.
     
    .PARAMETER Enabled
        Whether the Role Provider should be enabled.
        Only enabled Providers are used when resolving a role.
     
    .EXAMPLE
        PS C:\> Set-PIMRoleProvider -Name Get-PIMRole -Enabled $false
 
        Disables the Role Provider 'Get-PIMRole'
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name,

        [scriptblock]
        $Conversion,

        [scriptblock]
        $ListNames,

        [int]
        $Priority,

        [bool]
        $Enabled
    )
    
    process {
        foreach ($providerName in $Name) {
            $provider = $script:roleProviders[$providerName]
            if (-not $provider) {
                Write-Error "Provider not found: $providerName"
                continue
            }

            if ($Conversion) { $provider.Conversion = $Conversion }
            if ($ListNames) { $provider.ListNames = $ListNames }
            if ($PSBoundParameters.ContainsKey('Priority')) { $provider.Priority = $Priority }
            if ($PSBoundParameters.ContainsKey('Enabled')) { $provider.Enabled = $Enabled }
        }
    }
}


function Unregister-PIMRoleProvider {
    <#
    .SYNOPSIS
        Remove an existing Role Provider.
     
    .DESCRIPTION
        Remove an existing Role Provider.
        Role Providers are plugins that allow resolving role names using the logic provided within.
     
    .PARAMETER Name
        Name of the Role Provider to remove
     
    .EXAMPLE
        PS C:\> Unregister-PIMRoleProvider -Name Get-PIMRole
         
        Removes the 'Get-PIMRole' Role Provider
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name
    )
    process {
        foreach ($providerName in $Name) {
            $script:roleProviders.Remove($providerName)
        }
    }
}

# Registered role providers, logic resolving and listing roles
$script:roleProviders = @{ }

# Roles that come with every tenant. Their roleTemplateId is global across all tenants.
$script:defaultBuiltinRoles = @{
    '62e90394-69f5-4237-9190-012177145e10' = 'Global Administrator'
    'd29b2b05-8046-44ba-8758-1e26182fcf32' = 'Directory Synchronization Accounts'
    '88d8e3e3-8f55-4a1e-953a-9b9898b8876b' = 'Directory Readers'
    'e6d1a23a-da11-4be4-9570-befc86d067a7' = 'Compliance Data Administrator'
    'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' = 'SharePoint Administrator'
    '5d6b6bb7-de71-4623-b4af-96380a352509' = 'Security Reader'
    '69091246-20e8-4a56-aa4d-066075b2a7a8' = 'Teams Administrator'
    'f70938a0-fc10-4177-9e90-2178f8765737' = 'Teams Communications Support Engineer'
    '2b499bcd-da44-4968-8aec-78e1674fa64d' = 'Device Managers'
    '194ae4cb-b126-40b2-bd5b-6091b380977d' = 'Security Administrator'
    'baf37b3a-610e-45da-9e62-d9d1e5e8914b' = 'Teams Communications Administrator'
    '17315797-102d-40b4-93e0-432062caca18' = 'Compliance Administrator'
    'f2ef992c-3afb-46b9-b7cf-a126ee74c451' = 'Global Reader'
    '29232cdf-9323-42fd-ade2-1d097af3e4de' = 'Exchange Administrator'
    'a9ea8996-122f-4c74-9520-8edcd192826c' = 'Power BI Administrator'
    '9360feb5-f418-4baa-8175-e2a00bac4301' = 'Directory Writers'
}

# Mapping of manually defined roles
$script:manuallyMappedRoles = @{ }
$manualRolesPath = Join-Path $env:APPDATA 'PowerShell/PIM.Graph/roles.clixml'
if (Test-Path $manualRolesPath) {
    try { $script:manuallyMappedRoles = Import-Clixml -Path $manualRolesPath -ErrorAction Stop }
    catch { Write-Warning "Error loading roles mapping configuration file. File may be corrupt. Delete or repair the file. Path: $manualRolesPath" }
}

$conversion = {
    param (
        $Identity,

        $AsName
    )

    foreach ($pair in $script:defaultBuiltinRoles.GetEnumerator()) {
        if ($AsName) {
            if ($pair.Key -eq $Identity) { return $pair.Value }
        }
        else {
            if ($pair.Value -eq $Identity) { return $pair.Key }
        }
    }
}
$listnames = {
    $script:defaultBuiltinRoles.Values
}

$param = @{
    Name       = 'builtin'
    Conversion = $conversion
    ListNames  = $listnames
    Priority   = 2
    Enabled    = $true
    Description = 'A static mapping of the common builtin roles.'
}

Register-PIMRoleProvider @param

$conversion = {
    param (
        $Identity,

        $AsName
    )

    $roles = Get-PIMRole
    if ($AsName) {
        $roles | Where-Object {
            $_.Id -eq $Identity -or
            $_.roleTemplateId -eq $Identity
        } | Select-Object -First 1 | ForEach-Object displayName
    }
    else {
        $roles | Where-Object displayName -EQ $Identity | Select-Object -First 1 | ForEach-Object displayName
    }
}
$listnames = {
    (Get-PIMRole).displayName
}

$param = @{
    Name       = 'Get-PIMRole'
    Conversion = $conversion
    ListNames  = $listnames
    Priority   = 60
    Enabled    = $true
    Description = 'Uses Get-PIMRole to resolve roles against graph. Requires scope RoleManagement.Read.Directory'
}

Register-PIMRoleProvider @param

$conversion = {
    param (
        $Identity,

        $AsName
    )

    foreach ($pair in $script:manuallyMappedRoles.GetEnumerator()) {
        if ($AsName) {
            if ($pair.Key -eq $Identity) { return $pair.Value }
        }
        else {
            if ($pair.Value -eq $Identity) { return $pair.Key }
        }
    }
}
$listnames = {
    $script:manuallyMappedRoles.Values
}

$param = @{
    Name        = 'manual'
    Conversion  = $conversion
    ListNames   = $listnames
    Priority    = 1
    Enabled     = $true
    Description = 'Allows manually defining a name-to-role mapping using Set-PIMRoleMapping.'
}

Register-PIMRoleProvider @param

#region Role Names
$completion = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    $providers = Get-PIMRoleProvider -Enabled
    $names = foreach ($provider in $providers) {
        & $provider.ListNames
    }
    $names | Sort-Object -Unique | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
        if ($_ -match "\s") { "'$_'" }
        else { $_ }
    }
}
Register-ArgumentCompleter -CommandName Resolve-PIMRole -ParameterName Identity -ScriptBlock $completion
Register-ArgumentCompleter -CommandName Enable-PIMRole -ParameterName Role -ScriptBlock $completion
Register-ArgumentCompleter -CommandName Get-PIMRoleAssignment -ParameterName Role -ScriptBlock $completion
#endregion Role Names

#region Role Provider
$completion = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    (Get-PIMRoleProvider -Name "$wordToComplete*").Name | ForEach-Object {
        if ($_ -match "\s") { "'$_'" }
        else { $_ }
    }
}
Register-ArgumentCompleter -CommandName Get-PIMRoleProvider -ParameterName Name -ScriptBlock $completion
Register-ArgumentCompleter -CommandName Set-PIMRoleProvider -ParameterName Name -ScriptBlock $completion
Register-ArgumentCompleter -CommandName Unregister-PIMRoleProvider -ParameterName Name -ScriptBlock $completion
#endregion Role Provider