In any case, the example script I use in the article demonstrates how to use the Get-MgDirectoryRoleMember cmdlet to find holders of the Exchange administrator and Global administrator roles. These are the people who need to run PowerShell against Exchange Online, so the script leaves their accounts intact. For anyone else, the script calls the Set-User cmdlet to disable PowerShell access. I suggest that the script is a good candidate for Azure Automation to make sure that new accounts can’t use PowerShell.
Privileged Identity Management
Everything works for most tenants. The problem is that some tenants use Azure AD Privileged Identity Management (PIM), an optional service that requires Azure AD Premium P2 licenses. PIM is most commonly used by large enterprises to control access to resources. Unlike normal open-ended permanent assignments to privileged roles like Exchange administrator, PIM allows the assignments to be time-limited on an on-demand basis.
To do this, PIM differentiates between eligible and active role assignments. An eligible role assignment is not currently effective. If needed, an administrator can activate the assignment to allow its holder to use the permissions available to active role holders. Assignments can be time-limited and expire after a certain period. A comment for the original article pointed out that it didn’t handle PIM assignments and the script is therefore unusable in tenants that use PIM.
If you look at role assignments through the Privileged Identity Management section of the Microsoft Entra admin center, you can see those with eligible, active, and expired assignments for the different roles used in the tenant. Figure 1 shows the active assignments for the Exchange administrator and Global administrator roles. You can see that some service principals are in the set of Exchange administrators. Azure Automation uses these service principals to allow managed identities to sign into Exchange Online and run cmdlets as an administrator.
Figure 1: PIM assignments for the Exchange administrator and Global administrator roles
The problem is that the Get-MgDirectoryRoleMember cmdlet only reports active role assignments. The assignments eligible for activation are ignored. For the purposes of this exercise, tenants using PIM must include accounts with eligible assignments when determining what accounts can access PowerShell.
Given that the AzureADPreview module is due for deprecation in March 2024, I looked for an equivalent Microsoft Graph PowerShell SDK cmdlet. Microsoft’s cmdlet map to help developers move from the Azure AD and MSOL modules to the SDK didn’t help. I had great hope for the Get-MgBetaRoleManagementDirectoryRoleAssignment cmdlet but the cmdlet appears to only return “normal” role assignments.
I took the original script and amended it to use Get-AzureADMSPrivilegedRoleAssignment to fetch the assignments known for the Global administrator and Exchange administrator roles.
Write-Output "Retrieving assignment information from Privileged Identity Management..."
# Get information about accounts holding Exchange administrator
[array]$ExoRoleMembers = Get-AzureADMSPrivilegedRoleAssignment -ProviderId aadRoles -ResourceId $TenantId -Filter "RoleDefinitionId eq '$($ExoAdminRoleId)'" -ErrorAction Stop | Select-Object RoleDefinitionId, SubjectId, StartDateTime, EndDateTime, AssignmentState, MemberType
If (!($ExoRoleMembers)) { Write-Output "Can't find any Exchange administrators! Exiting..." ; break }
# Do the same for global administrators
[array]$GARoleMembers = Get-AzureADMSPrivilegedRoleAssignment -ProviderId aadRoles -ResourceId $TenantId -Filter "RoleDefinitionId eq '$($GlobalAdminRoleId)'" -ErrorAction Stop | Select-Object RoleDefinitionId, SubjectId, StartDateTime, EndDateTime, AssignmentState, MemberType
If (!($GARoleMembers)) { Write-Output "Can't find any global administrators! Exiting..." ; break }
The script then loops through the arrays of assignments to fetch details of user account (with Get-MgUser) and members of groups used for PIM (with Get-MgGroupMember). The script stores information about the assignments that we can report (Figure 2).
Figure 2: Reporting PIM role assignments
The next step is to create an array of administrator user principal names to check against Exchange mailboxes. Basically, if a mailbox belongs to an administrator, we allow PowerShell access. If it doesn’t, we block PowerShell access.
[array]$ExoMailboxes = Get-ExoMailbox -Filter {CustomAttribute5 -eq $Null} -ResultSize Unlimited -RecipientTypeDetails UserMailbox -Properties CustomAttribute5
ForEach ($Mbx in $ExoMailboxes) {
# If not an admin holder, go ahead and block PowerShell
If ($Mbx.userPrincipalName -notin $AdminAccounts) {
Write-Output ("Blocking PowerShell access for mailbox {0}..." -f $Mbx.displayName)
Try {
Set-User -Identity $Mbx.userPrincipalName -RemotePowerShellEnabled $False -Confirm:$False
$MessageText = "PowerShell disabled on " + (Get-Date -format s)
Set-Mailbox -Identity $Mbx.userPrincipalName -CustomAttribute5 $MessageText
}
Catch {
Write-Output ("Error disabling PowerShell for mailbox {0}" -f $Mbx.userPrincipalNane )
}
}
} # End ForEach
An improvement to the original script is that the final step is to check that administrator accounts have PowerShell access. This is to pick up new administrators that receive individual PIM assignments or join a group with a PIM assignment.
Write-Output "Checking administrator mailboxes to make sure that they have PowerShell access..."
ForEach ($Mbx in $AdminAccounts) {
[string]$mbx = $mbx
$PSEnabled = (Get-User -Identity $Mbx -ErrorAction SilentlyContinue).RemotePowerShellEnabled
If (!($PsEnabled)) {
Write-Output ("Resetting PowerShell access for admin account {0}" -f $Mbx)
Set-User -Identity $Mbx -RemotePowerShellEnabled $True -Confirm:$False
}
}
The nice thing about working with Microsoft 365 is that there’s always something to learn. Authors learn from the comments posted for our articles. The comments force us to research before we can answer questions posed by readers. That’s a good thing.
Support the work of the Office 365 for IT Pros team by subscribing to the Office 365 for IT Pros eBook. Your support pays for the time we need to track, analyze, and document the changing world of Microsoft 365 and Office 365.
2 Replies to “Retrieving Azure AD (Entra ID) Privileged Identity Management Role Assignments”
Great article and script as always, thank you! I am doing something similar, although in a slightly different way, but I’m also monitoring the Azure AD logs for changes in assignments using Get-MgAuditLogDirectoryAudit (https://github.com/NunoFilipeMota/PublicScripts/blob/main/Monitor-PIMRoles.ps1). This is so we get alerted when someone is assigned a key PIM role, such as Exchange Admin or Global Admin for example. Just in case someone sneaky manages to get added to a role, does something, and then removes him/herself from that role, which a script monitoring the members wouldn’t catch.
{"id":null,"mode":"button","open_style":"in_modal","currency_code":"EUR","currency_symbol":"\u20ac","currency_type":"decimal","blank_flag_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":100,"top_media_type":"featured_image","featured_image_url":"https:\/\/office365itpros.com\/wp-content\/uploads\/2022\/11\/cover-141x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Virtual Tip Jar","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"Office 365 for IT Pros","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"Office 365 for IT Pros","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for supporting the work of Office 365 for IT Pros!","payment_confirmation_title":"Office 365 for IT Pros","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
Great article and script as always, thank you! I am doing something similar, although in a slightly different way, but I’m also monitoring the Azure AD logs for changes in assignments using Get-MgAuditLogDirectoryAudit (https://github.com/NunoFilipeMota/PublicScripts/blob/main/Monitor-PIMRoles.ps1). This is so we get alerted when someone is assigned a key PIM role, such as Exchange Admin or Global Admin for example. Just in case someone sneaky manages to get added to a role, does something, and then removes him/herself from that role, which a script monitoring the members wouldn’t catch.
Or use Search-UnifiedAuditLog to detect when people are added to a role:
$records = Search-UnifiedAuditLog -StartDate 12-jul-2023 -EndDate 13-jul-2023 -Formatted -ResultSize 5000 -Operations “Add member to role”