In the past, I’ve written some blog posts that show how you can setup automation across tenants leveraging GDAP. GDAP effectively broken a lot of MSP and vendor automation because it requires a specific configuration. In this article, I am going to share a script I have created so that you can update permissions for your automations over time across all of your GDAP relationships. (applies to both MSPs and vendors)

Problem Statement

As an MSP or vendor that integrates with Microsoft, you want to set up an app registration for automations with the principles of least privilege. Inevitably, you may need to increase scope of your API permissions on the app registration over time. If you do not have the Global Administrator role assigned to the group with your service principal, your API calls will inevitably fail as you add permissions to your app registration. The only way to overcome this is to go back and add the role you need to call the specific API to every single GDAP relationship in partner center. Real Life example:

  • You have an existing app registration you have consented to across all of your customers. 
  • Your customers have a GDAP relationship with the access assignments set to a handful of roles. 
  • You add the Device.ReadWrite.All permission to your app registration
  • You try to make API calls with this newly added permissions and find out you can’t
  • In investigating, you see that the roles part of the access assignment do not include any that would allow you to write to devices. 
  • You have to add Intune Administrator role to all GDAP access assignments across customers 

This final statement is what I wanted to solve for through scripting. We have 1000+ customers and no one has the time to manually go to each relationship and add the permission to the access assignment we need.  

Automation Steps

1. Get a Refresh Token

Prerequisites: Follow my previous post on setting up an App Registration: How to leverage Microsoft APIs for Automation – (tminus365.com)

2. Define your variables

The script I wrote requires you to define some variables that will be used:

GroupID => This is the security group that has your service principal you used to set up the integration. You can find this ID by going to the properties of the Group in Entra ID

RoleID => This is the ID of the role you want to include of the access assignment. These values can be found from the following support article: Microsoft Entra built-in roles | Microsoft Learn

3. Ensure your App Registration has the correct GDAP scope

The API calls we will make to get GDAP relationships, access assignments and update them will require the following Delegated admin permission: 

DelegatedAdminRelationship.ReadWrite.All

GDAP APIs leveraged:

4. Run the Automation Script

The following script will prompt you for secrets to acquire an access token and then ask for the variables we defined earlier. Here is a summary of the logic:

  • Get an Access token for graph
  • Get all active GDAP relationships
  • Define a Variable for the security Group ID that your service principal is located in
  • Define a Variable for the role ID you want to add: Microsoft Entra built-in roles | Microsoft Learn
  • Define a blank variable called RelationshipID
  • Define an variable called GroupAssignments which is an empty array
  • Loop through all GDAP relationships IDs
    • Set the variable of Relationship ID to the ID you are passing into the API
    • For each ID, loop through each object
    • See if the acccesContainer.accessContainerId = the Security Group where your service principal is located
    • If this is true, store the ID and the Relationship ID as an Object added to an GroupAssigments array as a new object appended to the array
  • Loop through the GroupAssigments array and pass them into the update API call from Graph in the request:
function Get-AccessToken {
    param (
        [Parameter(Mandatory=$true)]
        [string]$clientID,

        [Parameter(Mandatory=$true)]
        [string]$clientSecret,

        [Parameter(Mandatory=$true)]
        [string]$tenantID = “common”, # Your tenantID

        [Parameter(Mandatory=$true)]
        [string]$refreshToken, # Your refreshToken

        [string]$scope = “https://graph.microsoft.com/.default” # Default scope for Microsoft Graph
    )

    # Token endpoint
    $tokenUrl = “https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token”

    # Prepare the request body
    $body = @{
        client_id     = $clientID
        scope         = $scope
        client_secret = $clientSecret
        grant_type    = “refresh_token”
        refresh_token = $refreshToken
    }

    # Request the token
    $response = Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType “application/x-www-form-urlencoded” -Body $body

    # Return the access token
    return $response.access_token
}

$accessToken = Get-AccessToken

# Prompt the user for the Security Group ID
$SecurityGroup = Read-Host -Prompt “Enter the Security Group ID where your service principal is located”

# Prompt the user for the Role ID they wish to add to assignments
$RoleID = Read-Host -Prompt “Enter the Role ID you want to add to assignments”

# Initialize the RelationshipID variable with a blank value
$RelationshipID = “”

