A PowerShell module for self-service activation and deactivation of PIM roles and group memberships across three surfaces:
| Surface | Noun | Cmdlet prefix |
|---|---|---|
| Azure AD / Entra ID directory roles | DirectoryRole |
OPIM |
| Azure resource (RBAC) roles | AzureRole |
OPIM |
| Entra ID PIM groups | EntraIDGroup |
OPIM |
Originally created by Justin Grote @justinwgrote. Overhauled and maintained by Omnicit.
Install-Module Omnicit.PIM
Import-Module Omnicit.PIM# Connect (request only what you need)
Connect-MgGraph -Scopes 'RoleEligibilitySchedule.ReadWrite.Directory',
'RoleAssignmentSchedule.ReadWrite.Directory',
'AdministrativeUnit.Read.All'
# List eligible roles
Get-OPIMDirectoryRole
# List active role assignments
Get-OPIMDirectoryRole -Activated
# List BOTH eligible and active in one call
Get-OPIMDirectoryRole -All
# Activate — tab-complete the role name
Enable-OPIMDirectoryRole <tab>
# Activate using positional params: Role (pos 0), Justification (pos 1), Hours (pos 2)
Enable-OPIMDirectoryRole 'Global Administrator (elig-id)' 'Incident response' 4
# Activate by schedule ID (from Get-OPIMDirectoryRole id property)
Enable-OPIMDirectoryRole -Identity 'elig-001'
# Activate all eligible roles for 4 hours with a justification
Get-OPIMDirectoryRole | Enable-OPIMDirectoryRole -Hours 4 -Justification 'Incident response'
# Deactivate by schedule instance ID (from Get-OPIMDirectoryRole -Activated id property)
Disable-OPIMDirectoryRole -Identity 'active-instance-001'
# Deactivate all active roles
Get-OPIMDirectoryRole -Activated | Disable-OPIMDirectoryRole
# Activate and wait for provisioning before continuing
Get-OPIMDirectoryRole | Enable-OPIMDirectoryRole -Wait# Connect
Connect-AzAccount
# List eligible roles (current user, all scopes)
Get-OPIMAzureRole
# List active role assignments
Get-OPIMAzureRole -Activated
# List BOTH eligible and active in one call
Get-OPIMAzureRole -All
# List eligible roles at a specific subscription scope
Get-OPIMAzureRole -Scope '/subscriptions/00000000-...'
# List active roles at a specific scope (exact scope match only)
Get-OPIMAzureRole -Activated -Scope '/subscriptions/00000000-...'
# Activate — tab-complete the role name
Enable-OPIMAzureRole <tab>
# Activate using positional params: Role (pos 0), Justification (pos 1), Hours (pos 2)
Enable-OPIMAzureRole 'Contributor -> My Subscription (elig-name)' 'Incident response' 4
# Activate by schedule Name (the Name property from Get-OPIMAzureRole)
Enable-OPIMAzureRole -Identity 'elig-schedule-name'
# Deactivate by schedule instance Name (from Get-OPIMAzureRole -Activated)
Disable-OPIMAzureRole -Identity 'active-schedule-name'
# Deactivate all active roles
Get-OPIMAzureRole -Activated | Disable-OPIMAzureRole# Connect (additional scope required)
Connect-MgGraph -Scopes 'RoleEligibilitySchedule.ReadWrite.Directory',
'PrivilegedEligibilitySchedule.ReadWrite.AzureADGroup'
# List eligible group memberships/ownerships
Get-OPIMEntraIDGroup
# List active group assignments
Get-OPIMEntraIDGroup -Activated
# List BOTH eligible and active in one call
Get-OPIMEntraIDGroup -All
# Filter by access type
Get-OPIMEntraIDGroup -AccessType member
# Activate — tab-complete the group name
Enable-OPIMEntraIDGroup <tab>
# Activate using positional params: Group (pos 0), Justification (pos 1), Hours (pos 2)
Enable-OPIMEntraIDGroup 'Finance Team - member (elig-id)' 'Project work' 2
# Activate by schedule ID (from Get-OPIMEntraIDGroup id property)
Enable-OPIMEntraIDGroup -Identity 'elig-001'
# Activate all eligible group assignments
Get-OPIMEntraIDGroup | Enable-OPIMEntraIDGroup -Hours 2 -Justification 'Project work'
# Deactivate by schedule instance ID (from Get-OPIMEntraIDGroup -Activated id property)
Disable-OPIMEntraIDGroup -Identity 'active-instance-001'
# Deactivate all active group assignments
Get-OPIMEntraIDGroup -Activated | Disable-OPIMEntraIDGroupEnable-OPIMMyRoles (alias: pim) is the all-in-one activation command. It connects to
Microsoft Graph (and Azure if Azure roles are configured) and activates all eligible directory
roles, PIM group assignments, and Azure RBAC roles for the current user.
# Activate all eligible roles/groups for 1 hour (prompts for Graph login if not connected)
pim
# Activate using a named tenant alias looked up in TenantMap.psd1, for 4 hours
pim -TenantAlias contoso -Hours 4 -Justification 'Incident response'
# Wait until directory role activations are fully provisioned
pim -TenantAlias corp -WaitThe default activation duration is 1 hour. Override persistently:
$PSDefaultParameterValues['Enable-OPIM*:Hours'] = 4The four *-OPIMConfiguration cmdlets manage the TenantMap.psd1 file that pim uses to
resolve tenant aliases:
| Cmdlet | Alias | Purpose |
|---|---|---|
Install-OPIMConfiguration |
— | Create — add a new alias. Error if alias already exists. |
Get-OPIMConfiguration |
Get-PIMConfig |
Read — return one typed object per alias. |
Set-OPIMConfiguration |
Set-PIMConfig |
Update — change TenantId or replace stored role lists. |
Remove-OPIMConfiguration |
Remove-PIMConfig |
Delete — remove an alias, preserve the rest. |
# Register a new tenant alias
Install-OPIMConfiguration -TenantAlias contoso -TenantId '00000000-0000-0000-0000-000000000000'
# Register and store specific directory roles as the default activation set
Get-OPIMDirectoryRole | Where-Object { $_.roleDefinition.displayName -like 'Compliance*' } |
Install-OPIMConfiguration -TenantAlias contoso -TenantId '<guid>'
# Preview without writing
Install-OPIMConfiguration -TenantAlias contoso -TenantId '<guid>' -WhatIfInstall is create-only. If the alias already exists a non-terminating error is emitted. Use
Set-OPIMConfigurationto update an existing alias.
# List all tenant aliases
Get-OPIMConfiguration
# Inspect a specific alias
Get-OPIMConfiguration -TenantAlias contoso
# Use a custom file path
Get-OPIMConfiguration -TenantMapPath 'D:\config\MyTenants.psd1'# Update only the TenantId, preserve stored role lists
Set-OPIMConfiguration -TenantAlias contoso -TenantId '<new-guid>'
# Replace the stored DirectoryRoles list
Get-OPIMDirectoryRole | Where-Object { $_.roleDefinition.displayName -like '*Admin*' } |
Set-OPIMConfiguration -TenantAlias contoso
# Replace the stored EntraIDGroups list
Get-OPIMEntraIDGroup | Set-OPIMConfiguration -TenantAlias contoso
# Preview without writing
Set-OPIMConfiguration -TenantAlias contoso -TenantId '<new-guid>' -WhatIf# Remove the 'contoso' alias (other aliases are preserved)
Remove-OPIMConfiguration -TenantAlias contoso
# Preview without writing
Remove-OPIMConfiguration -TenantAlias contoso -WhatIfThe TenantMap is a PowerShell data file (.psd1) that maps short tenant aliases to Azure Tenant
IDs. Each alias can optionally store a default set of roles and groups to activate, so pim only
activates what you actually need rather than everything eligible.
The TenantAlias is the key. You can change the TenantId (e.g. after a tenant migration) by running
Set-OPIMConfiguration -TenantAlias <same-alias> -TenantId <new-guid>without losing your stored role/group configuration.
$env:USERPROFILE\.config\Omnicit.PIM\TenantMap.psd1
Each entry is a nested hashtable under the alias key. The only required field is TenantId;
the role/group arrays are optional — omit them and pim will activate all eligible items:
@{
# Alias 'corp' — activates only the two stored directory roles and one group
'corp' = @{
TenantId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
DirectoryRoles = @('e8611ab8-c189-46e8-94e1-60213ab1f814') # roleDefinitionId
EntraIDGroups = @('75b93f19-07b0-4d87-8b7f-6bd04d79f023_member') # groupId_accessId
AzureRoles = @('schedule-name-from-get-opimazurerole')
}
# Alias 'partner' — no role list: activates ALL eligible items at login
'partner' = @{
TenantId = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
}
}The file is safe to edit manually — it is standard PowerShell data file syntax.
| Type | Stored value | Field on Get-OPIM* object |
|---|---|---|
| Directory Role | roleDefinitionId |
$_.roleDefinitionId |
| Entra ID Group | "{groupId}_{accessId}" |
"$($_.groupId)_$($_.accessId)" |
| Azure Role | Schedule name | $_.Name |
These identifiers are stable across eligibility renewals. The accessId in the group key is
either member or owner, so you can store member and owner eligibility for the same group
independently.
# Add a new tenant alias with no role defaults (activates all eligible at runtime)
Install-OPIMConfiguration -TenantAlias contoso -TenantId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
# Add a second tenant
Install-OPIMConfiguration -TenantAlias fabrikam -TenantId 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
# Update the TenantId for an existing alias (role lists are preserved)
Set-OPIMConfiguration -TenantAlias contoso -TenantId '<new-guid>'
# Read back the current configuration
Get-OPIMConfiguration
# Remove an alias (other aliases are preserved)
Remove-OPIMConfiguration -TenantAlias fabrikam
# Preview without writing
Install-OPIMConfiguration -TenantAlias contoso -TenantId '<guid>' -WhatIfPipe Get-OPIM* output (optionally filtered with Where-Object) to store exactly which
roles/groups pim should activate for a tenant. Both eligible (default) and activated
(-Activated) objects are accepted — useful for piping your currently active roles as the
default set. -TenantMap is implied; -TenantAlias and -TenantId are required.
# Store all eligible directory roles for this tenant
Get-OPIMDirectoryRole |
Install-OPIMConfiguration -TenantAlias contoso -TenantId '<guid>'
# Store only specific directory roles (by display name pattern)
Get-OPIMDirectoryRole |
Where-Object { $_.roleDefinition.displayName -in 'Compliance Administrator','User Administrator' } |
Install-OPIMConfiguration -TenantAlias contoso -TenantId '<guid>'
# Store currently active groups as defaults (pipe from -Activated)
Get-OPIMEntraIDGroup -Activated |
Install-OPIMConfiguration -TenantAlias contoso -TenantId '<guid>'
# Store eligible PIM group memberships only (not ownerships)
Get-OPIMEntraIDGroup -AccessType member |
Install-OPIMConfiguration -TenantAlias contoso -TenantId '<guid>'
# Store specific Azure RBAC roles
Get-OPIMAzureRole |
Where-Object { $_.RoleDefinitionDisplayName -like 'Contributor*' } |
Install-OPIMConfiguration -TenantAlias contoso -TenantId '<guid>'
# Update directory roles and groups incrementally using Set-OPIMConfiguration
Get-OPIMDirectoryRole |
Where-Object { $_.roleDefinition.displayName -like '*Admin*' } |
Set-OPIMConfiguration -TenantAlias contoso
Get-OPIMEntraIDGroup |
Set-OPIMConfiguration -TenantAlias contosoTip: Pipe new role/group objects to
Set-OPIMConfigurationto replace the stored list for that category. Categories not supplied via pipeline retain their existing values. To update only theTenantIdwithout touching role lists, runSet-OPIMConfigurationwith-TenantIdand no pipeline input.
# One-off override
pim -TenantAlias contoso -TenantMapPath 'D:\config\MyTenants.psd1'
# Permanent: add to your profile
$PSDefaultParameterValues['Enable-OPIMMyRoles:TenantMapPath'] = 'D:\config\MyTenants.psd1'
$PSDefaultParameterValues['Install-OPIMConfiguration:TenantMapPath'] = 'D:\config\MyTenants.psd1'
$PSDefaultParameterValues['Get-OPIMConfiguration:TenantMapPath'] = 'D:\config\MyTenants.psd1'
$PSDefaultParameterValues['Set-OPIMConfiguration:TenantMapPath'] = 'D:\config\MyTenants.psd1'
$PSDefaultParameterValues['Remove-OPIMConfiguration:TenantMapPath'] = 'D:\config\MyTenants.psd1'# ── First-time setup (run once) ───────────────────────────────────────────────
# 1. Register tenant aliases
Install-OPIMConfiguration -TenantAlias corp -TenantId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
Install-OPIMConfiguration -TenantAlias partner -TenantId 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
# 2. Connect to the corp tenant and configure default roles
Connect-MgGraph -TenantId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -Scopes `
'RoleEligibilitySchedule.ReadWrite.Directory', `
'RoleAssignmentSchedule.ReadWrite.Directory', `
'PrivilegedEligibilitySchedule.ReadWrite.AzureADGroup', `
'PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup', `
'AdministrativeUnit.Read.All'
Get-OPIMDirectoryRole |
Where-Object { $_.roleDefinition.displayName -in 'Compliance Administrator','Security Reader' } |
Set-OPIMConfiguration -TenantAlias corp
Get-OPIMEntraIDGroup -AccessType member |
Set-OPIMConfiguration -TenantAlias corp
# ── Daily use ─────────────────────────────────────────────────────────────────
# Activate only the stored roles in corp tenant
pim -TenantAlias corp -Hours 8 -Justification 'Daily operations'
# Activate everything eligible in partner tenant (no stored role list)
pim -TenantAlias partner -Hours 2 -Justification 'Partner review'For backwards compatibility and convenience, short PIM-prefixed aliases are available:
| Canonical cmdlet | Aliases |
|---|---|
Get-OPIMDirectoryRole |
Get-PIMADRole, Get-PIMRole |
Enable-OPIMDirectoryRole |
Enable-PIMADRole, Enable-PIMRole |
Disable-OPIMDirectoryRole |
Disable-PIMADRole, Disable-PIMRole |
Wait-OPIMDirectoryRole |
Wait-PIMADRole, Wait-PIMRole |
Get-OPIMAzureRole |
Get-PIMResourceRole |
Enable-OPIMAzureRole |
Enable-PIMResourceRole |
Disable-OPIMAzureRole |
Disable-PIMResourceRole |
Get-OPIMEntraIDGroup |
Get-PIMGroup |
Enable-OPIMEntraIDGroup |
Enable-PIMGroup |
Disable-OPIMEntraIDGroup |
Disable-PIMGroup |
Enable-OPIMMyRoles |
pim |
The default activation period is 1 hour. Override per-call with -Hours, or make it persistent:
$PSDefaultParameterValues['Enable-OPIM*:Hours'] = 4Or add to your profile: $PSDefaultParameterValues['Enable-OPIM*:Hours'] = 4
All three Enable-OPIM* cmdlets accept positional arguments in this order:
| Position | Parameter | Example |
|---|---|---|
| 0 | -RoleName / -GroupName |
'Global Administrator (elig-id)' |
| 1 | -Justification |
'Incident response' |
| 2 | -Hours |
4 |
# Explicit
Enable-OPIMDirectoryRole -RoleName 'Global Administrator (elig-id)' -Justification 'Incident' -Hours 4
# Positional (identical result)
Enable-OPIMDirectoryRole 'Global Administrator (elig-id)' 'Incident' 4All Get-OPIM* cmdlets support three modes. -All and -Activated are mutually exclusive:
| Command | Returns |
|---|---|
Get-OPIMDirectoryRole |
Eligible (inactive) roles only |
Get-OPIMDirectoryRole -Activated |
Currently active role assignments |
Get-OPIMDirectoryRole -All |
Both eligible and active for the current user |
The same applies to Get-OPIMEntraIDGroup and Get-OPIMAzureRole.
Note:
-Allreturns both schedule types for the current user. It does not list other users' roles. Both result types are returned with their correct TypeNames so Format views apply.
Every Enable-OPIM* and Disable-OPIM* cmdlet accepts -Identity to target a specific schedule
by ID without needing tab completion:
# Get the ID of an eligible role
Get-OPIMDirectoryRole | Select-Object id, @{n='Role';e={$_.roleDefinition.displayName}}
# Activate by ID
Enable-OPIMDirectoryRole -Identity 'elig-001'
# Deactivate by ID (use the id from -Activated output)
Get-OPIMDirectoryRole -Activated | Select-Object id, @{n='Role';e={$_.roleDefinition.displayName}}
Disable-OPIMDirectoryRole -Identity 'active-instance-001'For Azure RBAC roles the identity is the Name property (not id):
Get-OPIMAzureRole | Select-Object Name, RoleDefinitionDisplayName, ScopeId
Enable-OPIMAzureRole -Identity 'eligible-schedule-name'
Get-OPIMAzureRole -Activated | Select-Object Name, RoleDefinitionDisplayName
Disable-OPIMAzureRole -Identity 'active-schedule-name'Get-OPIMDirectoryRole and Get-OPIMEntraIDGroup accept an OData -Filter string for
server-side filtering. Common examples:
# Filter by role definition (Directory roles)
Get-OPIMDirectoryRole -Filter "roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'"
# Filter by group ID (Entra ID Groups)
Get-OPIMEntraIDGroup -Filter "groupId eq '00000000-0000-0000-0000-000000000000'"
# Filter by principal (requires elevated permissions)
Get-OPIMDirectoryRole -Filter "principalId eq '00000000-0000-0000-0000-000000000000'"
# -Identity is shorthand for id eq '<value>' filter
Get-OPIMDirectoryRole -Identity 'elig-001'
# equivalent to:
Get-OPIMDirectoryRole -Filter "id eq 'elig-001'"All activation and deactivation commands support -WhatIf and -Confirm:
Get-OPIMDirectoryRole | Enable-OPIMDirectoryRole -WhatIfThis module distinguishes:
- Eligible — a role assignment you can activate but haven't yet
- Activated — an eligible role you have explicitly turned on for a time window
- Active (persistent) — a role that is always on (outside scope of this module)
Use -Activated on the Get-OPIM* cmdlets to see currently active assignments.
| Dependency | Purpose |
|---|---|
Microsoft.Graph.Authentication 2.36+ |
Directory roles and Entra ID group PIM (raw Invoke-MgGraphRequest) |
Az.Resources 9.0.3+ |
Azure resource (RBAC) roles |
This module uses Sampler + ModuleBuilder for compilation. The key distinction between source mode and compiled mode is:
| Source mode | Compiled mode | |
|---|---|---|
| Import | Import-Module ./Source/Omnicit.PIM.psd1 -Force |
Import-Module ./output/module/Omnicit.PIM/<ver>/Omnicit.PIM.psd1 |
| Functions | Dot-sourced at runtime by Omnicit.PIM.psm1 |
Merged into a single Omnicit.PIM.psm1 by ModuleBuilder |
| Type data | Loaded by Omnicit.PIM.psm1 |
Loaded by suffix.ps1 (appended to built psm1) |
| Format data | Loaded by Omnicit.PIM.psm1 |
Loaded by suffix.ps1 (appended to built psm1) |
The source psm1 is a source-mode-only loader. Its contents are discarded during a build. ModuleBuilder replaces it entirely with a compiled file that merges all Classes/, Private/, and Public/ files in load order.
Do not put runtime initialization logic here expecting it to run in the compiled module. Use suffix.ps1 instead.
ModuleBuilder appends suffix.ps1 to the compiled psm1 verbatim (configured in build.yaml as suffix: suffix.ps1). This is the correct place for any initialization that must run at module import time in the compiled module — type data registration, format data registration, alias setup, etc.
A prefix.ps1 (not currently used) would be prepended to the compiled psm1 in the same way.
# Bootstrap dependencies (first time)
./build.ps1 -ResolveDependency -Tasks noop
# Compile the module
./build.ps1
# Run Pester tests + PSScriptAnalyzer
./build.ps1 -AutoRestore -Tasks test
# Import from source for interactive development
Import-Module ./Source/Omnicit.PIM.psd1 -ForceThis module is a fork/overhaul of JAz.PIM by Justin Grote @justinwgrote, released under the MIT License.