Private/Consent/Set-ApplicationConsent.ps1

function Set-ApplicationConsent {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$CustomerTenantId,
        
        [Parameter()]
        [switch]$Force
    )

    try {
        # Step 1: Create initial consent through Partner Center
        $customer = Get-PartnerCustomer -CustomerTenantId $CustomerTenantId
        if (!$customer) {
            Write-ModuleLog -Message "Customer with tenant ID $CustomerTenantId not found" -Level Error -Component 'ApplicationConsent' -ThrowError
        }

        $consentBody = @{
            ApplicationId     = $script:PartnerCredentials.ApplicationId
            ApplicationGrants = @(
                @{
                    EnterpriseApplicationId = '00000003-0000-0000-c000-000000000000'
                    Scope                   = @(
                        'DelegatedPermissionGrant.ReadWrite.All',
                        'Directory.ReadWrite.All',
                        'AppRoleAssignment.ReadWrite.All'
                    ) -Join ','
                }
            )
        }

        $response = Invoke-PartnerCenterRequest `
            -Uri "https://api.partnercenter.microsoft.com/v1/customers/$CustomerTenantId/applicationconsents" `
            -Method POST `
            -Body $consentBody `

        # Handle initial consent response
        switch ($response.StatusCode) {
            { $_ -in @(200, 201) } {
                Write-ModuleLog -Message "Successfully created consent for $($customer.displayName)" -Level Info -Component 'ApplicationConsent'
                Write-ModuleLog -Message "Waiting for consent to propagate..." -Level Info -Component 'ApplicationConsent'
                Start-Sleep -Seconds 5
            }
            409 {
                Write-ModuleLog -Message "Consent already exists for $($customer.displayName)" -Level Warning -Component 'ApplicationConsent'
                if ($Force) {
                    Write-ModuleLog -Message "Force specified - recreating consent..." -Level Warning -Component 'ApplicationConsent'
                    $consentUri = "https://api.partnercenter.microsoft.com/v1/customers/$CustomerTenantId/applicationconsents/$($script:PartnerCredentials.ApplicationId)"
                    Invoke-PartnerCenterRequest -Uri $consentUri -Method DELETE -ErrorAction SilentlyContinue | Out-Null
                    return Set-ApplicationConsent -CustomerTenantId $CustomerTenantId
                }
            }
            400 {
                if ($response.message -like "*doesnt exist in customer tenant*") {
                    Write-ModuleLog -Message "Successfully created partial consent for $($customer.displayName)" -Level Info -Component 'ApplicationConsent'
                    Write-ModuleLog -Message "Some APIs are not available in the tenant - this is normal" -Level Warning -Component 'ApplicationConsent'
                    Start-Sleep -Seconds 5
                }
                else {
                    Write-ModuleLog -Message "Bad request: $($response.message)" -Level Error -Component 'ApplicationConsent' -ThrowError
                }
            }
            default {
                Write-ModuleLog -Message "Unexpected response: $($response.StatusCode) - $($response.message)" -Level Error -Component 'ApplicationConsent' -ThrowError
            }
        }

        # Step 2: Connect to customer tenant to configure app permissions
        Write-ModuleLog -Message "Connecting to customer tenant to configure permissions" -Level Info -Component 'ApplicationConsent'
        Connect-CustomerGraph -CustomerTenantId $CustomerTenantId -FlowType "Delegated" -Force

        # Step 3: Get our service principal
        $ServicePrincipalList = Get-MgServicePrincipal -All
        $ServicePrincipal = $ServicePrincipalList | Where-Object { $_.AppId -eq $script:PartnerCredentials.ApplicationId }
        
        $retryCount = 0
        $maxRetries = 20
        while (!$ServicePrincipal -and $retryCount -lt $maxRetries) {
            Write-ModuleLog -Message "Service principal not found, waiting for propagation... Attempt $($retryCount + 1)/$maxRetries" -Level Info -Component 'ApplicationConsent'
            Start-Sleep -Seconds 10
            $ServicePrincipalList = Get-MgServicePrincipal -All
            $ServicePrincipal = $ServicePrincipalList | Where-Object { $_.AppId -eq $script:PartnerCredentials.ApplicationId }
            $retryCount++
        }

        if (!$ServicePrincipal) {
            Write-ModuleLog -Message "Service principal not found after waiting" -Level Error -Component 'ApplicationConsent' -ThrowError
        }

        # Step 4: Load required permissions from manifest and translator
        $ManifestPath = Join-Path $script:ModuleRoot "Config\Consent\SAMManifest.json"
        $TranslatorPath = Join-Path $script:ModuleRoot "Config\Consent\PermissionsTranslator.json"

        if (!(Test-Path $ManifestPath)) {
            Write-ModuleLog -Message "SAM Manifest not found at $ManifestPath" -Level Error -Component 'ApplicationConsent' -ThrowError
        }

        $RequiredResourceAccess = Get-Content $ManifestPath | 
        ConvertFrom-Json | 
        Select-Object -ExpandProperty requiredResourceAccess

        $PermissionTranslator = if (Test-Path $TranslatorPath) {
            Get-Content $TranslatorPath | ConvertFrom-Json
        }

        # Step 5: Configure app roles (application permissions)
        foreach ($App in $RequiredResourceAccess) {
            $ResourceServicePrincipal = $ServicePrincipalList | Where-Object { $_.AppId -eq $App.resourceAppId }

            if (!$ResourceServicePrincipal) {
                Write-ModuleLog -Message "Creating service principal for $($App.resourceAppId)" -Level Info -Component 'ApplicationConsent'
                $ResourceServicePrincipal = New-MgServicePrincipal -AppId $App.resourceAppId -ErrorAction SilentlyContinue
                if (!$ResourceServicePrincipal) {
                    Write-ModuleLog -Message "Failed to create service principal for $($App.resourceAppId) - API might not be available in tenant" -Level Warning -Component 'ApplicationConsent'
                    continue
                }
            }

            # Assign app roles
            # Get current role assignments
            $currentRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $ServicePrincipal.Id

            foreach ($Role in ($App.ResourceAccess | Where-Object { $_.type -eq 'Role' })) {
                try {
                    # Check if role is already assigned
                    $existingAssignment = $currentRoles | Where-Object { 
                        $_.AppRoleId -eq $Role.Id -and 
                        $_.ResourceId -eq $ResourceServicePrincipal.Id 
                    }
            
                    if ($existingAssignment) {
                        Write-ModuleLog -Message "App role $($Role.Id) is already assigned for $($ResourceServicePrincipal.DisplayName)" `
                            -Level Verbose `
                            -Component 'ApplicationConsent'
                        continue
                    }
            
                    $params = @{
                        ServicePrincipalId = $ServicePrincipal.Id
                        PrincipalId        = $ServicePrincipal.Id
                        ResourceId         = $ResourceServicePrincipal.Id
                        AppRoleId          = $Role.Id
                    }
                    
                    Write-ModuleLog -Message "Assigning app role $($Role.Id) for $($ResourceServicePrincipal.DisplayName)" `
                        -Level Info `
                        -Component 'ApplicationConsent'
                    New-MgServicePrincipalAppRoleAssignment @params -ErrorAction Stop | Out-Null
                }
                catch {
                    Write-ModuleLog -Message "Failed to assign role $($Role.Id): $_" `
                        -Level Warning `
                        -Component 'ApplicationConsent'
                }
            }

            # Step 6: Configure delegated permissions
            $DelegatedScopes = $App.ResourceAccess | Where-Object { $_.type -eq 'Scope' }
            if ($DelegatedScopes) {
                # Get current delegated permissions
                $currentGrants = Get-MgOauth2PermissionGrant -Filter "clientId eq '$($ServicePrincipal.Id)'"
                $existingGrant = $currentGrants | Where-Object { $_.resourceId -eq $ResourceServicePrincipal.Id }

                # Calculate new scope string
                $NewScope = if ($PermissionTranslator) {
                    @(($PermissionTranslator | 
                            Where-Object { $_.id -in $DelegatedScopes.id }).value | 
                        Sort-Object -Unique) -join ' '
                }
                else {
                    @($DelegatedScopes | 
                        ForEach-Object { $_.id } | 
                        Sort-Object -Unique) -join ' '
                }

                try {
                    if ($existingGrant) {
                        # Compare existing and new scopes
                        $existingScopes = $existingGrant.Scope -split ' ' | Sort-Object
                        $newScopes = $NewScope -split ' ' | Sort-Object
            
                        $scopeComparison = Compare-Object -ReferenceObject $existingScopes -DifferenceObject $newScopes
            
                        if ($null -eq $scopeComparison) {
                            Write-ModuleLog -Message "All delegated permissions already exist for $($ResourceServicePrincipal.DisplayName)" `
                                -Level Info `
                                -Component 'ApplicationConsent'
                        }
                        else {
                            # Update existing grant with new scopes
                            Write-ModuleLog -Message "Updating delegated permissions for $($ResourceServicePrincipal.DisplayName)" `
                                -Level Info `
                                -Component 'ApplicationConsent'
            
                            # Log changes if any
                            $added = ($scopeComparison | Where-Object { $_.SideIndicator -eq '=>' }).InputObject
                            $removed = ($scopeComparison | Where-Object { $_.SideIndicator -eq '<=' }).InputObject
            
                            if ($added) {
                                Write-ModuleLog -Message "Adding scopes: $($added -join ', ')" `
                                    -Level Info `
                                    -Component 'ApplicationConsent'
                            }
                            if ($removed) {
                                Write-ModuleLog -Message "Removing scopes: $($removed -join ', ')" `
                                    -Level Info `
                                    -Component 'ApplicationConsent'
                            }

                            Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId $existingGrant.Id -Scope $NewScope
                        }
                    }
                    else {
                        # Create new permission grant
                        $params = @{
                            ClientId    = $ServicePrincipal.Id
                            ConsentType = 'AllPrincipals'
                            ResourceId  = $ResourceServicePrincipal.Id
                            Scope       = $NewScope
                        }

                        if ($ResourceServicePrincipal.DisplayName -eq "Microsoft Partner Center" -or $ResourceServicePrincipal.DisplayName -eq "M365 License Manager") {
                            # Skip consent for Partner Center
                            Write-ModuleLog -Message "Skipping consent for $($ResourceServicePrincipal.DisplayName), since it's a customer tenant" -Level Verbose -Component 'ApplicationConsent'
                            continue
                        }

                        Write-ModuleLog -Message "Granting new delegated permissions for $($ResourceServicePrincipal.DisplayName): $NewScope" `
                            -Level Info `
                            -Component 'ApplicationConsent'
                        New-MgOAuth2PermissionGrant @params -ErrorAction Stop | Out-Null
                    }
                }
                catch {
                    Write-ModuleLog -Message "Failed to manage delegated permissions for $($ResourceServicePrincipal.DisplayName): $_" `
                        -Level Warning `
                        -Component 'ApplicationConsent'
                }
            }
        }

        # Step 7
        # Disconnect and reconnect to get the updated token
        # Keep disconnecting graph until there are no open connections
        $GraphContext = Get-MgContext
        if ($GraphContext) {
            $dc = Disconnect-Graph -ErrorAction SilentlyContinue | Out-Null
            $GraphContext = Get-MgContext
            Write-ModuleLog -Message "Disconnected from Graph to clear connections" -Level Info -Component 'ApplicationConsent'
        }
        Write-ModuleLog -Message "Waiting for 10 seconds before reconnecting to Graph" -Level Info -Component 'ApplicationConsent'
        Start-Sleep -Seconds 10
        Connect-CustomerGraph -CustomerTenantId $CustomerTenantId -FlowType "Delegated" -Force

        $GraphContext = Get-MgContext
        while (!$GraphContext.Scopes.Contains("RoleManagement.ReadWrite.Directory")) {
            Write-ModuleLog -Message "RoleManagement.ReadWrite.Directory permission is missing in the token, re-connecting until we have it." -Level Info -Component 'ApplicationConsent'
            $dc = Disconnect-Graph -ErrorAction SilentlyContinue | Out-Null
            Start-Sleep -Seconds 10
            Connect-CustomerGraph -CustomerTenantId $CustomerTenantId -FlowType "Delegated" -Force
            $GraphContext = Get-MgContext
        }



        # Step 8
        # Assign "Compliance Administrator" and "Exchange Administrator" roles to the service principal if they exist
        $ComplianceAdministratorRole = Get-MgDirectoryRole | Where-Object { $_.DisplayName -eq "Compliance Administrator" }
        $ComplianceAdministrators = Get-MgDirectoryRoleMemberAsServicePrincipal -DirectoryRoleId $ComplianceAdministratorRole.Id -ErrorAction Stop
        if ($ComplianceAdministrators.Id -notcontains $ServicePrincipal.Id) {
            New-MgDirectoryRoleMemberByRef -DirectoryRoleId $ComplianceAdministratorRole.Id -BodyParameter @{"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($ServicePrincipal.Id)" } -ErrorAction Stop
            Write-ModuleLog -Message "Assigning Compliance Administrator role to service principal" -Level Info -Component 'ApplicationConsent'
        }


        $ExchangeAdministratorRole = Get-MgDirectoryRole | Where-Object { $_.DisplayName -eq "Exchange Administrator" }
        $ExchangeAdministrators = Get-MgDirectoryRoleMemberAsServicePrincipal -DirectoryRoleId $ExchangeAdministratorRole.Id -ErrorAction Stop
        if ($ExchangeAdministrators.Id -notcontains $ServicePrincipal.Id) {
            New-MgDirectoryRoleMemberByRef -DirectoryRoleId $ExchangeAdministratorRole.Id -BodyParameter @{"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($ServicePrincipal.Id)" } -ErrorAction Stop
            Write-ModuleLog -Message "Assigning Exchange Administrator role to service principal" -Level Info -Component 'ApplicationConsent'
        }

        Write-ModuleLog -Message "Successfully configured application permissions for $($customer.displayName)" -Level Info -Component 'ApplicationConsent'

        # Keep disconnecting graph until there are no open connections
        $GraphContext = Get-MgContext
        if ($GraphContext) {
            $dc = Disconnect-Graph -ErrorAction SilentlyContinue | Out-Null
            $GraphContext = Get-MgContext
        }
    }
    catch {
        Write-ModuleLog -Message "Failed to set up application consent: $($_.Exception.Message)" -Level Error -Component 'ApplicationConsent' -ErrorRecord $_ -ThrowError
    }
}