KnowIT.DevOps.psm1


#region === Source functions ===

### Source file: 'ConvertFrom-ADOResponse.ps1' ###

filter ConvertFrom-ADOResponse {
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSTypeNameAttribute('KnowIT.DevOps.ADOResponse')] $InputObject
    )

    $statusCode = $InputObject.StatusCode
    if($InputObject.Success) {
        return $InputObject.Value
    }

    if($statusCode -eq 404) {
        Write-Verbose "Response ADO REST: No se obtuvo ningun registro"
        return
    }

    Write-Error -ErrorRecord ([Management.Automation.ErrorRecord]::new(
        [Exception]$InputObject.Error.message, $InputObject.Error.typeKey,
        [Management.Automation.ErrorCategory]::InvalidResult, $InputObject))
}

### Source file: 'Invoke-AzDevOpsAPI.ps1' ###

function Invoke-AzDevOpsAPI {
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Organization,

        [string]$Project,

        [string]$Path,

        [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')]
        [string]$Method = 'GET',

        [object]$Body,

        [hashtable]$Headers = @{},

        [string]$Version = '7.1',

        [switch]$DetailResponse
    )

    try {
        Update-CallerPreference $PSCmdlet

        $token = GetADOToken
        $Headers.Authorization = "Bearer $token"
        $Headers.Accept = "application/json; api-version=$Version"
        if(-not [string]::IsNullOrWhiteSpace($Project)) { $Project += '/' }
        $restParams = @{
            Method      = $Method
            Uri         = 'https://dev.azure.com/{0}/{1}_apis/{2}' -f $Organization, $Project, $Path.TrimStart('/')
            Headers     = $Headers
            Body        = $Body ? (ConvertTo-Json $Body -Depth 10) : $null
            ContentType = 'application/json'
            Verbose     = $false
        }
        Write-Verbose "Request ADO REST: [v$Version] $($restParams.Method.ToUpper()) $($restParams.Uri)"
        if($Body) { Write-Debug "Body: $($restParams.Body)" }

        $result = Invoke-RestMethod @restParams -SkipHttpErrorCheck -StatusCodeVariable statusCode

        if($statusCode -lt 400) {
            Write-Verbose "Response ADO REST: [$statusCode] $([Net.HttpStatusCode]$statusCode)"
            $response = [PSCustomObject]@{
                PSTypeName = 'KnowIT.DevOps.ADOResponse'
                Success    = $true
                StatusCode = $statusCode
                Value      = $result.value ?? $result
                Count      = $result.count
            }
        }
        else { #TODO: Manjerar la respuesta 404 cuando la ruta es incorrecta
            Write-Debug 'Procesando error en la respuesta del API...'
            Write-Verbose "Response ADO REST: [$statusCode] $($result.typeKey) - $($result.message -replace "`r?`n", ' ')"
            $response = [PSCustomObject]@{
                PSTypeName = 'KnowIT.DevOps.ADOResponse'
                Success    = $false
                StatusCode = $statusCode
                Error      = $result
            }
        }

        if($DetailResponse) {
            $response
        }
        else {
            $response | ConvertFrom-ADOResponse
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }

}

### Source file: 'Set-AzDevopsAzureConnection.ps1' ###

function Set-AzDevOpsAzureConnection {

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidatePattern('^[a-z]+(-[a-z0-9]+)*$')]
        [string]$Name,

        [Parameter(Mandatory)]
        [string]$Organization,

        [Parameter(Mandatory)]
        [string]$ProjectName,

        [Parameter(Mandatory)]
        [string]$ResourceGroup,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Roles,

        [switch]$PassThru
    )

    try {
        Update-CallerPreference $PSCmdlet
        Write-Verbose "[+] Establece conexión [$Name] en el proyecto '$ProjectName' de Azure DevOps" -Verbose
        $azContext = Get-AzContext
        if($azContext.Subscription.TenantId -ne $azContext.Subscription.HomeTenantId) {
            throw "La conexión actual a Azure se realizo mediante una cuenta de administración delegada ($($azContext.Account)) a la suscripción '$($azContext.Subscription.Name)'.
            Es necesario conectarse con una cuenta con permisos de acceso al Tenant propietario de la suscripción [$($azContext.Subscription.HomeTenantId)]."

        }

        $project = Invoke-AzDevOpsApi -Organization $Organization -Path "projects/$ProjectName"
        if(!$project) {
            throw "No existe o no se tiene acceso al proyecto '$ProjectName' en la Organización [$Organization] de Azure DevOps"
        }

        $devopsSP = Register-EntraServicePrincipal "AzDevOps/$Name"

        $connection = @{
            name = $Name
            type = 'AzureRM'
            url  = 'https://management.azure.com/'
            data = @{
                subscriptionId   = $azContext.Subscription.Id
                subscriptionName = $azContext.Subscription.Name
                environment      = 'AzureCloud'
                scopeLevel       = 'Subscription'
            }
            authorization = @{
                scheme     = 'WorkloadIdentityFederation'
                parameters = @{
                    serviceprincipalid = $devopsSP.appId
                    tenantid = $devopsSP.appOwnerOrganizationId
                }
            }
            serviceEndpointProjectReferences = @(
                @{
                    projectReference = @{
                        id = $project.id
                        name = $project.name
                    }
                    name = $Name
                }
            )
        }
        $endpoint = Invoke-AzDevOpsApi -Organization $Organization -Project $project.name -Path "serviceEndpoint/endpoints?endpointNames=$Name"
        if($endpoint) {
            $endpoint = Invoke-AzDevOpsApi -Organization $Organization -Project $project.name -Path "serviceEndpoint/endpoints/$($endpoint.id)" -Method PUT -Body $connection
        }
        else {
            $endpoint = Invoke-AzDevOpsApi -Organization $Organization -Project $project.name -Path 'serviceEndpoint/endpoints' -Method POST -Body $connection
        }

        Set-EntraAppFederatedIdentity $devopsSP.appId -Name 'AzDevOps-Federated' -Issuer $endpoint.authorization.parameters.workloadIdentityFederationIssuer -Subject $endpoint.authorization.parameters.workloadIdentityFederationSubject

        Write-Verbose " Asignando roles requeridos a Resource Group: [$ResourceGroup]" -Verbose
        $assignedRoles = (Get-AzRoleAssignment -ResourceGroupName $ResourceGroup -ObjectId $devopsSP.id).RoleDefinitionName
        $Roles.Where({ $_ -notin $assignedRoles }).ForEach({
            $null = New-AzRoleAssignment -ResourceGroupName $ResourceGroup -RoleDefinitionName $_ -ObjectId $devopsSP.id })

        if($PassThru) { $devopsSP }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'KnowIT.ModuleHelpers.ps1' ###
function Update-CallerPreference {
    # https://devblogs.microsoft.com/scripting/weekend-scripter-access-powershell-preference-variables/
    param(
        [ValidateNotNull()]
        [PSTypeName('System.Management.Automation.PSScriptCmdlet')]$ScriptCmdlet = (Get-Variable PSCmdlet -Scope 1 -ValueOnly),

        [ValidateSet('ErrorAction', 'Warning', 'Verbose', 'Debug', 'Information', 'Progress', 'Confirm', 'WhatIf')]
        [string[]]$Skip
    )

    $commonParameters = 'ErrorAction', 'Warning', 'Verbose', 'Debug', 'Information', 'Progress', 'Confirm', 'WhatIf'
    $currentDebugPreference = $DebugPreference

    Write-Debug "Updating [$($ScriptCmdlet.MyInvocation.MyCommand)] Preference variables:"
    foreach($p in $commonParameters) {
        if($ScriptCmdlet.MyInvocation.BoundParameters.ContainsKey($p)) {
            continue
        }
        $var = "${p}Preference"

        if($p -eq 'ErrorAction') {
            $val = 'Stop'
            $scope = 'Forced'
        }
        elseif($p -in $Skip) {
            $val = Get-Variable -Scope Global -Name $var -ValueOnly
            $scope = 'Global'
        }
        else {
            $val = $ScriptCmdlet.GetVariableValue($var)
            $scope = 'Caller'
        }
        Write-Debug " (From $scope scope) $var = $val " -Debug:$currentDebugPreference
        Set-Variable -Scope 1 -Name $var -Value $val
    }
}

function Map-Object {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '', Justification = 'Internal functions')]
    param([scriptblock]$ScriptBlock)

begin {
    $code = "& { process { $ScriptBlock } }"
    $pipeline = [scriptblock]::Create($code).GetSteppablePipeline()
    $pipeline.Begin($true)
}
process {
    $pipeline.Process($_)
}
end {
    $pipeline.End()
}
}

### Source file: 'Tokens.ps1' ###

function GetADOToken {
    $token = Get-AzAccessToken -ResourceUrl 'https://app.vssps.visualstudio.com' -AsSecureString -Debug:$false
    ConvertFrom-SecureString $token.Token -AsPlainText
}

#endregion