PIM.Graph.psm1
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-EntraRequest -Service $script:entraServices.Graph -Path 'me' -ErrorAction Stop).Id } catch { $PSCmdlet.ThrowTerminatingError($_) } return } if ($Identity -as [guid]) { return $Identity } $queryHash = @{ '$select' = 'id' '$filter' = "userPrincipalName eq '$Identity' or mail eq '$Identity'" } try { $user = Invoke-EntraRequest -Service $script:entraServices.Graph -Path "users" -Query $queryHash -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } if (-not $user) { throw "User not found: $user" } $user.id } } function Disable-PIMRole { <# .SYNOPSIS Disables / cancels a role membership activation. .DESCRIPTION Disables / cancels a role membership activation. Already active memberships must have been active for at least 5 minutes before being cancelled. .PARAMETER Role Name of the role, whose membership you want to disable again. Only applies to the current user. .PARAMETER RequestId Specific ID of the request that activated an eligible role membership. Can be from any role or user. .EXAMPLE PS C:\> Disable-PIMRole -Role 'Security Reader' Deactivates temporary role memberships of the current user and the role "Security Reader". .EXAMPLE PS C:\> Get-PIMRoleRequest -User example@contoso.onmicrosoft.com | Disable-PIMRole Deactivates all temporary role memberships active for example@contoso.onmicrosoft.com #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ParameterSetName = 'ByRole')] [string] $Role, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByID')] [string] $RequestId ) begin { Assert-EntraConnection -Service $script:entraServices.Graph -Cmdlet $PSCmdlet } process { if ($Role) { $requests = Get-PIMRoleRequest -Role $Role -User me -ErrorAction $ErrorActionPreference | Where-Object Action -eq 'selfActivate' } else { $requests = Get-PIMRoleRequest -RequestID $RequestID -ErrorAction $ErrorActionPreference } if (-not $requests) { return } foreach ($request in $requests) { if ($request.Action -ne 'selfActivate') { Write-Error "Only revoking privilege requests. $($request.RequestID): $($request.Action)" -TargetObject $request continue } # Case: Request Scheduled for the future if ($request.Status -eq 'Granted') { try { $null = Invoke-EntraRequest -Service $script:entraServices.Graph -Method POST -Path "roleManagement/directory/roleAssignmentScheduleRequests/$($request.RequestID)/cancel" -ErrorAction Stop } catch { $PSCmdlet.WriteError($_) } continue } # Case: Active Request Schedule $limit = (Get-Date).AddMinutes(-5) if ($request.Start -ge $limit) { Write-Error "Cannot cancel a request that has been open less than 5 minutes! $($request.Role) | $($request.Start)" continue } $body = [ordered]@{ action = "selfDeactivate" principalId = $request.PrincipalID roleDefinitionId = $request.RoleID directoryScopeId = $request.Data.directoryScopeId scheduleInfo = $request.Data.scheduleInfo } try { Invoke-EntraRequest -Service $script:entraServices.Graph -Path "roleManagement/directory/roleAssignmentScheduleRequests" -Method POST -Body $body -Header @{ 'Content-Type' = 'application/json' } -ErrorAction Stop } catch { $PSCmdlet.WriteError($_) } } } } 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 = "/" ) begin { Assert-EntraConnection -Service $script:entraServices.Graph -Cmdlet $PSCmdlet } process { $resolvedRole = Resolve-PIMRole -Identity $Role $body = [ordered]@{ action = "selfActivate" principalId = (Invoke-EntraRequest -Service $script:entraServices.Graph -Path "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-EntraRequest -Service $script:entraServices.Graph -Path "roleManagement/directory/roleAssignmentScheduleRequests" -Method POST -Body $body -Header @{ 'Content-Type' = 'application/json' } -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 = '*' ) begin { Assert-EntraConnection -Service $script:entraServices.Graph -Cmdlet $PSCmdlet } process { Invoke-EntraRequest -Service $script:entraServices.Graph -Path "roleManagement/directory/roleDefinitions" | Where-Object displayName -Like $Name | ForEach-Object { $_.PSObject.TypeNames.Insert(0, 'PIM.Graph.Role') $_ } } } 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. .PARAMETER AllUsers Retrieve all assignments for all users. .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 #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding()] param ( [string] $Role, [string] $User = 'me', [switch] $AllUsers ) begin { Assert-EntraConnection -Service $script:entraServices.Graph -Cmdlet $PSCmdlet $typeMap = @{ $true = 'Temporary' $false = 'Permanent' } $filterSegments = @() if ($Role) { $roleID = Resolve-PIMRole -Identity $Role $filterSegments += "roleDefinitionId eq '$roleID'" } if ($User -and -not $AllUsers) { if ('me' -eq $User) { $userID = Resolve-User -Me } else { $userID = Resolve-User -Identity $User } $filterSegments += "principalId eq '$userID'" } $queryHash = @{ '$expand' = "principal" } if ($filterSegments) { $queryHash['$filter'] = $filterSegments -join ' and ' } } process { $active = Invoke-EntraRequest -Service $script:entraServices.Graph -Path "roleManagement/directory/roleAssignments" -Query $queryHash $eligible = Invoke-EntraRequest -Service $script:entraServices.Graph -Path "roleManagement/directory/roleEligibilitySchedules" -Query $queryHash $roleHash = @{ } try { $roles = Get-PIMRole -ErrorAction Stop foreach ($roleItem in $roles) { $roleHash[$roleItem.templateId] = $roleItem } } catch { } foreach ($assignment in $active) { $eligibleItem = @($eligible).Where{ $_.principalId -eq $assignment.principalId -and $_.roleDefinitionId -eq $assignment.roleDefinitionId -and $_.directoryScopeId -eq $assignment.directoryScopeId } [PSCustomObject]@{ PSTypeName = 'PIM.Graph.RoleAssignment' # General Info RoleID = $assignment.roleDefinitionId RoleName = $roleHash[$assignment.roleDefinitionId].displayName PrincipalID = $assignment.principalId DirectoryScope = $assignment.directoryScopeId Type = $typeMap[($eligibleItem -as [bool])] # Principal Details PrincipalName = $assignment.principal.displayName PrincipalType = $assignment.principal.'@odata.type' -replace '#microsoft\.graph\.' # Assignment data AssignmentID = $assignment.id EligibilityID = $eligibleItem.id Principal = $assignment.principal } } foreach ($assignment in $eligible) { $activeItem = @($active).Where{ $_.principalId -eq $assignment.principalId -and $_.roleDefinitionId -eq $assignment.roleDefinitionId -and $_.directoryScopeId -eq $assignment.directoryScopeId } if ($activeItem) { continue } [PSCustomObject]@{ PSTypeName = 'PIM.Graph.RoleAssignment' # General Info RoleID = $assignment.roleDefinitionId RoleName = $roleHash[$assignment.roleDefinitionId].displayName PrincipalID = $assignment.principalId DirectoryScope = $assignment.directoryScopeId Type = 'Eligible' # Principal Details PrincipalName = $assignment.principal.displayName PrincipalType = $assignment.principal.'@odata.type' -replace '#microsoft\.graph\.' # Assignment data AssignmentID = $null EligibilityID = $assignment.id Principal = $assignment.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. Defaults to the current user. .PARAMETER Current Only include role activation requests that are currently active. .PARAMETER Include What requests to include, that would usually not be returned. - All: All of the below. - Expired: Requests that have already expired. Allows historic searches, as long as data is retained by entra. - Revoked: Requests that were active but have been revoked before their natural conclusion. - Canceled: Requests that were scheduled in the future and cancelled before taking effect. .PARAMETER AllUsers Search for requests from all users. .PARAMETER RequestID Retrieve a specific role request by its ID. .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(DefaultParameterSetName = 'Filter')] param ( [Parameter(ParameterSetName = 'Filter')] [string] $Role, [Parameter(ParameterSetName = 'Filter')] [string] $User = 'me', [Parameter(ParameterSetName = 'Filter')] [switch] $Current, [Parameter(ParameterSetName = 'Filter')] [ValidateSet('All', 'Expired', 'Revoked', 'Canceled')] [string[]] $Include, [Parameter(ParameterSetName = 'Filter')] [switch] $AllUsers, [Parameter(Mandatory = $true, ParameterSetName = 'ByID')] [string] $RequestID ) begin { Assert-EntraConnection -Service $script:entraServices.Graph -Cmdlet $PSCmdlet function Get-ExpirationTime { [CmdletBinding()] param ( $ScheduleInfo, [switch] $Utc ) 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) } if ($Utc) { $end } else { $end.ToLocalTime() } } $includeRevoked = $Include -contains 'All' -or $Include -contains 'Revoked' $includeCanceled = $Include -contains 'All' -or $Include -contains 'Canceled' $includeExpired = $Include -contains 'All' -or $Include -contains 'Expired' $requestParam = @{ Service = $script:entraServices.Graph Path = "roleManagement/directory/roleAssignmentScheduleRequests" Query = @{ '$expand' = "principal" } } if ($RequestID) { $requestParam.Path = "roleManagement/directory/roleAssignmentScheduleRequests/$RequestID" return # continues with Process } $filterSegments = @() if ($Role) { $roleID = Resolve-PIMRole -Identity $Role $filterSegments += "roleDefinitionId eq '$roleID'" } if ($User -and -not $AllUsers) { if ('me' -eq $User) { $userID = Resolve-User -Me } else { $userID = Resolve-User -Identity $User } $filterSegments += "principalId eq '$userID'" } if ($filterSegments) { $requestParam.Query['$filter'] = $filterSegments -join ' and ' } } process { $requests = Invoke-EntraRequest @requestParam foreach ($request in $requests | Sort-Object { $_.ScheduleInfo.startDateTime }) { if (-not $includeCanceled -and $request.status -eq 'Canceled' -and -not $RequestID) { continue } if (-not $includeRevoked -and -not $RequestID) { if ($request.status -eq 'Revoked') { continue } $revocation = @($requests).Where{ $_.status -eq 'Revoked' -and $_.principalId -eq $request.principalId -and $_.roleDefinitionId -eq $request.roleDefinitionId -and $_.scheduleInfo.startDateTime -eq $request.scheduleInfo.startDateTime -and $_.scheduleInfo.expiration.duration -eq $request.scheduleInfo.expiration.duration -and $_.scheduleInfo.expiration.endDateTime -eq $request.scheduleInfo.expiration.endDateTime } if ($revocation) { continue } } $start = $request.scheduleInfo.startDateTime.ToLocalTime() $end = Get-ExpirationTime -ScheduleInfo $request.scheduleInfo $now = Get-Date if ($end -lt $now -and -not $includeExpired -and -not $RequestID) { continue } if ($Current -and $start -gt $now) { continue } [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.ToLocalTime() StartUtc = $request.scheduleInfo.startDateTime End = Get-ExpirationTime -ScheduleInfo $request.scheduleInfo EndUtc = Get-ExpirationTime -ScheduleInfo $request.scheduleInfo -Utc 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 ) begin { Assert-EntraConnection -Service $script:entraServices.Graph -Cmdlet $PSCmdlet } 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 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" } } # Service to use with EntraAuth requests $script:entraServices = @{ Graph = 'Graph' } $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 templateId } } $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 Disable-PIMRole -ParameterName Role -ScriptBlock $completion Register-ArgumentCompleter -CommandName Enable-PIMRole -ParameterName Role -ScriptBlock $completion Register-ArgumentCompleter -CommandName Get-PIMRoleAssignment -ParameterName Role -ScriptBlock $completion Register-ArgumentCompleter -CommandName Get-PIMRoleRequest -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 |