44. $PSScriptRoot \..\AzureFunctions\Invoke-GraphApiRequest.ps1
55
66<#
7- Assigns permission to an Azure Application
7+ . SYNOPSIS
8+ Assigns API permissions to an Azure AD application.
89
9- The resourceAccessObject which is passed in the body of the Graph API call
10- It specifies the resources that the application needs to access
11- resourceAppId specifies the resources that the application needs to access and also the set of delegated permissions and application roles that it needs for each of those resources
12- resourceAccess id is the unique identifier of an app role or delegated permission exposed by the resource application
13- resourceAccess type specifies whether the id property references a delegated permission or an app role (application permission)
10+ . DESCRIPTION
11+ This function adds required resource access permissions to an Azure AD application using the Microsoft Graph API.
12+ It constructs a requiredResourceAccess object containing:
13+ - resourceAppId: The identifier of the resource application (e.g., Microsoft Graph) that the app needs access to.
14+ - resourceAccess: An array of permissions, where each entry includes:
15+ - id: The unique identifier of an app role or delegated permission exposed by the resource application.
16+ - type: Either "Role" (application permission) or "Scope" (delegated permission).
1417
15- See:
18+ The function supports two modes of operation:
19+ 1. Merge mode (KeepExistingPermissions = $true, default):
20+ - Retrieves existing permissions from the application
21+ - Preserves permissions for other resource applications (e.g., EWS when adding Graph permissions)
22+ - Merges new permissions with existing ones for the same resource application, avoiding duplicates
23+
24+ 2. Overwrite mode (KeepExistingPermissions = $false):
25+ - Replaces all existing requiredResourceAccess entries with only the new permissions
26+ - Use with caution as this removes permissions for other resource applications
27+
28+ . PARAMETER AzAccountsObject
29+ An object containing Azure account information, including the AccessToken for authentication.
30+
31+ . PARAMETER ApplicationId
32+ The object ID of the Azure AD application to update (not the AppId/ClientId).
33+
34+ . PARAMETER ResourceId
35+ The AppId of the resource application providing the permissions (e.g., "00000003-0000-0000-c000-000000000000" for Microsoft Graph).
36+
37+ . PARAMETER ApiPermissions
38+ An array of permission objects, each containing:
39+ - AppRole: An object with an 'id' property representing the permission GUID
40+ - PermissionType: Either "Application" (for app-only permissions) or "Delegated" (for user-delegated permissions)
41+
42+ . PARAMETER KeepExistingPermissions
43+ When $true (default), preserves existing permissions and merges new ones.
44+ When $false, replaces all permissions with only the specified new permissions.
45+
46+ . PARAMETER GraphApiUrl
47+ The base URL for Microsoft Graph API calls (e.g., "https://graph.microsoft.com/v1.0").
48+
49+ . OUTPUTS
50+ System.Boolean - Returns $true if permissions were successfully added, $false otherwise.
51+
52+ . LINK
1653 https://learn.microsoft.com/graph/api/application-update
1754 https://learn.microsoft.com/graph/api/resources/requiredresourceaccess
1855 https://learn.microsoft.com/graph/api/resources/resourceaccess
@@ -31,32 +68,108 @@ function Add-AzureApplicationRole {
3168 $ResourceId ,
3269
3370 [ValidateNotNullOrEmpty ()]
34- $AppRoleId ,
71+ [ System.Object []] $ApiPermissions ,
3572
36- [ValidateSet (" Scope" , " Role" )]
37- $Type = " Role" ,
73+ $KeepExistingPermissions = $true ,
3874
3975 [ValidateNotNullOrEmpty ()]
4076 $GraphApiUrl
4177 )
4278
4379 Write-Verbose " Adding permission to Azure Application: $ApplicationId via Graph Api: $GraphApiUrl "
44- Write-Verbose " ResourceId: $ResourceId - AppRoleId: $AppRoleId - Type: $Type "
80+ Write-Verbose " ResourceId: $ResourceId - Permissions to be added: $ ( $ApiPermissions | ConvertTo-Json - Depth 4 ) "
81+
82+ # This list will hold all requiredResourceAccess entries for the final PATCH request
83+ # Each entry represents permissions for a specific resource application (e.g., Graph, EWS)
84+ $requiredResourceAccessList = New-Object System.Collections.Generic.List[object ]
85+
86+ # Transform the input ApiPermissions into the resourceAccess format expected by Graph API
87+ # Each entry maps to: { id: <permission GUID>, type: "Role" or "Scope" }
88+ # "Role" = Application permission (app-only), "Scope" = Delegated permission (user context)
89+ $newResourceAccessEntries = foreach ($entry in $ApiPermissions ) {
90+ [PSCustomObject ]@ {
91+ id = $entry.AppRole.id
92+ type = if ($entry.PermissionType -eq " Application" ) { " Role" } else { " Scope" }
93+ }
94+ }
4595
46- $resourceAccessObject = [PSCustomObject ]@ {
47- requiredResourceAccess = @ (
48- [PSCustomObject ]@ {
49- resourceAppId = $ResourceId
50- resourceAccess = @ (
51- [PSCustomObject ]@ {
52- id = $AppRoleId
53- type = $Type
96+ if ($KeepExistingPermissions ) {
97+ Write-Verbose " Retrieving existing requiredResourceAccess from the application"
98+ $getApplicationParams = @ {
99+ Query = " applications/$ApplicationId " + ' ?$select=requiredResourceAccess'
100+ AccessToken = $AzAccountsObject.AccessToken
101+ GraphApiUrl = $GraphApiUrl
102+ }
103+
104+ $getApplicationResponse = Invoke-GraphApiRequest @getApplicationParams
105+
106+ if ($getApplicationResponse.Successful -eq $false ) {
107+ Write-Verbose " Failed to retrieve existing application permissions"
108+ return $false
109+ }
110+
111+ $existingRequiredResourceAccess = $getApplicationResponse.Content.requiredResourceAccess
112+
113+ if ($null -ne $existingRequiredResourceAccess ) {
114+ # Iterate through all existing resource access entries
115+ # We need to preserve permissions for other resources while merging permissions for our target resource
116+ foreach ($existingResource in $existingRequiredResourceAccess ) {
117+ if ($existingResource.resourceAppId -eq $ResourceId ) {
118+ # Found the target resource - merge existing permissions with new ones
119+ # Start with existing permissions and add new ones that don't already exist
120+ $mergedResourceAccess = New-Object System.Collections.Generic.List[object ]
121+ $mergedResourceAccess.AddRange (@ ($existingResource.resourceAccess ))
122+
123+ # Add each new permission only if it doesn't already exist (avoid duplicates)
124+ # Comparing by id alone is sufficient because appRoles and oauth2PermissionScopes
125+ # use independently assigned GUIDs - the same id never appears as both Role and Scope
126+ foreach ($newEntry in $newResourceAccessEntries ) {
127+ $existingEntry = $mergedResourceAccess | Where-Object { $_.id -eq $newEntry.id }
128+ if ($null -eq $existingEntry ) {
129+ $mergedResourceAccess.Add ($newEntry )
130+ }
54131 }
55- )
132+ $requiredResourceAccessList.Add ([PSCustomObject ]@ {
133+ resourceAppId = $ResourceId
134+ resourceAccess = $mergedResourceAccess.ToArray ()
135+ })
136+ } else {
137+ # Keep existing resource access for other resourceAppIds
138+ $requiredResourceAccessList.Add ($existingResource )
139+ }
56140 }
57- )
141+
142+ # If ResourceId was not found in existing permissions, add it as new
143+ $resourceIdExists = $existingRequiredResourceAccess | Where-Object { $_.resourceAppId -eq $ResourceId }
144+ if ($null -eq $resourceIdExists ) {
145+ $requiredResourceAccessList.Add ([PSCustomObject ]@ {
146+ resourceAppId = $ResourceId
147+ resourceAccess = @ ($newResourceAccessEntries )
148+ })
149+ }
150+ } else {
151+ # No existing permissions, add new ones
152+ $requiredResourceAccessList.Add ([PSCustomObject ]@ {
153+ resourceAppId = $ResourceId
154+ resourceAccess = @ ($newResourceAccessEntries )
155+ })
156+ }
157+ } else {
158+ # Overwrite mode - only include new permissions
159+ $requiredResourceAccessList.Add ([PSCustomObject ]@ {
160+ resourceAppId = $ResourceId
161+ resourceAccess = @ ($newResourceAccessEntries )
162+ })
163+ }
164+
165+ # Use .ToArray() instead of @() to avoid "Argument types do not match" exception
166+ # when converting System.Collections.Generic.List[object] to an array
167+ $resourceAccessObject = [PSCustomObject ]@ {
168+ requiredResourceAccess = $requiredResourceAccessList.ToArray ()
58169 }
59170
171+ # Prepare the PATCH request to update the application's requiredResourceAccess property
172+ # Depth 4 is required for proper JSON serialization of nested resourceAccess arrays
60173 $updateApplicationParams = @ {
61174 Query = " applications/$ApplicationId "
62175 AccessToken = $AzAccountsObject.AccessToken
@@ -66,12 +179,12 @@ function Add-AzureApplicationRole {
66179 GraphApiUrl = $GraphApiUrl
67180 }
68181
69- # Graph API call to add permissions to the Azure Application
182+ # Execute the Graph API PATCH request to update the application's permissions
70183 if ($PSCmdlet.ShouldProcess (" PATCH $ResourceId " , " Invoke-GraphApiRequest" )) {
71184 $updateApplicationResponse = Invoke-GraphApiRequest @updateApplicationParams
72185
73186 if ($updateApplicationResponse.Successful -eq $false ) {
74- Write-Verbose " Something went wrong while adding permissions this Azure Application"
187+ Write-Verbose " Something went wrong while adding permissions to this Azure Application"
75188 return $false
76189 }
77190
0 commit comments