NewAzDoServiceConnection.psm1

Function New-AzDoServiceConnection {
    <#
    .SYNOPSIS
    This function creates an Azure DevOps service connection for AzureRM.
    .DESCRIPTION
    A service principal with set permissions is created in Azure.
    This principal is used to create an AzureRM service connection in Azure DevOps
    .PARAMETER AzServicePrincipalName
    The name the Service Principal in Azure. Has to be unique
    .PARAMETER AzSubscriptionName
    The subscription that the service connection will connect to.
    If no resourcegroupscope is added, permissions will be set to this subscription
    .PARAMETER AzResourceGroupScope
    A resourcegroup that the Connection needs permissions to.
    If left empty, permissions will be set to the subscription.
    .PARAMETER AzRole
    The AzRoleDefinition that the Service principal needs
    .PARAMETER AzDoOrganizationName
    The organization name in Azure DevOps
    .PARAMETER AzDoProjectName
    The project name in Azure DevOps
    .PARAMETER AzDoConnectionName
    A name for the Azure DevOps Connection.
    If left empty, defaults to the name of the subscription without spaces
    .PARAMETER AzDoUserName
    The username to use to connect to Azure DevOps
    .PARAMETER AzDoToken
    The PAT token to use to connect to Azure DevOps
    .EXAMPLE
    $Parameters = @{
    AzServicePrincipalName = example
    AzSubscriptionName = "subscription01"
    AzResourceGroupScope = "RG01"
    AzRole = "owner"
    AzDoOrganizationName = AzDoCompany
    AzDoProjectName = AzureDeployment
    AzDoUserName = user@domain.com
    AzDoToken = "afweafawe3228faefa0w32f0A"
    }
 
    New-AzDoServiceConnection @Parameters
    ===
 
    Will create a serviceprincipal called example with owner permissions to the resourcegroup RG01.
    Will create a connection in Azure DevOps organization AzDoCompany for project AzureDeployment.
    }
    .NOTES
    PAT token needs permissions for Service Connections: Read, query, & manage
    Minimum permissions for Azure account:
    - Azure Application administrator
    - Owner on the resourcegroup or subscription that is scoped.
 
    Created by Barbara Forbes
    https://4bes.nl
     
    #>

    #Requires -Module Az.Resources, Az.Accounts
    [cmdletbinding()]
    param(
        [parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string]$AzServicePrincipalName,

        [parameter(Mandatory = $true)]
        [ValidateScript( { Get-AzSubscription -SubscriptionName $_ })]
        [string]$AzSubscriptionName,

        [parameter(Mandatory = $false)]
        [string]$AzResourceGroupScope,

        [parameter(Mandatory = $false)]
        [ValidateScript( { Get-AzRoleAssignment -RoleDefinitionName $_ })]
        [string]$AzRole = "Contributor",

        [parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string]$AzDoOrganizationName,

        [parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string]$AzDoProjectName,

        [parameter(Mandatory = $false)]
        [ValidateNotNullorEmpty()]
        [string]$AzDoConnectionName,

        [parameter(Mandatory = $false)]
        [ValidateNotNullorEmpty()]
        [string]$AzDoUserName,

        [parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string]$AzDoToken
    )

    Write-Verbose "Starting Function New-AzDoServiceConnection"

    # Create the header to authenticate to Azure DevOps
    $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $AzDoUserName, $AzDoToken)))
    $Header = @{
        Authorization = ("Basic {0}" -f $base64AuthInfo)
    }
    Remove-Variable AzDoToken
    try {
        $AzSubscription = Get-AzSubscription -SubscriptionName $AzSubscriptionName -ErrorAction Stop
    }
    Catch {
        Throw "Could not find subscription $AzSubscriptionName. Please verify it exists"
    }
    $AzSubscriptionID = $AzSubscription.Id
    $TenantId = $AzSubscription.TenantId
    if ($AzResourceGroupScope) {
        Write-Verbose "Changing Context to $AzSubscriptionName"
        $Null = Set-AzContext $AzSubscriptionID
        # Check if resourcegroup exists before setting it as the scope
        Try {
            $null = Get-AzResourceGroup -Name $AzResourceGroupScope -ErrorAction Stop
            Write-Verbose "Resourcegroup exists"
        }
        Catch {
            Throw "Resourcegroup $AzResourceGroupScope was not found"
        }
        $Scope = "/subscriptions/$AzSubscriptionID/resourceGroups/$AzResourceGroupScope"
    }
    else {
        $Scope = "/subscriptions/$AzSubscriptionID"
    }

    Write-Verbose "Scope set: $Scope"
    # Create the Service Principal
    Try {
        $Parameters = @{
            DisplayName = $AzServicePrincipalName
            Role        = $AzRole
            Scope       = $Scope
            ErrorAction = "Stop"
        }
        $ServicePrincipal = New-AzADServicePrincipal @Parameters
        Write-Verbose "Created ServicePrincipal $AzServicePrincipalName"
    }
    Catch {
        Throw "Could not create the ServicePrincipal: $_"
    }

    ## Get ProjectId
    $URL = "https://dev.azure.com/$AzDoOrganizationName/_apis/projects?api-version=6.0"
    Try {
        $AzDoProjectNameproperties = (Invoke-RestMethod $URL -Headers $Header -ErrorAction Stop).Value
        Write-Verbose "Collected Azure DevOps Projects"
    }
    Catch {
        if ($_ | Select-String -Pattern "Access Denied: The Personal Access Token used has expired.") {
            Throw "Access Denied: The Azure DevOps Personal Access Token used has expired."
        }
        else {
            $ErrorMessage = $_ | ConvertFrom-Json
            Throw "Could not collect project: $($ErrorMessage.message)"
        }
    }
    $AzDoProjectID = ($AzDoProjectNameproperties | Where-Object { $_.Name -eq $AzDoProjectName }).id
    Write-Verbose "Collected ID: $AzDoProjectID"

    if (-not $AzDoConnectionName) {
        $AzDoConnectionName = $AzSubscriptionName -replace " "
    }

    $AZResourcesInstalled = Get-Module Az.Resources -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1
    if ($AZResourcesInstalled.Version.Major -ge 5){
        $PlainTextSecret = $ServicePrincipal.PasswordCredentials.SecretText
        $ServicePrincipalId = $ServicePrincipal.AppId
    }
    else {
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $PlainTextSecret = $ServicePrincipal.Secret | ConvertFrom-SecureString -AsPlainText
        }
        else {
            $PlainTextSecret = [System.Net.NetworkCredential]::new("", $ServicePrincipal.Secret).Password
        }
        $ServicePrincipalId = $ServicePrincipal.ApplicationId
    }


    # Create body for the API call
    $Body = @{
        data                             = @{
            subscriptionId   = $AzSubscriptionID
            subscriptionName = $AzSubscriptionName
            environment      = "AzureCloud"
            scopeLevel       = "Subscription"
            creationMode     = "Manual"
        }
        name                             = ($AzSubscriptionName -replace " ")
        type                             = "AzureRM"
        url                              = "https://management.azure.com/"
        authorization                    = @{
            parameters = @{
                tenantid            = $TenantId
                serviceprincipalid  = $ServicePrincipalId
                authenticationType  = "spnKey"
                serviceprincipalkey = $PlainTextSecret
            }
            scheme     = "ServicePrincipal"
        }
        isShared                         = $false
        isReady                          = $true
        serviceEndpointProjectReferences = @(
            @{
                projectReference = @{
                    id   = $AzDoProjectID
                    name = $AzDoProjectName
                }
                name             = $AzDoConnectionName
            }
        )
    }
    Remove-Variable PlainTextSecret
    $URL = "https://dev.azure.com/$AzDoOrganizationName/$AzDoProjectName/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4"
    $Parameters = @{
        Uri         = $URL
        Method      = "POST"
        Body        = ($Body | ConvertTo-Json -Depth 3)
        Headers     = $Header
        ContentType = "application/json"
        Erroraction = "Stop"
    }
    try {
        Write-Verbose "Creating Connection"
        $Result = Invoke-RestMethod @Parameters
    }
    Catch {
        $ErrorMessage = $_ | ConvertFrom-Json
        Throw "Could not create Connection: $($ErrorMessage.message)"
    }
    Write-Verbose "Connection Created"
    $Result
}