Public/Invoke-EABillingSPNPermissionsSetup.ps1

function Invoke-EABillingSPNPermissionsSetup {
    <#
.SYNOPSIS
Creates a new SPN, or uses an existing SPN/MI, and assigns it the 'SubscriptionCreator' role to it to allow it to create subscriptions in the specified EA billing enrollment account.

.DESCRIPTION
Creates a new SPN, or uses an existing SPN/MI, and assigns it the 'SubscriptionCreator' role to it to allow it to create subscriptions in the specified EA billing enrollment account.

.EXAMPLE
# Create a new SPN and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters
/Invoke-EABillingSPNPermissionsSetup.ps1 -eaEnrollmentNumber "123456" -eaEnrollmentAccountNumber "987654"

# Create a new SPN and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaBillingAccountResourceId' parameter
./Invoke-EABillingSPNPermissionsSetup.ps1 -eaBillingAccountResourceId '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654'

# Create a new SPN, with a custom name, and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters
./Invoke-EABillingSPNPermissionsSetup.ps1 -eaEnrollmentNumber "123456" -eaEnrollmentAccountNumber "987654" -newSpnDisplayName 'spn-lz-sub-vending-custom-name'

# Create a new SPN, with a custom name, and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaBillingAccountResourceId' parameter
./Invoke-EABillingSPNPermissionsSetup.ps1 -eaBillingAccountResourceId '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' -newSpnDisplayName 'spn-lz-sub-vending-custom-name'

# Use an existing SPN/MI and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters
./Invoke-EABillingSPNPermissionsSetup.ps1 -eaEnrollmentNumber "123456" -eaEnrollmentAccountNumber "987654" -existingSpnMiObjectId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

# Use an existing SPN/MI and grant it the 'SubscriptionCreator' role on the specified EA billing account - using the 'eaBillingAccountResourceId' parameter
./Invoke-EABillingSPNPermissionsSetup.ps1 -eaBillingAccountResourceId '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' -existingSpnMiObjectId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
.NOTES
# Release notes 22/12/2022 - V1.0.0:
- Initial release.

# Release notes 23/12/2022 - V1.1.0:
- Added simplified inputs for the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters to form the 'eaBillingAccountResourceId' parameter value, instead of having to provide the full resource ID in the 'eaBillingAccountResourceId' parameter.
#>


    # Check for pre-reqs
    #Requires -PSEdition Core
    #Requires -Modules @{ ModuleName="Az.Accounts"; ModuleVersion="2.10.4" }
    #Requires -Modules @{ ModuleName="Az.Resources"; ModuleVersion="6.5.0" }

    [CmdletBinding(DefaultParameterSetName = "Default")]
    param (
        [Parameter(ParameterSetName = "Default", Mandatory = $false, Position = 1, HelpMessage = "Provide the EA enrollment number that the SPN will be granted the 'SubscriptionCreator' role upon. Example: '1234567'. This parameter is only used if the 'eaBillingAccountResourceId' parameter is not provided. It's value is used to create the 'eaBillingAccountResourceId' parameter value, that looks like this: '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' (this parameter value is the middle numerical value).")]
        [string]
        $eaEnrollmentNumber,

        [Parameter(ParameterSetName = "Default", Mandatory = $false, Position = 2, HelpMessage = "Provide the EA enrollment Account Number/ID that the SPN will be granted the 'SubscriptionCreator' role upon. Example: '987654'. This parameter is only used if the 'eaBillingAccountResourceId' parameter is not provided. It's value is used to create the 'eaBillingAccountResourceId' parameter value, that looks like this: '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/987654' (this parameter value is the middle numerical value).")]
        [string]
        $eaEnrollmentAccountNumber,

        [Parameter(ParameterSetName = "Advanced", Mandatory = $false, Position = 4, HelpMessage = "Provide the EA enrollment/billing account ID that the SPN will be granted the 'SubscriptionCreator' role upon. Example: '/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/123456'")]
        [string]
        $eaBillingAccountResourceId,

        [Parameter(ParameterSetName = "Default", Mandatory = $false, Position = 3, HelpMessage = "(Optional) Provide an existing Service Principal Name (SPN) (aka Enterprise Application) 'Object ID' to grant the 'SubscriptionCreator' role to on the specified billing account instead of creating a new one. If left blank a new SPN will be created. This can also be the object ID of a Managed Identity's SPN.")]
        [string]
        $existingSpnMiObjectId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",

        [Parameter(ParameterSetName = "Default", Mandatory = $false, Position = 5, HelpMessage = "(Optional) Provide a Display Name for the new Service Principal (SPN) (aka Enterprise Application) to be created. If left blank the default value of 'spn-lz-sub-vending' will be used.")]
        [string]
        $newSpnDisplayName = "spn-lz-sub-vending"
    )

    # Checks
    Write-Host "Checking inputs..." -ForegroundColor Cyan
    Write-Host ""

    # Check $eaBillingAccountResourceId is valid and populate from $eaEnrollmentNumber and $eaEnrollmentAccountNumber if not provided
    if ($eaBillingAccountResourceId -eq $null -or $eaBillingAccountResourceId -eq "") {
        Write-Host "eaBillingAccountResourceId paramter value not set, forming parameter value from eaEnrollmentNumber and eaEnrollmentAccountNumber parameters..." -ForegroundColor Magenta
        if ($eaEnrollmentNumber -eq $null -or $eaEnrollmentAccountNumber -eq $null -or $eaEnrollmentNumber -eq '' -or $eaEnrollmentAccountNumber -eq '') {
            throw "No values provdided for the 'eaEnrollmentNumber' and 'eaEnrollmentAccountNumber' parameters. These parameters are required if the 'eaBillingAccountResourceId' parameter is not provided. Please provide values for these parameters and try again."
        }
        $eaBillingAccountResourceId = "/providers/Microsoft.Billing/billingAccounts/$eaEnrollmentNumber/enrollmentAccounts/$eaEnrollmentAccountNumber"
        Write-Host "eaBillingAccountResourceId parameter value set to '$($eaBillingAccountResourceId)'" -ForegroundColor Green
        Write-Host ""
    }

    # Check $eaBillingAccountResourceId is valid and exists
    Write-Host "EA billing account parameters provided..." -ForegroundColor Cyan
    Write-Host "Checking the specified EA billing account ID '$($eaBillingAccountResourceId)' exists..." -ForegroundColor Yellow

    if ($null -ne $eaBillingAccountResourceId -and $eaBillingAccountResourceId -ne "") {
        $geteaBillingAccountResourceId = Invoke-AzRestMethod -Method GET -Path "$($eaBillingAccountResourceId)?api-version=2019-10-01-preview" -ErrorAction SilentlyContinue

        if ($geteaBillingAccountResourceId.StatusCode -ne 200) {
            Write-Error "HTTP Status Code: $($geteaBillingAccountResourceId.StatusCode)"
            Write-Error "HTTP Repsone Content: $($geteaBillingAccountResourceId.Content)"
            throw "The specified EA billing account ID '$($eaBillingAccountResourceId)' does not exist. Please check the value and try again. Also ensure you are logged in as the EA Account Owner for the specified EA billing account."
        } else {
            Write-Host "The specified EA billing account ID '$($eaBillingAccountResourceId)' exists. Continuing..." -ForegroundColor Green
            Write-Host ""
        }
    }

    # Check $existingSpnMiObjectId is valid and exists
    if ($existingSpnMiObjectId -ne $null -and $existingSpnMiObjectId -ne "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") {
        Write-Host "Existing SPN/MI provided..." -ForegroundColor Cyan
        Write-Host "Checking the specified SPN/MI 'Object ID' '$($existingSpnMiObjectId)' exists..." -ForegroundColor Yellow
        $getexistingSpnMiObjectId = Get-AzADServicePrincipal -ObjectId $existingSpnMiObjectId -ErrorAction SilentlyContinue

        if ($null -eq $getexistingSpnMiObjectId) {
            throw "The specified SPN/MI 'Object ID' '$($existingSpnMiObjectId)' does not exist. Please check the value and try again."
        } else {
            Write-Host "The specified SPN/MI 'Object ID' '$($existingSpnMiObjectId)' exists with a Display Name of: '$($getexistingSpnMiObjectId.DisplayName)' with a Type of: '$($getexistingSpnMiObjectId.ServicePrincipalType)'. Continuing..." -ForegroundColor Green
            Write-Host ""
            $finalSpnMiObjectId = $getexistingSpnMiObjectId.Id
            $finalSpnMiAppId = $getexistingSpnMiObjectId.AppId
            $finalSpnMiDisplayName = $getexistingSpnMiObjectId.DisplayName
            $finalSpnMiType = $getexistingSpnMiObjectId.ServicePrincipalType
        }
    } else {
        # Create a new SPN (aka Enterprise Application) as no existing SPN/MI was provided via the $existingSpnMiObjectId parameter
        Write-Host "No Existing SPN/MI provided. Proceeding to create a new SPN (aka Enterprise Application)..." -ForegroundColor Cyan
        Write-Host "Creating a new SPN (aka Enterprise Application) with a Display Name of '$($newSpnDisplayName)'..." -ForegroundColor Yellow

        $newSpn = New-AzADServicePrincipal -DisplayName $newSpnDisplayName -Description "Service Principal Name (SPN) for the Landing Zone Subscription Vending. See https://aka.ms/lz-vending/bicep or https://aka.ms/lz-vending/tf for more information." -ErrorAction Stop
        Write-Host "New SPN (aka Enterprise Application) created with a Display Name of '$($newSpn.DisplayName)' and an Object ID of '$($newSpn.Id)'." -ForegroundColor Green
        Write-Host ""

        $finalSpnMiObjectId = $newSpn.Id
        $finalSpnMiDisplayName = $newSpn.DisplayName
        $finalSpnMiType = $newSpn.ServicePrincipalType
        $finalSpnMiAppId = $newSpn.AppId
    }

    ## convert this to a retry loop
    # Start-Sleep -Seconds 15

    # Grant SPN/MI access to the specified EA billing account
    Write-Host "Pre-reqs passed and complete..." -ForegroundColor Cyan
    Write-Host "Granting the 'SubscriptionCreator' role (ID: 'a0bcee42-bf30-4d1b-926a-48d21664ef71') on the EA Billing Account ID of: '$($eaBillingAccountResourceId)' to the AAD Object ID of: '$($finalSpnMiObjectId)' which has the Display Name of: '$($finalSpnMiDisplayName)'..." -ForegroundColor Yellow

    # Get the current AAD Tenant ID
    $currentTenant = Get-AzTenant -ErrorAction Stop

    # Create GUID for role assignment name
    $roleAssignmentName = New-Guid

    $roleAssignmentHashTable = [ordered]@{
        "properties" = @{
            "principalId"       = "$finalSpnMiObjectId"
            "roleDefinitionId"  = "$eaBillingAccountResourceId/billingRoleDefinitions/a0bcee42-bf30-4d1b-926a-48d21664ef71"
            "principalTenantId" = "$($currentTenant.TenantId)"
        }
    }
    $roleAssignmentPayloadJson = $roleAssignmentHashTable | ConvertTo-Json -Depth 100

    $grantRbac = Invoke-AzRestMethod -Method PUT -Path "$($eaBillingAccountResourceId)/billingRoleAssignments/$($roleAssignmentName)?api-version=2019-10-01-preview" -Payload $roleAssignmentPayloadJson -ErrorAction SilentlyContinue

    # Create variables for retry loop
    $retryCount = 0
    $retryLimit = 10
    $retryDelay = 5


    if ($grantRbac.StatusCode -eq 400 -and $grantRbac.Content.Contains("are not valid")) {
        while ($retryCount -le $retryLimit) {
            Write-Host "The 'SubscriptionCreator' role has not been granted to the SPN/MI. Retrying in $retryDelay seconds to allow platform replication to occur..." -ForegroundColor Magenta

            Start-Sleep -Seconds $retryDelay
            $retryCount++

            $grantRbac = Invoke-AzRestMethod -Method PUT -Path "$($eaBillingAccountResourceId)/billingRoleAssignments/$($roleAssignmentName)?api-version=2019-10-01-preview" -Payload $roleAssignmentPayloadJson -ErrorAction SilentlyContinue

            if ($grantRbac.StatusCode -eq 200) {
                break
            }
        }
    }
    if ($grantRbac.StatusCode -ne 200) {
        Write-Error "HTTP Status Code: $($grantRbac.StatusCode)"
        Write-Error "HTTP Repsone Content: $($grantRbac.Content)"
        throw "An error occurred while attempting to grant the 'SubscriptionCreator' role to the SPN/MI. Please check the error message above and try again."
    } else {
        Write-Host "The 'SubscriptionCreator' role has been granted to the SPN/MI." -ForegroundColor Green
        Write-Host ""
        Write-Host "The SPN/MI 'Object ID' is: '$($finalSpnMiObjectId)'" -ForegroundColor Green
        Write-Host "The SPN/MI 'App ID' is: '$($finalSpnMiAppId)'" -ForegroundColor Green
        Write-Host "The SPN/MI 'Display Name' is: '$($finalSpnMiDisplayName)'" -ForegroundColor Green
        Write-Host "The SPN/MI 'Type' is: '$($finalSpnMiType)'" -ForegroundColor Green
    }

    return
}