<# | |
.SYNOPSIS | |
Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). | |
.PARAMETER DelegatedPermissions | |
If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions switch is set, | |
both application and delegated permissions will be returned. | |
.PARAMETER ApplicationPermissions | |
If set, will return application permissions. If neither this switch nor the DelegatedPermissions switch is set, | |
both application and delegated permissions will be returned. | |
.PARAMETER UserProperties | |
The list of properties of user objects to include in the output. Defaults to DisplayName only. | |
.PARAMETER ServicePrincipalProperties | |
The list of properties of service principals (i.e. apps) to include in the output. Defaults to DisplayName only. | |
.PARAMETER ShowProgress | |
Whether or not to display a progress bar when retrieving application permissions (which could take some time). | |
.PARAMETER PrecacheSize | |
The number of users to pre-load into a cache. For tenants with over a thousand users, | |
increasing this may improve performance of the script. | |
.EXAMPLE | |
PS C:\> .\Get-AzureADPSPermissions.ps1 | Export-Csv -Path "permissions.csv" -NoTypeInformation | |
Generates a CSV report of all permissions granted to all apps. | |
.EXAMPLE | |
PS C:\> .\Get-AzureADPSPermissions.ps1 -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" } | |
Get all apps which have application permissions for Directory.Read.All. | |
.EXAMPLE | |
PS C:\> .\Get-AzureADPSPermissions.ps1 -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId") | |
Gets all permissions granted to all apps and includes additional properties for users and service principals. | |
#> | |
[CmdletBinding()] | |
param( | |
[switch] $DelegatedPermissions, | |
[switch] $ApplicationPermissions, | |
[string[]] $UserProperties = @("DisplayName"), | |
[string[]] $ServicePrincipalProperties = @("DisplayName"), | |
[switch] $ShowProgress, | |
[int] $PrecacheSize = 999 | |
) | |
# Get tenant details to test that Connect-AzureAD has been called | |
try { | |
$tenant_details = Get-AzureADTenantDetail | |
} catch { | |
throw "You must call Connect-AzureAD before running this script." | |
} | |
Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f ` | |
$tenant_details.ObjectId, ` | |
($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name) | |
# An in-memory cache of objects by {object ID} andy by {object class, object ID} | |
$script:ObjectByObjectId = @{} | |
$script:ObjectByObjectClassId = @{} | |
# Function to add an object to the cache | |
function CacheObject ($Object) { | |
if ($Object) { | |
if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) { | |
$script:ObjectByObjectClassId[$Object.ObjectType] = @{} | |
} | |
$script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object | |
$script:ObjectByObjectId[$Object.ObjectId] = $Object | |
} | |
} | |
# Function to retrieve an object from the cache (if it's there), or from Azure AD (if not). | |
function GetObjectByObjectId ($ObjectId) { | |
if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) { | |
Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId) | |
try { | |
$object = Get-AzureADObjectByObjectId -ObjectId $ObjectId | |
CacheObject -Object $object | |
} catch { | |
Write-Verbose "Object not found." | |
} | |
} | |
return $script:ObjectByObjectId[$ObjectId] | |
} | |
# Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode) | |
# or by iterating over all ServicePrincipal objects. The latter is required if there are more than | |
# 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD. | |
function GetOAuth2PermissionGrants ([switch]$FastMode) { | |
if ($FastMode) { | |
Get-AzureADOAuth2PermissionGrant -All $true | |
} else { | |
$script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } { | |
if ($ShowProgress) { | |
Write-Progress -Activity "Retrieving delegated permissions..." ` | |
-Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` | |
-PercentComplete (($i / $servicePrincipalCount) * 100) | |
} | |
$client = $_.Value | |
Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId | |
} | |
} | |
} | |
$empty = @{} # Used later to avoid null checks | |
# Get all ServicePrincipal objects and add to the cache | |
Write-Verbose "Retrieving all ServicePrincipal objects..." | |
Get-AzureADServicePrincipal -All $true | ForEach-Object { | |
CacheObject -Object $_ | |
} | |
$servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count | |
if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { | |
# Get one page of User objects and add to the cache | |
Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize) | |
Get-AzureADUser -Top $PrecacheSize | Where-Object { | |
CacheObject -Object $_ | |
} | |
Write-Verbose "Testing for OAuth2PermissionGrants bug before querying..." | |
$fastQueryMode = $false | |
try { | |
# There's a bug in Azure AD Graph which does not allow for directly listing | |
# oauth2PermissionGrants if there are more than 999 of them. The following line will | |
# trigger this bug (if it still exists) and throw an exception. | |
$null = Get-AzureADOAuth2PermissionGrant -Top 999 | |
$fastQueryMode = $true | |
} catch { | |
if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) { | |
Write-Verbose ("Fast query for delegated permissions failed, using slow method...") | |
} else { | |
throw $_ | |
} | |
} | |
# Get all existing OAuth2 permission grants, get the client, resource and scope details | |
Write-Verbose "Retrieving OAuth2PermissionGrants..." | |
GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object { | |
$grant = $_ | |
if ($grant.Scope) { | |
$grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object { | |
$scope = $_ | |
$grantDetails = [ordered]@{ | |
"PermissionType" = "Delegated" | |
"ClientObjectId" = $grant.ClientId | |
"ResourceObjectId" = $grant.ResourceId | |
"Permission" = $scope | |
"ConsentType" = $grant.ConsentType | |
"PrincipalObjectId" = $grant.PrincipalId | |
} | |
# Add properties for client and resource service principals | |
if ($ServicePrincipalProperties.Count -gt 0) { | |
$client = GetObjectByObjectId -ObjectId $grant.ClientId | |
$resource = GetObjectByObjectId -ObjectId $grant.ResourceId | |
$insertAtClient = 2 | |
$insertAtResource = 3 | |
foreach ($propertyName in $ServicePrincipalProperties) { | |
$grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) | |
$insertAtResource++ | |
$grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) | |
$insertAtResource ++ | |
} | |
} | |
# Add properties for principal (will all be null if there's no principal) | |
if ($UserProperties.Count -gt 0) { | |
$principal = $empty | |
if ($grant.PrincipalId) { | |
$principal = GetObjectByObjectId -ObjectId $grant.PrincipalId | |
} | |
foreach ($propertyName in $UserProperties) { | |
$grantDetails["Principal$propertyName"] = $principal.$propertyName | |
} | |
} | |
New-Object PSObject -Property $grantDetails | |
} | |
} | |
} | |
} | |
if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { | |
# Iterate over all ServicePrincipal objects and get app permissions | |
Write-Verbose "Retrieving AppRoleAssignments..." | |
$script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } { | |
if ($ShowProgress) { | |
Write-Progress -Activity "Retrieving application permissions..." ` | |
-Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` | |
-PercentComplete (($i / $servicePrincipalCount) * 100) | |
} | |
$sp = $_.Value | |
Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true ` | |
| Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object { | |
$assignment = $_ | |
$resource = GetObjectByObjectId -ObjectId $assignment.ResourceId | |
$appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id } | |
$grantDetails = [ordered]@{ | |
"PermissionType" = "Application" | |
"ClientObjectId" = $assignment.PrincipalId | |
"ResourceObjectId" = $assignment.ResourceId | |
"Permission" = $appRole.Value | |
} | |
# Add properties for client and resource service principals | |
if ($ServicePrincipalProperties.Count -gt 0) { | |
$client = GetObjectByObjectId -ObjectId $assignment.PrincipalId | |
$insertAtClient = 2 | |
$insertAtResource = 3 | |
foreach ($propertyName in $ServicePrincipalProperties) { | |
$grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) | |
$insertAtResource++ | |
$grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) | |
$insertAtResource ++ | |
} | |
} | |
New-Object PSObject -Property $grantDetails | |
} | |
} | |
} |
This comment has been minimized.
This comment has been minimized.
Thanks for this post. the command get-AzureADOAuth2PermissionGrant is returning only finite number of results. If i run "get-AzureADOAuth2PermissionGrant -all $true" it is throwing the below error.
Please let me know if you have any workaround for the same. Is there any other command which provides the user scopes? Thanks, |
This comment has been minimized.
This comment has been minimized.
You can actually modify the script to read the permissions per Azure AD Service Prinicipal and using Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $sp.ObjectId instead of "Get-AzureADOAuth2PermissionGrant -all $true" Snipset: Get all registered applications
. Get all existing OAuth2 permission grants, get the client, resource and scope details
. hth, Claus |
This comment has been minimized.
This comment has been minimized.
Hi Claus, Thanks for the solution. It worked like a charm!! below is the format I am using..
Regards, |
This comment has been minimized.
This comment has been minimized.
@evgaff @shesha1 There's currently a bug in Azure AD when you have more than 1000 OAuth2PermissionGrants (delegated permission grants) in the tenant. As @cwitjes rightly points out, a workaround available today is to query these from each ServicePrincipal object's. Unfortunately, this is orders of magnitude slower than the original approach. I've updated the script to test for the bug, and if it is triggered (because it hasn't been fixed and your tenant has more than 1000 grants), then it will fall back to the per-object (slow) query. |
This comment has been minimized.
This comment has been minimized.
Would it be possible to change this to export either samaccountname, UPN or mail attributes? |
This comment has been minimized.
This comment has been minimized.
@ryandriftingfat I've updated the script to support |
This comment has been minimized.
This comment has been minimized.
This is perfect, thank you so much. |
This comment has been minimized.
This comment has been minimized.
Hello, Thank you so much for this script ! In a recent effort to extend the script's output so its result displays 2 additional columns representing If I compare some the description I get of the permissions, I see it is not the exact description string/text from the two below approaches. Approach 1/ If I extend the script you provide by adding the following
Versus Approach 2/ If I use the following code region
#region Get/Display the "AdminConsentDescription", "UserConsentDescription" for each unique "ResourceDisplayName" $FullyQualifiedPermissions | #endregion The environment being quite large (50k consented mix of Delegated / Application permissions),
Again thanks ! |
This comment has been minimized.
This comment has been minimized.
So, to include the delegated permission display name or description, you could add the following ~L153: # $scope is the delegated permission claim value, obtained from the OAuth2PermissionGrant
$scope = $_
# Get the service principal for the resource application
$resource = GetObjectByObjectId -ObjectId $grant.ResourceId
# Get the delegated permission (OAuth2Permission) with the same value as $scope
$delegatedPermission = $resource.OAuth2Permissions | Where-Object { $_.Value -ieq $scope }
# Include the delegated permission details in the output object
$grantDetails = [ordered]@{
"PermissionType" = "Delegated"
"ClientObjectId" = $grant.ClientId
"ResourceObjectId" = $grant.ResourceId
"Permission" = $scope
"ConsentType" = $grant.ConsentType
"PrincipalObjectId" = $grant.PrincipalId
"AdminConsentDisplayName" = $delegatedPermission.AdminConsentDisplayName
"AdminConsentDescription" = $delegatedPermission.AdminConsentDescription
"UserConsentDisplayName" = $delegatedPermission.UserConsentDisplayName
"UserConsentDescription" = $delegatedPermission.UserConsentDescription
} |
This comment has been minimized.
This comment has been minimized.
Hi @psignoret What would be a best way to run this in Azure runbook and export in CSV ?
|
This comment has been minimized.
This comment has been minimized.
Hi Philippe Are you able to help with my previous post? |
This comment has been minimized.
This comment has been minimized.
@r3-zbigniews Can you include how you're calling the script? |
This comment has been minimized.
This comment has been minimized.
@psignoret I'm using
|
This comment has been minimized.
This comment has been minimized.
@r3-zbigniews Can you share how you're calling this script? |
This comment has been minimized.
This comment has been minimized.
|
This comment has been minimized.
This comment has been minimized.
@psignoret what do you think? |
This comment has been minimized.
This comment has been minimized.
Thank you for the script. |
This comment has been minimized.
Thanks for posting this. I've been trying to get this work by looking at each part of the script as I have to work with a huge directory.
Have you ever seen this? When running Get-AzureADOAuth2PermissionGrant -all $true it always errors out:
Get-AzureADOAuth2PermissionGrant : Unexpected end when deserializing array. Path 'value[1000]', line 1, position
359298.
I've updated to the latest AzureADPreview 2.0.2.5 and tried from multiple machines and accounts (into the same directory).
Interestingly, I can do -top 57, which returns exactly 1000 objects. When set to 58, it fails.
Any thoughts?