Last active
November 1, 2022 09:12
-
-
Save psignoret/52faefbde5bb7f1de636d8c5b385f27b to your computer and use it in GitHub Desktop.
Uses Microsoft Graph PowerShell to do roughly the same thing that happens when admin consent is granted.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Grants an app admin consent for delegated permissions and app roles | |
.DESCRIPTION | |
Given a client app ID and the list of required permissions for the app, this script will perform | |
mostly the same steps that take place when tenant-wide admin consent is granted: | |
1. A service principal for the client app will be created, if necessary | |
2. A service principal for each resource service will be created, if necessary and possible | |
3. Required delegated permissions will be granted on behalf of all users | |
4. Required app roles will be granted | |
Depending on which permission are required, these steps may require a Global Administrator, | |
Privileged Role Administrator, Application Administrator, or Cloud Application Administrator. | |
WARNING: This is a very high privilege operation which will potentially grant a third party | |
privileged access to your environment. Only use this if you are absolutely certain | |
you understand who controls the app you are granting access to. | |
IMPORTANT: This script only grants access, existing granted permissions are not revoked. | |
.EXAMPLE | |
# Grant admin consent in the same tenant where the app is registered | |
Connect-MgGraph -Scopes "Application.ReadWrite.All AppRoleAssignment.ReadWrite.All DelegatedPermissionGrant.ReadWrite.All" | |
.\Grant_admin_consent.ps1 -ClientAppId "453d500c-514a-498f-9d6e-205f229c45d2" | |
.EXAMPLE | |
# Import app details from a JSON file (for example, the app manifest downloaded from the Azure portal) | |
Connect-MgGraph -Scopes "Application.ReadWrite.All AppRoleAssignment.ReadWrite.All DelegatedPermissionGrant.ReadWrite.All" | |
Get-Content "app.json" -Raw | .\Grant_admin_consent.ps1 | |
.EXAMPLE | |
# Retrieve application details from app registered one tenant... | |
Connect-MgGraph -Scopes "Application.Read.All" -Tenant "tenant1.example.com" | |
$application = Get-MgApplication -Filter "appId eq '453d500c-514a-498f-9d6e-205f229c45d2'" | |
# ...then grant admin consent for the app in another tenant. | |
Connect-MgGraph -Scopes "Application.ReadWrite.All AppRoleAssignment.ReadWrite.All DelegatedPermissionGrant.ReadWrite.All" -Tenant "tenant2.example.com" | |
$application | .\Grant_admin_consent.ps1 | |
.EXAMPLE | |
# Export details from the Application object to JSON in a text file... | |
Connect-MgGraph -Scopes "Application.Read.All" | |
Get-MgApplication -Filter "appId eq '453d500c-514a-498f-9d6e-205f229c45d2'" ` | |
| select AppId, RequiredResourceAccess | ConvertTo-Json | Out-File "app.json" | |
# ... then import app details from a JSON file (this also works with the app manifest downloaded from the Azure portal) | |
Connect-MgGraph -Scopes "Application.ReadWrite.All AppRoleAssignment.ReadWrite.All DelegatedPermissionGrant.ReadWrite.All" | |
Get-Content "app.json" -Raw | .\Grant_admin_consent.ps1 | |
.EXAMPLE | |
# Directly specify the client app ID and required permissions | |
Connect-MgGraph -Scopes "Application.ReadWrite.All AppRoleAssignment.ReadWrite.All DelegatedPermissionGrant.ReadWrite.All" | |
.\Grant_admin_consent.ps1 -ClientAppId "453d500c-514a-498f-9d6e-205f229c45d2" -InputObject @( | |
@{ | |
"ResourceAppId" = "00000003-0000-0000-c000-000000000000" # Microsoft Graph | |
"ResourceAccess" = @( | |
@{ "Type" = "Scope"; "Id" = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" } # User.Read | |
@{ "Type" = "Scope"; "Id" = "64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0" } # email | |
@{ "Type" = "Role"; "Id" = "9492366f-7969-46a4-8d15-ed1a20078fff" } # Sites.ReadWrite.All | |
) | |
}, | |
@{ | |
"ResourceAppId" = "797f4846-ba00-4fd7-ba43-dac1f8f63013" # Azure Resource Management | |
"ResourceAccess" = @( | |
@{ "Type" = "Scope"; "Id" = "41094075-9dad-400e-a0bd-54e686782033" } # user_impersonation | |
) | |
} | |
) | |
#> | |
#Requires -Modules Microsoft.Graph.Applications, Microsoft.Graph.Identity.SignIns | |
[CmdletBinding()] | |
param( | |
# The AppId for the client app which will be granted admin consent | |
$ClientAppId, | |
# The app's required delegated permissions and app roles. This can be an array of objects (e.g. | |
# the RequiredResourceAccess property from Get-MgApplication), an object with the | |
# RequiredResourceAccess field (e.g. the output from Get-MgApplication), or the JSON string | |
# representation of either of two (e.g. JSON copied from the Azure AD app manifest) | |
[Parameter(ValueFromPipeline = $true)] | |
$InputObject, | |
[switch] $Force | |
) | |
begin { | |
Write-Host ("WARNING: You are doing something dangerous. Using this script could give " ` | |
+ "a third party access to your organization's data. Make sure you understand who " ` | |
+ "controls the app you are granting access to.") -ForegroundColor Red | |
# Make sure we're connected to Microsoft Graph PowerShell and using the v1.0 profile | |
if ((Get-MgProfile).Name -ne "v1.0") { Select-MgProfile -Name "v1.0" } | |
try { | |
Get-MgServicePrincipal -Top 1 -PageSize 1 -ErrorAction Stop | Out-Null | |
} catch { | |
Write-Warning "Must be connected to Microsoft Graph PowerShell" | |
throw $_.Exception | |
} | |
} | |
process { | |
# Some basic validation and transformation of InputObject | |
if ($null -eq $InputObject) { | |
if ($null -eq $ClientAppId) { | |
Write-Warning "InputObject is required" | |
return | |
} else { | |
$InputObject = Get-MgApplication -Filter "appId eq '$($ClientAppId)'" | |
if (-not $InputObject) { | |
Write-Warning "App registration with AppId '$($ClientAppId)' not found. InputObject is required" | |
return | |
} | |
} | |
} elseif ($InputObject -is [string]) { | |
try { | |
$InputObject = $InputObject | ConvertFrom-Json | |
} catch { | |
Write-Warning "Received string value for InputObject, but it's not valid JSON" | |
} | |
} | |
if ($InputObject -is [Array]) { | |
$rras = $InputObject | |
} elseif ($InputObject.RequiredResourceAccess -and $InputObject.RequiredResourceAccess -is [Array]) { | |
$rras = $InputObject.RequiredResourceAccess | |
} elseif ($InputObject.ResourceAppId) { | |
$rras = @($InputObject) | |
} else { | |
Write-Warning "InputObject is expected to be an array or an object with the RequiredResourceAccess property" | |
return | |
} | |
# Check that ClientAppId is either provided explicitly, or is a property of the input object | |
if (-not $ClientAppId) { | |
if ($InputObject.AppId) { | |
$ClientAppId = $InputObject.AppId | |
} else { | |
Write-Warning "ClientAppId required" | |
return | |
} | |
} | |
if (-not ($Force -or (Read-Host "Do you understand who controls the app with AppId $($ClientAppId)? (yes/no)") -eq "yes")) { | |
return | |
} | |
# A helper function we'll use later on to prompt for confirmation | |
function MaybePromptForConfirmation($name, $description, $value) { | |
if ($script:Force) { | |
return $true | |
} | |
$title = "$($permission.Value)" | |
$question = " $($name)`n $($description)`n Are you sure you want to proceed?" | |
$choices = '&Yes', '&No' | |
$decision = $Host.UI.PromptForChoice($title, $question, $choices, 1) | |
return $decision -eq 0 | |
} | |
# Create a service principal for the client app, if it doesn't already exist | |
$client = Get-MgServicePrincipal -Filter "appId eq '$($clientAppId)'" | |
if (-not $client) { | |
Write-Host "Creating client app's service principal" | |
$client = New-MgServicePrincipal -AppId $clientAppId | |
} | |
Write-Host "Client: '$($client.displayName)' (appId: $($client.appId))" | |
# Each requiredResourceAccess identifies an API (the resource app) and a list of required | |
# delegated permissions and app roles. | |
:rras foreach ($rra in $rras) { | |
# Retrieve the service principal for the resource app (the API). If it doesn't exist, try to | |
# create it. If it can't be created, skip the required permissions for this API and move on | |
# to the next one. | |
$resource = Get-MgServicePrincipal -Filter "appId eq '$($rra.resourceAppId)'" | |
if (-not $resource) { | |
Write-Host "A service principal for the required resource '$($rra.resourceAppId)' was not found. Attempting to create it..." | |
$resource = New-MgServicePrincipal -AppId $rra.resourceAppId | |
if ($resource) { | |
# This should not be unnecessary, but sometimes a refresh is needed... | |
$resource = Get-MgServicePrincipal -ServicePrincipalId $resource.Id | |
} else { | |
Write-Warning "Unable to create resource service principal. Skipping resource." | |
continue :rras | |
} | |
} | |
Write-Host "Resource: '$($resource.displayName)' (appId: $($resource.appId))" | |
# Create an app role assignment for each required app role (application permission). Skip any | |
# app role for which an assignment already exists. | |
$requiredAppRoleIds = $rra.resourceAccess | ? { $_.type -eq "Role" } | % { $_.id } | |
if ($requiredAppRoleIds) { | |
$assignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $client.Id ` | |
-Filter "resourceId eq $($resource.Id)" | |
$assignments = {$assignments}.Invoke() | |
foreach ($appRoleId in $requiredAppRoleIds) { | |
$appRole = $resource.AppRoles | ? { $_.Id -eq $appRoleId } | |
if (-not ($assignments | ? { $_.AppRoleId -eq $appRoleId })) { | |
Write-Host " App role '$($appRole.Value)' not yet granted" | |
if (MaybePromptForConfirmation -name $appRole.DisplayName ` | |
-description $appRole.Description ` | |
-value $appRole.Value) { | |
$assignments.Add( | |
(New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $resource.Id ` | |
-PrincipalId $client.Id ` | |
-ResourceId $resource.Id ` | |
-AppRoleId $appRole.Id) | |
) | |
Write-Host " App role '$($appRole.Value)' granted" | |
} else { | |
Write-Host " Skipping app role '$($appRole.Value)' ($($appRole.Id))" | |
} | |
} else { | |
Write-Host " App role '$($appRole.Value)' already granted" | |
} | |
} | |
} | |
# Create (or update) a delegated permissions grant on behalf of all principals | |
$requiredScopeIds = $rra.resourceAccess | ? { $_.Type -eq "Scope" } | % { $_.id } | |
if ($requiredScopeIds) { | |
# Retrieve any existing delegated permission grant on behalf of all for this API | |
$existingGrant = Get-MgOauth2PermissionGrant -Filter "clientId eq '$($client.Id)' and resourceId eq '$($resource.Id)' and consentType eq 'AllPrincipals'" | |
$grantedScopeValues = @() | |
if ($existingGrant) { | |
$grantedScopeValues = @($existingGrant.Scope.Split(" ") | sort) | |
} | |
# Determine which new delegated permissions will be granted (and get confirmation) | |
$finalScopeValues = { $grantedScopeValues }.Invoke() | |
foreach ($requiredScopeId in $requiredScopeIds) { | |
$requiredScope = $resource.Oauth2PermissionScopes | ? { $_.Id -eq $requiredScopeId } | |
if (-not $grantedScopeValues.Contains($requiredScope.Value)) { | |
Write-Host " Delegated permission '$($requiredScope.Value)' not yet granted" | |
if (MaybePromptForConfirmation -name $requiredScope.AdminConsentDisplayName ` | |
-description $requiredScope.AdminConsentDescription ` | |
-value $requiredScope.Value) { | |
$finalScopeValues.Add($requiredScope.Value) | |
} else { | |
Write-Host " Skipping delegated permission '$($requiredScope.Value)'" | |
} | |
} else { | |
Write-Host " Delegated permission '$($requiredScope.Value)' already granted" | |
} | |
} | |
# Grant new delegated permissions by updating (or creating) the delegated permissions grant | |
$newScopeValues = @($finalScopeValues | ? { -not $grantedScopeValues.Contains($_) }) | |
if ($newScopeValues) { | |
if ($existingGrant) { | |
Write-Host " Updating delegated permissions grant from '$($grantedScopeValues -join " ")' to '$($finalScopeValues -join " ")'" | |
Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId $existingGrant.Id -Scope ($finalScopeValues -join " ") | |
} else { | |
Write-Host " Creating delegated permissions grant for '$($finalScopeValues -join " ")'" | |
New-MgOauth2PermissionGrant -ConsentType "AllPrincipals" -ClientId $client.Id -ResourceId $resource.Id -Scope ($finalScopeValues -join " ") | Out-Null | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment