Graph.EasyPIM.psm1
function Enable-PIMRole { param( [Parameter(Mandatory=$false)] [Alias("SkipReason")] [switch]$SkipJustification, [Parameter(Mandatory=$false)] [Alias("Reason")] [string]$Justification, [Parameter(Mandatory=$false)] [string]$TicketingSystem ) <# .DESCRIPTION Enable Entra ID PIM roles via an easy to use TUI (Text User Interface). Only supports enabling; not disabling. .PARAMETER SkipJustification Optional. If specified, it sets the reason/ justifaction for activation to be "xxx". .PARAMETER Justification Optional. If specified, it sets the reason/ justifaction for activation to whatever is input. .PARAMETER TicketingSystem Optional. If specified, it sets the tickting system (for role activations that need a ticket number) to be whatever is input. #> begin { $requiredScopesArray = "RoleEligibilitySchedule.Read.Directory","RoleEligibilitySchedule.ReadWrite.Directory","RoleManagement.Read.Directory","RoleManagement.Read.All","RoleAssignmentSchedule.ReadWrite.Directory","RoleManagement.ReadWrite.Directory","RoleAssignmentSchedule.Remove.Directory" try { Connect-MgGraph -Scopes $requiredScopesArray -NoWelcome -ErrorAction Stop } catch { throw "$($_.Exception.Message)" } $context = Get-MgContext $scopes = $context.scopes if ($scopes -notcontains "Directory.ReadWrite.All") { foreach ($requiredScope in $requiredScopesArray) { if ($requiredScope -notin $scopes) { Write-Warning "Required scope '$requiredScope' missing" } } } $userId = (Get-MgUser -UserId $context.Account).Id try { Write-Progress -Activity "Fetching all eligibile Entra ID roles" [array]$myEligibleRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -ExpandProperty RoleDefinition -All -Filter "principalId eq '$userId'" -ErrorAction Stop Write-Progress -Activity "Fetching all active Entra ID roles" [array]$myActiveRoles = Get-MgRoleManagementDirectoryRoleAssignmentSchedule -ExpandProperty RoleDefinition -All -Filter "principalId eq '$userId'" -ErrorAction Stop } catch { throw "Error fetching roles: $($_.Exception.Message)" } # Create a cache of assignments. This is faster as I can lookup a bunch of them beforehand. $policyAssignmentHash = @{} # I must set scopeId to '/' coz if I search for a specific scopeId it errors: Attempted to perform an unauthorized operation. $searchSnippetMain = "scopeType eq 'DirectoryRole' and scopeId eq '/' and (" $searchSnippetsArray = @() # Filter has a max length (not sure what) so I will do it in batches of 5. # A temp variable I keep incrementing $counter = 0 # Total number of entries for this scope $totalCount = $myEligibleRoles.Count # Loop through the entries foreach ($roleObj in $myEligibleRoles) { Write-Progress -Activity "Fetching policies assigned to roles" -Id 0 $counter++ $roleDefinitionId = $roleObj.RoleDefinitionId # An array where I keep adding the snippets $searchSnippetsArray += "roleDefinitionId eq '$roleDefinitionId'" # In batches of 5, or if the counter has reached the end... if ($counter % 5 -eq 0 -or $counter -ge $totalCount) { # ... construct the search snippet $searchSnippet = $searchSnippetMain + $($searchSnippetsArray -join ' or ') + ")" # Do the search Write-Progress -Activity "Fetching..." -ParentId 0 -Id 1 -Status "${counter}/${totalCount}" -PercentComplete $($counter*100/$totalCount) try { $policyAssignment = Get-MgPolicyRoleManagementPolicyAssignment -Filter $searchSnippet -ExpandProperty "policy(`$expand=rules)" -ErrorAction Stop } catch { throw "Error fetching policy assignments: $($_.Exception.Message)" } # And add it to the hash foreach ($result in $policyAssignment) { $policyAssignmentHash[$($result.RoleDefinitionId)] = $result } # Initialize the array again $searchSnippetsArray = @() } } # I tried to do the same for policies & rules, but couldn't get it working... I can't seem to filter on PolicyId or policyId or any other variants! Write-Progress -Id 1 -Completed Write-Progress -Id 0 -Completed } process { # Cache the policy expiration rules so I don't have to lookup each time. # I don't think this is really needed coz in my testing there seems to be a separate policy per role, but no harm done I suppose... useful when troubleshooting! $policyExpRulesCache = @{} $policyEnablementRulesCache = @{} $roleDefinitionsCache = @{} # I use these for showing progress [int]$counter = 0 [int]$totalCount = $myEligibleRoles.Count [array]$myActiveRoleIds = $myActiveRoles.RoleDefinitionId $roleStates = foreach ($roleObj in $myEligibleRoles) { $counter++ $percentageComplete = ($counter/$totalCount)*100 $roleDefinitionId = $roleObj.RoleDefinitionId $roleName = $roleObj.RoleDefinition.DisplayName $roleDefinitionsCache[$roleDefinitionId] = $roleName $timespanArray = @() $roleExpired = $false $roleAssignmentType = "Not Active" Write-Progress -Activity "Processing role '$roleName'" -Id 2 -PercentComplete $percentageComplete -Status "$counter/$totalCount" if ($roleDefinitionId -in $myActiveRoleIds) { $activeRoleObj = $myActiveRoles | Where-Object { $_.RoleDefinitionId -eq "$roleDefinitionId" } # Double checking coz during my testing I ran into instances where this was sometimes incomplete if ($activeRoleObj.ScheduleInfo.Expiration.EndDateTime) { $roleAssignmentType = $activeRoleObj.AssignmentType $timeSpan = New-TimeSpan -Start (Get-Date).ToUniversalTime() -End $activeRoleObj.ScheduleInfo.Expiration.EndDateTime if ($timeSpan.Days -gt 0) { if ($timeSpan.Days -eq 1) { $timespanArray += "$($timeSpan.Days) day" } else { $timespanArray += "$($timeSpan.Days) days" } } if ($timeSpan.Hours -gt 0) { if ($timeSpan.Hours -eq 1) { $timespanArray += "$($timeSpan.Hours) hour" } else { $timespanArray += "$($timeSpan.Hours) hours" } } if ($timeSpan.Minutes -gt 0) { if ($timeSpan.Minutes -eq 1) { $timespanArray += "$($timeSpan.Minutes) minute" } else { $timespanArray += "$($timeSpan.Minutes) minutes" } } # Just in case there's a delay between getting the states and when I calculate this... if ($timeSpan.Ticks -lt 0) { $roleExpired = $true } } else { $roleExpired = $true } } else { $roleExpired = $true } # Using the roledefinitionid, find the policy assignment on this role # https://learn.microsoft.com/en-us/graph/api/resources/unifiedrolemanagementpolicyassignment?view=graph-rest-1.0 <# $roleDirectoryScopeId = $roleObj.DirectoryScopeId Write-Progress -Activity "Fetching policy assignment of role '$roleName'" -Id 2 -PercentComplete $percentageComplete -Status "$counter/$totalCount" try { $policyAssignment = Get-MgPolicyRoleManagementPolicyAssignment -Filter "scopeId eq '$roleDirectoryScopeId' and scopeType eq 'DirectoryRole' and roleDefinitionId eq '$roleDefinitionId'" -ErrorAction Stop } catch { Write-Warning "Error fetching policy assignments for '$roleName': $($_.Exception.Message)" continue } #> # Skipping the above code as I now cache it before hand. This is faster than doing individual lookups. $policyAssignment = $policyAssignmentHash[$roleDefinitionId] # From there find the policy :) # https://learn.microsoft.com/en-us/graph/api/resources/unifiedrolemanagementpolicy?view=graph-rest-1.0 $policyId = $policyAssignment.PolicyId Write-Progress -Activity "Fetching policy id '$(($policyId -split '_')[2])'" -ParentId 2 -Id 3 try { $policyObj = Get-MgPolicyRoleManagementPolicy -UnifiedRoleManagementPolicyId $policyId -ExpandProperty Rules -ErrorAction Stop } catch { Write-Warning "Error fetching policy id '$policyId': $($_.Exception.Message)" continue } # The policy is what defines the max duration of the role and other factors. We are interested in here are the rules # https://learn.microsoft.com/en-us/graph/api/resources/unifiedrolemanagementpolicyrule?view=graph-rest-1.0 # If I have the rule already cached, use that if ($policyExpRulesCache.Keys -contains $policyId) { $expirationRule = $policyExpRulesCache.$policyId } else { # The 'Expiration_EndUser_Assignment' rule in the policy is what defines the maximum duration # https://learn.microsoft.com/en-us/graph/api/resources/unifiedrolemanagementpolicyexpirationrule?view=graph-rest-1.0 $expirationRule = ($policyObj.Rules | Where-Object { $_.Id -eq "Expiration_EndUser_Assignment" }).AdditionalProperties $policyExpRulesCache.$policyId = $expirationRule } if ($expirationRule.maximumDuration -match "^PT") { # Thanks https://stackoverflow.com/a/57296616 $timeSpan = [System.Xml.XmlConvert]::ToTimeSpan($expirationRule.maximumDuration) $maxDurationArray = @() if ($timeSpan.Days -gt 0) { if ($timeSpan.Days -eq 1) { $maxDurationArray += "$($timeSpan.Days) day" } else { $maxDurationArray += "$($timeSpan.Days) days" } } if ($timeSpan.Hours -gt 0) { if ($timeSpan.Hours -eq 1) { $maxDurationArray += "$($timeSpan.Hours) hour" } else { $maxDurationArray += "$($timeSpan.Hours) hours" } } if ($timeSpan.Minutes -gt 0) { if ($timeSpan.Minutes -eq 1) { $maxDurationArray += "$($timeSpan.Minutes) minute" } else { $maxDurationArray += "$($timeSpan.Minutes) minutes" } } # Just in case there's a delay between getting the states and when I calculate this... $maxDuration = $maxDurationArray -join ' ' } else { $maxDuration = $expirationRule.maximumDuration } # Repeat, but for the enablement rules if ($policyEnablementRulesCache.Keys -contains $policyId) { $enablementRule = $policyEnablementRulesCache.$policyId } else { # The 'Expiration_EndUser_Assignment' rule in the policy is what defines the maximum duration # https://learn.microsoft.com/en-us/graph/api/resources/unifiedrolemanagementpolicyexpirationrule?view=graph-rest-1.0 $enablementRule = ($policyObj.Rules | Where-Object { $_.Id -eq "Enablement_EndUser_Assignment" }).AdditionalProperties.enabledRules $policyEnablementRulesCache.$policyId = $expirationRule } Write-Progress -Completed -Id 3 [pscustomobject][ordered]@{ "RoleName" = $roleName "Status" = $roleAssignmentType "ExpiresIn" = if (!($roleExpired)) { $timespanArray -join ' ' } "MaxDuration" = $maxDuration "EnablementRules" = $enablementRule -join '|' -replace 'Justification','Reason' -replace 'Ticketing','Ticket' -replace 'MultiFactorAuthentication','MFA' "More" = [pscustomobject]@{ "RoleDefinitionId" = $roleObj.RoleDefinitionId "DirectoryScopeId" = $roleObj.DirectoryScopeId "MaxDuration" = $expirationRule.maximumDuration "EnablementRule" = $enablementRule } } } Write-Progress -Completed -Id 2 $userSelections = $roleStates | Out-ConsoleGridView # Lets ask for the required info upfront $justificationsHash = @{} $ticketSystemHash = @{} $ticketNumberHash = @{} foreach ($selection in $userSelections) { if ($selection.Status -eq "Not Active") { if ($selection.More.EnablementRule -contains "Justification") { Write-Host -NoNewline -ForegroundColor Yellow "'$($selection.RoleName)' " if ($SkipJustification) { $justificationsHash[$($selection.RoleName)] = "xxx" Write-Host "Reason will be set to: xxx" } elseif ($Justification.Length -ne 0) { $justificationsHash[$($selection.RoleName)] = $Justification Write-Host "Reason will be set to: $Justification" } else { $justificationInput = Read-Host "Please provide a reason" # If the justitication ends with an asterisk, use it for everything else that follows... if ($justificationInput -match '\*$') { $justificationInput = $justificationInput -replace '\*$','' $Justification = $justificationInput } $justificationsHash[$($selection.RoleName)] = $justificationInput } } if ($selection.More.EnablementRule -contains "Ticketing") { Write-Host -NoNewline -ForegroundColor Yellow "'$($selection.RoleName)' " $ticketNumberHash[$($selection.RoleName)] = Read-Host "Please provide a ticket number" if ($TicketingSystem.Length -ne 0) { Write-Host -NoNewline -ForegroundColor Yellow "'$($selection.RoleName)' " $ticketingSystemInput = Read-Host "Please provide the ticketing system name" # If the justitication ends with an asterisk, use it for everything else that follows... if ($ticketingSystemInput -match '\*$') { $ticketingSystemInput = $ticketingSystemInput -replace '\*$','' $TicketingSystem = $ticketingSystemInput } $ticketSystemHash[$($selection.RoleName)] = $ticketingSystemInput } else { $ticketSystemHash[$($selection.RoleName)] = $TicketingSystem } } } } $requestObjsArray = @() foreach ($selection in $userSelections) { Write-Host -NoNewline -ForegroundColor Yellow "'$($selection.RoleName)' " if ($selection.Status -ne "Not Active") { Write-Host "Skipping as its status is '$($selection.Status)'" } else { Write-Host "Enabling for $($selection.MaxDuration)" $params = @{ Action = "selfActivate" PrincipalId = $userId RoleDefinitionId = $selection.More.RoleDefinitionId DirectoryScopeId = $selection.More.DirectoryScopeId ScheduleInfo = @{ StartDateTime = Get-Date Expiration = @{ Type = "AfterDuration" Duration = $selection.More.MaxDuration } } } if ($selection.More.EnablementRule -contains "Justification") { $params.Justification = $justificationsHash[$($selection.RoleName)] } if ($selection.More.EnablementRule -contains "Ticketing") { $params.TicketInfo = @{ TicketNumber = $ticketNumberHash[$($selection.RoleName)] TicketSystem = $ticketSystemHash[$($selection.RoleName)] } } try { $requestObj = New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params -ErrorAction Stop # Show the output to screen <# $requestObj | Select-Object -Property @{ "Name" = "Role"; "Expression" = { $roleDefinitionsCache[$($_.RoleDefinitionId)] } },Status #> # And add it to an array so we can loop over in the end $requestObjsArray += $requestObj } catch { Write-Error "Error activating '$($selection.RoleName)': $($_.Exception.Message)" } } } if ($userSelections.Count -ne 0) { $counter = 0 $maxWaitSecs = 20 while ($counter -lt $maxWaitSecs) { Write-Progress "Waiting $maxWaitSecs seconds before showing the final status" -PercentComplete $($counter*100/$maxWaitSecs) -Status " " Start-Sleep -Seconds 1 $counter++ } } $finalOutput = foreach ($requestObj in $requestObjsArray) { Get-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -UnifiedRoleAssignmentScheduleRequestId $requestObj.Id | Select-Object -Property @{ "Name" = "Role"; "Expression" = { $roleDefinitionsCache[$($_.RoleDefinitionId)] } },Status } $finalOutput | Format-Table } } |