# Initialize the GroupAssignments variable as an empty array
$GroupAssignments = @()


# Define your Graph API endpoint for GDAP relationships
$gdapApiUrl = “https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships?$filter=status eq ‘active'”

# Use the existing access token for authorization
$headers = @{
    Authorization = “Bearer $accessToken
    “Content-Type” = “application/json”
}

# Function to get GDAP relationships and access assignments
Function Get-GDAPAssignments {
    param (
        [string]$apiUrl,
        [hashtable]$headers
    )

    Try {
        # Make the API call to get the GDAP assignments
        $response = Invoke-RestMethod -Uri $apiUrl -Headers $headers -Method Get
        # Check if there are more pages of data
        while ($response.‘@odata.nextLink’) {
            # If there are more pages, update the apiUrl and perform another request
            $apiUrl = $response.‘@odata.nextLink’
            $nextPage = Invoke-RestMethod -Uri $apiUrl -Headers $headers -Method Get
            $response.value += $nextPage.value
            $response.‘@odata.nextLink’ = $nextPage.‘@odata.nextLink’
        }
        # Return the list of assignments
        return $response.value
    } Catch {
        Write-Error “Error fetching GDAP assignments: $_
    }
}

# Call the function to get GDAP assignments
$activeGDAPRelationships = Get-GDAPAssignments -apiUrl $gdapApiUrl -headers $headers

# Display the assignments (for verification)
$activeGDAPRelationships | Format-Table


# Assume $gdapRelationships is the object containing all the GDAP relationships obtained from a previous API call
foreach ($gdapRelationship in $activeGDAPRelationships) {
    # Set the RelationshipID to the current GDAP Relationship ID
    $RelationshipID = $gdapRelationship.id

     # Store the customer’s display name
     $customerDisplayName = $gdapRelationship.customer.displayName

    # Form the URI for the API call
    $uri = “https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$RelationshipID/accessAssignments”

    # Make the API call
    $response = Invoke-RestMethod -Headers @{Authorization = “Bearer $accessToken} -Uri $uri -Method Get

    # Check if any access assignments match the Security Group ID
    foreach ($assignment in $response.value) {
        if ($assignment.accessContainer.accessContainerId -eq $SecurityGroup) {
             $roleDefinitionIds = @()
             # Create an array to hold all roleDefinitionIds including the new one
             $roleDefinitionIds += $assignment.accessDetails.unifiedRoles | ForEach-Object { $_.roleDefinitionId }
             # Add the new RoleID to the array of roleDefinitionIds
             $roleDefinitionIds += $RoleID
 
            # Create a new object with the assignment ID and Relationship ID
            $assignmentObject = [PSCustomObject]@{
                AssignmentID    = $assignment.id
                RelationshipID  = $RelationshipID
                etag           = $assignment.‘@odata.etag’
                CustomerName   = $customerDisplayName
                RoleDefinitionIds = $roleDefinitionIds
            }
            # Add the object to the GroupAssignments array
            $GroupAssignments += $assignmentObject
        }
    }
}

# Now $GroupAssignments array contains the assignments that match the Security Group

# Loop through each assignment in the GroupAssignments array
foreach ($groupAssignment in $GroupAssignments) {
    try {
        $headers = @{
            Authorization = “Bearer $accessToken
            ‘If-Match’ = $groupAssignment.ETag
            ‘Content-Type’ = ‘application/json’
        }
        $updateBody = @{
            accessDetails = @{
                unifiedRoles = $groupAssignment.RoleDefinitionIds | ForEach-Object {
                    @{ roleDefinitionId = $_ }
                }
            }
        }  | ConvertTo-Json -Depth 5
   

        $uri = “https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$($groupAssignment.RelationshipID)/accessAssignments/$($groupAssignment.AssignmentID)
       
        # Execute the PATCH request
        $response = Invoke-RestMethod -Headers $headers -Method PATCH -Uri $uri -Body $updateBody
        Write-Host “Success for $($groupAssignment.CustomerName) $($groupAssignment.AssignmentID) -ForegroundColor Green
    } catch {
        Write-Host “Failed for $($groupAssignment.CustomerName) $($groupAssignment.AssignmentID): $($_.Exception.Message) check to see if this role is available as part of the GDAP relationship” -ForegroundColor Red
    }
}

