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' return } # 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 # 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.companyProfile.companyName)" -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 } return $true } catch { Write-ModuleLog -Message "Failed to set up application consent: $($_.Exception.Message)" -Level Error -Component 'ApplicationConsent' -ErrorRecord $_ -ThrowError } } |