Public/Connect-AzureDevOps.ps1
|
function Connect-AzureDevOps { <# .SYNOPSIS Connects to Azure DevOps using service principal authentication via REST APIs .DESCRIPTION Establishes a connection to Azure DevOps organization using service principal authentication and the Azure DevOps REST API. This function acquires an access token and validates connectivity to the specified organization. Can use explicit parameters or automatically detect credentials from environment variables. Supports both local development and Azure DevOps pipeline execution with service connections. .PARAMETER OrganizationUri The URI of the Azure DevOps organization (e.g., 'https://dev.azure.com/myorg'). Can also be read from AZURE_DEVOPS_ORGANIZATION environment variable. .PARAMETER TenantId The Azure Active Directory tenant ID for authentication. Can also be read from tenantId environment variable (Azure DevOps service connection). .PARAMETER ClientId The service principal (application) client ID. Can also be read from servicePrincipalId environment variable (Azure DevOps service connection). .PARAMETER ClientSecret The service principal client secret for authentication. Can also be read from servicePrincipalKey environment variable (Azure DevOps service connection). .PARAMETER Project The Azure DevOps project name (optional, for scoped operations). Can also be read from AZURE_DEVOPS_PROJECT environment variable. .PARAMETER Force Forces re-authentication even if already connected .EXAMPLE # Using explicit parameters $SecureSecret = ConvertTo-SecureString 'your-client-secret' -AsPlainText -Force Connect-AzureDevOps -OrganizationUri 'https://dev.azure.com/myorg' -TenantId '00000000-0000-0000-0000-000000000000' -ClientId '00000000-0000-0000-0000-000000000000' -ClientSecret $SecureSecret .EXAMPLE # Using environment variables (automatically detected in Azure DevOps pipelines with service connections) Connect-AzureDevOps -OrganizationUri 'https://dev.azure.com/myorg' .EXAMPLE # Using mix of parameters and environment variables Connect-AzureDevOps -OrganizationUri 'https://dev.azure.com/myorg' -Force .EXAMPLE # In Azure DevOps YAML pipeline with service connection named 'MyAzureConnection' # - task: AzurePowerShell@5 # inputs: # azureSubscription: 'MyAzureConnection' # scriptType: 'inlineScript' # azurePowerShellVersion: 'LatestVersion' # inlineScript: | # # Service connection variables are automatically available # Connect-AzureDevOps -OrganizationUri 'https://dev.azure.com/myorg' .OUTPUTS PSCustomObject with connection status and organization information .NOTES Author: The Cloud Explorers Uses Azure DevOps REST API with OAuth2 client credentials flow Follows Microsoft's official OAuth2 authentication documentation Version: 2.2.0 #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [ValidateScript({ if ([string]::IsNullOrEmpty($_)) { return $true } if ($_ -match '^https://dev\.azure\.com/[a-zA-Z0-9\-]+/?$') { $true } else { throw "OrganizationUri must be in format 'https://dev.azure.com/organizationname'" } })] [string]$OrganizationUri, [Parameter(Mandatory = $false)] [ValidateScript({ if ([string]::IsNullOrEmpty($_)) { return $true } if ($_ -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { $true } else { throw "TenantId must be a valid GUID" } })] [string]$TenantId, [Parameter(Mandatory = $false)] [ValidateScript({ if ([string]::IsNullOrEmpty($_)) { return $true } if ($_ -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { $true } else { throw "ClientId must be a valid GUID" } })] [string]$ClientId, [Parameter(Mandatory = $false)] [System.Security.SecureString]$ClientSecret, [Parameter(Mandatory = $false)] [string]$Project, [Parameter(Mandatory = $false)] [switch]$Force ) begin { Write-Verbose "Starting Azure DevOps connection process using REST API" } process { try { # Resolve parameters from environment variables if not provided # Priority: ADO_ variables first, then Azure DevOps service connection variables $ResolvedOrganizationUri = if ([string]::IsNullOrEmpty($OrganizationUri)) { $env:AZURE_DEVOPS_ORGANIZATION } else { $OrganizationUri } $ResolvedTenantId = if ([string]::IsNullOrEmpty($TenantId)) { $env:tenantId } else { $TenantId } $ResolvedClientId = if ([string]::IsNullOrEmpty($ClientId)) { $env:servicePrincipalId } else { $ClientId } $ResolvedClientSecret = if ($null -eq $ClientSecret) { if (-not [string]::IsNullOrEmpty($env:servicePrincipalKey)) { ConvertTo-SecureString $env:servicePrincipalKey -AsPlainText -Force } else { $null } } else { $ClientSecret } $ResolvedProject = if ([string]::IsNullOrEmpty($Project)) { $env:AZURE_DEVOPS_PROJECT } else { $Project } # Validate required parameters are available if ([string]::IsNullOrEmpty($ResolvedOrganizationUri)) { throw "OrganizationUri must be provided either as parameter or AZURE_DEVOPS_ORGANIZATION environment variable" } if ([string]::IsNullOrEmpty($ResolvedTenantId)) { throw "TenantId must be provided either as parameter or tenantId environment variable" } if ([string]::IsNullOrEmpty($ResolvedClientId)) { throw "ClientId must be provided either as parameter or servicePrincipalId environment variable" } if ($null -eq $ResolvedClientSecret) { throw "ClientSecret must be provided either as parameter or servicePrincipalKey environment variable" } # Validate OrganizationUri format if ($ResolvedOrganizationUri -notmatch '^https://dev\.azure\.com/[a-zA-Z0-9\-]+/?$') { throw "OrganizationUri must be in format 'https://dev.azure.com/organizationname'" } Write-Verbose "Using OrganizationUri: $ResolvedOrganizationUri" Write-Verbose "Using TenantId: $ResolvedTenantId" Write-Verbose "Using ClientId: $ResolvedClientId" # Check if we already have a valid connection (unless Force is specified) $ExistingConnection = $script:AzureDevOpsConnection if (-not $Force -and $ExistingConnection -and $ExistingConnection.OrganizationUri -eq $ResolvedOrganizationUri.TrimEnd('/') -and $ExistingConnection.TenantId -eq $ResolvedTenantId -and $ExistingConnection.ClientId -eq $ResolvedClientId -and $ExistingConnection.AccessToken -and $ExistingConnection.TokenExpiry -gt (Get-Date).AddMinutes(5)) { Write-Host "Using existing Azure DevOps connection to: $ResolvedOrganizationUri" -ForegroundColor Green Write-Verbose "Connection still valid until: $($ExistingConnection.TokenExpiry)" return [PSCustomObject]@{ Status = 'Connected (Existing)' OrganizationUri = $ExistingConnection.OrganizationUri OrganizationName = $ExistingConnection.OrganizationName Project = $ExistingConnection.Project TenantId = $ExistingConnection.TenantId ClientId = $ExistingConnection.ClientId ProjectCount = $ExistingConnection.ProjectCount ConnectedAt = $ExistingConnection.ConnectedAt TokenExpiry = $ExistingConnection.TokenExpiry ParameterSource = $ExistingConnection.ParameterSource } } Write-Host "Connecting to Azure DevOps organization: $ResolvedOrganizationUri" -ForegroundColor Green # Get Azure DevOps access token using service principal Write-Verbose "Acquiring Azure DevOps access token..." $AccessToken = Get-AzureDevOpsAccessToken -TenantId $ResolvedTenantId -ClientId $ResolvedClientId -ClientSecret $ResolvedClientSecret if (-not $AccessToken) { throw "Failed to acquire Azure DevOps access token" } # Test the connection to Azure DevOps Write-Verbose "Testing Azure DevOps API connection..." $ConnectionTest = Test-AzureDevOpsConnection -OrganizationUri $ResolvedOrganizationUri -AccessToken $AccessToken if (-not $ConnectionTest.Success) { throw $ConnectionTest.Error } # Calculate token expiry (Azure AD tokens typically last 1 hour) $TokenExpiry = (Get-Date).AddHours(1) # Store connection information in script scope for other functions to use $script:AzureDevOpsConnection = @{ OrganizationUri = $ResolvedOrganizationUri.TrimEnd('/') OrganizationName = $ConnectionTest.OrganizationName Project = $ResolvedProject TenantId = $ResolvedTenantId ClientId = $ResolvedClientId AccessToken = $AccessToken TokenExpiry = $TokenExpiry ConnectedAt = Get-Date ProjectCount = $ConnectionTest.ProjectCount ApiVersion = $ConnectionTest.ApiVersion ParameterSource = @{ OrganizationUri = if ([string]::IsNullOrEmpty($OrganizationUri)) { 'Environment Variable' } else { 'Parameter' } Project = if ([string]::IsNullOrEmpty($Project)) { 'Environment Variable' } else { 'Parameter' } TenantId = if ([string]::IsNullOrEmpty($TenantId)) { 'Environment Variable' } else { 'Parameter' } ClientId = if ([string]::IsNullOrEmpty($ClientId)) { 'Environment Variable' } else { 'Parameter' } ClientSecret = if ($null -eq $ClientSecret) { 'Environment Variable' } else { 'Parameter' } } } # Create return object $ConnectionInfo = [PSCustomObject]@{ Status = 'Connected' OrganizationUri = $script:AzureDevOpsConnection.OrganizationUri OrganizationName = $script:AzureDevOpsConnection.OrganizationName Project = $script:AzureDevOpsConnection.Project TenantId = $script:AzureDevOpsConnection.TenantId ClientId = $script:AzureDevOpsConnection.ClientId ProjectCount = $script:AzureDevOpsConnection.ProjectCount ConnectedAt = $script:AzureDevOpsConnection.ConnectedAt TokenExpiry = $script:AzureDevOpsConnection.TokenExpiry ParameterSource = $script:AzureDevOpsConnection.ParameterSource } Write-Host "Successfully connected to Azure DevOps organization: $($ConnectionTest.OrganizationName)" -ForegroundColor Green Write-Verbose "Connection established at: $($ConnectionInfo.ConnectedAt)" Write-Verbose "Token expires at: $($ConnectionInfo.TokenExpiry)" Write-Verbose "Projects available: $($ConnectionTest.ProjectCount)" return $ConnectionInfo } catch { $ErrorMessage = "Failed to connect to Azure DevOps: $($_.Exception.Message)" Write-Error $ErrorMessage throw $_ } } end { Write-Verbose "Azure DevOps connection process completed" } } |