This will tell you if the tenant succeeds or fails. If it does fail, its most likely because the role isnt part of the existing GDAP relationship

5. Revoke and Re-add CPV consent 

When we add a new API permission to our app registration and perform the process of adding a new role to our GDAP access assignment, we need to revoke and er-add consent to the application as the customer tenant is only pre-approved with the original list of permissions provide. 

5a. Get an access token for partner center

# Define variables
$appId = 'your Azure AD application client ID'
$appSecret = 'your Azure AD application client secret'
$tenantId = 'your Azure AD tenant ID'
$scope = 'https://api.partnercenter.microsoft.com/.default'
$redirectUri = 'your redirect URI'

# Construct authorization endpoint URL
$authEndpoint = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?client_id=$appId&response_type=code&redirect_uri=$redirectUri&scope=$scope"

# Navigate to authorization endpoint and obtain authorization code
Start-Process $authEndpoint
$code = Read-Host "Enter authorization code"

$body = "grant_type=authorization_code&client_id=$appId&client_secret=$appSecret&code=$code&redirect_uri=$redirectUri&scope=$scope"
$headers = @{ 'Content-Type' = 'application/x-www-form-urlencoded' }
$tokenEndpoint = "https://login.microsoftonline.com/$tenantId/oauth2/token"
$response = Invoke-RestMethod -Method POST -Uri $tokenEndpoint -Body $body -Headers $headers

$AccessToken = $response.Access_Token

5b. Use the access token to revoke consent across you customers. 

You will want to loop through all of your customers. (use the same GDAP relationship API to get a list of tenant IDs) and run the following API to revoke consent from the App Registration using the existing App ID (ClientID). 

https://learn.microsoft.com/en-us/partner-center/developer/control-panel-vendor-apis#remove-consent 

 

5c. Re-add consent with new scope. 

You will modify the body in the following with the list of roles you want to consent on behalf of your customers. 

# Define variables
$appId = 'your Azure AD application client ID'
$appSecret = 'your Azure AD application client secret'
$tenantId = 'your Azure AD tenant ID'
$scope = 'https://api.partnercenter.microsoft.com/.default'
$redirectUri = 'your redirect URI'

# Construct authorization endpoint URL
$authEndpoint = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?client_id=$appId&response_type=code&redirect_uri=$redirectUri&scope=$scope"

# Navigate to authorization endpoint and obtain authorization code
Start-Process $authEndpoint
$code = Read-Host "Enter authorization code"

$body = "grant_type=authorization_code&client_id=$appId&client_secret=$appSecret&code=$code&redirect_uri=$redirectUri&scope=$scope"
$headers = @{ 'Content-Type' = 'application/x-www-form-urlencoded' }

$response = Invoke-RestMethod -Method POST -Uri $tokenEndpoint -Body $body -Headers $headers

$AccessToken = $response.AccessToken

$CustomerTenantId = 'Your Customer TenantID'

# Get list of customers
$uri = "https://api.partnercenter.microsoft.com/v1/customers"
$headers = @{
    Authorization = "Bearer $($AccessToken)"
    'Accept'      = 'application/json'
}

$Customers = Invoke-RestMethod -Uri $uri -Headers $headers

# Loop through each customer and run the existing script
foreach ($Customer in $Customers.value) {
    $CustomerTenantId = $Customer.id

    # Consent to required applications
    $uri = "https://api.partnercenter.microsoft.com/v1/customers/$CustomerTenantId/applicationconsents"
    $body = @{
        applicationGrants = @(
            @{
                enterpriseApplicationId = "00000003-0000-0000-c000-000000000000"
                scope                   = "Directory.Read.All,Directory.AccessAsUser.All"
            },
            @{
                enterpriseApplicationId = "00000002-0000-0ff1-ce00-000000000000"
                scope                   = "Exchange.Manage"
            }
        )
        applicationId   = $AppId
        displayName     = $AppDisplayName
    } | ConvertTo-Json

  Invoke-RestMethod -Uri $uri -Headers $headers -Method POST -Body $body -ContentType 'application/json'

  }	

Conclusion

I hope this article was helpful in showcasing how to automate updating access assignments across GDAP relationships. While you could just have the Global Admin role assigned to all access assignments with your automation, it is kind of defeating the purpose of the additional security GDAP should be providing. Happy scripting!

Share with the Community