Private/_Auth.ps1
|
function String-ContainsValue { param( [AllowNull()] [string] $Value ) return (-not [string]::IsNullOrWhiteSpace($Value)) } function Get-PPDOPacPath { param( [string] $PacPath ) if (String-ContainsValue $PacPath) { return $PacPath } return "$env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe" } function Invoke-PPDOExternalCommand { param( [Parameter(Mandatory = $true)] [string] $FilePath, [Parameter(Mandatory = $false)] [string[]] $Arguments = @(), [Parameter(Mandatory = $false)] [switch] $ThrowOnError ) Write-Verbose ("Running: {0} {1}" -f $FilePath, ($Arguments -join ' ')) $output = & $FilePath @Arguments $exitCode = $LASTEXITCODE if ($ThrowOnError -and $exitCode -ne 0) { throw ("Command failed with exit code {0}: {1} {2}`n{3}" -f $exitCode, $FilePath, ($Arguments -join ' '), ($output -join [Environment]::NewLine)) } return $output } function Resolve-PPDOAuthContext { [CmdletBinding()] param( [string] $ServerUrl = $global:devops_ServerUrl, [string] $UserName, [string] $Password, [string] $TenantId = $global:devops_TenantID, [ValidateSet('Auto', 'ServicePrincipal', 'User', 'ManagedIdentity', 'AzCredential')] [string] $AuthType = 'Auto', [bool] $UseClientSecret = $false, [bool] $UseManagedIdentity = $false, [string] $PacProfileName = 'ppdo', [string] $PacPath ) if (-not (String-ContainsValue $ServerUrl)) { throw "ServerUrl was not provided and global variable devops_ServerUrl is empty." } $resolvedType = $AuthType if ($resolvedType -eq 'Auto') { if ($UseManagedIdentity -or $global:devops_DataverseCredType -eq 'managedIdentity') { $resolvedType = 'ManagedIdentity' } elseif (-not (String-ContainsValue $UserName) -and -not(String-ContainsValue $Password)) { #When no user name or password, assume az credential $resolvedType = 'AzCredential' } elseif ($UseClientSecret -and $global:devops_DataverseCredType -eq 'servicePrincipal' -and (String-ContainsValue $global:devops_ClientID) -and ((String-ContainsValue $global:clientSecret) -or (String-ContainsValue $Password))) { #client secret is no longer best practice, use federated managed identity $resolvedType = 'ServicePrincipal' } elseif ($global:devops_DataverseCredType -eq 'user' -or (String-ContainsValue $UserName) -or (String-ContainsValue $global:devops_DataverseEmail)) { #Only when we don't have a service principal $resolvedType = 'User' } else { #default to this strategy $resolvedType = 'AzCredential' } } switch ($resolvedType) { 'ServicePrincipal' { if (-not (String-ContainsValue $UserName)) { $UserName = $global:devops_ClientID } if (-not (String-ContainsValue $Password)) { $Password = $global:clientSecret } if (-not (String-ContainsValue $TenantId)) { $TenantId = $global:devops_TenantID } if (-not (String-ContainsValue $UserName)) { throw "Service principal authentication requires ApplicationId/UserName or global devops_ClientID." } if (-not (String-ContainsValue $Password)) { throw "Service principal authentication requires ClientSecret/Password or global clientSecret." } if (-not (String-ContainsValue $TenantId)) { throw "Service principal authentication requires TenantId or global devops_TenantID." } } 'User' { if (-not (String-ContainsValue $UserName)) { $UserName = $global:devops_DataverseEmail } if (-not (String-ContainsValue $Password)) { $Password = $global:Password } if (-not (String-ContainsValue $UserName)) { throw "User authentication requires UserName or global devops_DataverseEmail." } } 'ManagedIdentity' { # If UserName is supplied for managed identity, treat it as a user-assigned managed identity client id. if (String-ContainsValue $UserName) { $env:AZURE_CLIENT_ID = $UserName } } 'AzCredential' { Write-Verbose "Using current Azure CLI credential" $account = az account show 2>$null | ConvertFrom-Json if ($null -eq $account) { throw "No Azure CLI login found. Run 'az login' first." } } } $ppdoContext = [pscustomobject]@{ AuthType = $resolvedType ServerUrl = $ServerUrl UserName = $UserName Password = $Password TenantId = $TenantId PacProfileName = $PacProfileName PacPath = (Get-PPDOPacPath -PacPath $PacPath) AccessToken = $null UseClientSecret = ($resolvedType -eq 'ServicePrincipal') UseManagedIdentity = ($resolvedType -eq 'ManagedIdentity') } Write-Verbose ($ppdoContext | Format-List * | Out-String) return $ppdoContext } function Connect-PPDOAzureCli { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject] $Context, [bool] $ForceLogin = $false, [bool] $UseDeviceCode = $false ) # For user auth, an existing Azure CLI token is acceptable. # For service principal and managed identity, sign in explicitly so the active Azure CLI identity # matches the same context used by PAC CLI and the ServiceClient token provider. if (-not $ForceLogin -and $Context.AuthType -eq 'User') { $existingToken = $null try { $existingToken = az account get-access-token --resource $Context.ServerUrl --query accessToken --output tsv 2>$null } catch { $existingToken = $null } if (String-ContainsValue $existingToken) { Write-Verbose "Azure CLI already has a token for Dataverse." return } } switch ($Context.AuthType) { 'AzCredential' { Write-Verbose "Using existing Azure CLI login" } 'ServicePrincipal' { $args = @( 'login', '--service-principal', '-u', $Context.UserName, '-p', $Context.Password, '--tenant', $Context.TenantId, '--allow-no-subscriptions' ) $null = Invoke-PPDOExternalCommand -FilePath 'az' -Arguments $args -ThrowOnError } 'ManagedIdentity' { $args = @('login', '--identity', '--allow-no-subscriptions') if (String-ContainsValue $Context.UserName) { $args += @('--username', $Context.UserName) } $null = Invoke-PPDOExternalCommand -FilePath 'az' -Arguments $args -ThrowOnError } 'User' { $args = @('login', '--allow-no-subscriptions') if ($UseDeviceCode) { $args += '--use-device-code' } if (String-ContainsValue $Context.TenantId) { $args += @('--tenant', $Context.TenantId) } $null = Invoke-PPDOExternalCommand -FilePath 'az' -Arguments $args -ThrowOnError } } } function New-PPDOPacAuth { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject] $Context, [bool] $DeleteExisting = $false, [bool] $UseDeviceCode = $false ) if ($DeleteExisting) { Write-Verbose ("Deleting any existing pac auth connection named '{0}'" -f $Context.PacProfileName) $null = Invoke-PPDOExternalCommand -FilePath $Context.PacPath -Arguments @('auth', 'delete', '--name', $Context.PacProfileName) } else { $profiles = Invoke-PPDOExternalCommand -FilePath $Context.PacPath -Arguments @('auth', 'list') if (($profiles -join [Environment]::NewLine) -match [regex]::Escape($Context.PacProfileName)) { $null = Invoke-PPDOExternalCommand -FilePath $Context.PacPath -Arguments @('auth', 'select', '--name', $Context.PacProfileName) -ThrowOnError return } } $args = @('auth', 'create', '--environment', $Context.ServerUrl, '--name', $Context.PacProfileName) switch ($Context.AuthType) { 'ServicePrincipal' { $args += @( '--applicationId', $Context.UserName, '--clientSecret', $Context.Password, '--tenant', $Context.TenantId ) } 'ManagedIdentity' { $args += '--managedIdentity' } 'User' { $args += @('--username', $Context.UserName) if (String-ContainsValue $Context.Password) { $args += @('--password', $Context.Password) } elseif ($UseDeviceCode) { $args += '--deviceCode' } } } $null = Invoke-PPDOExternalCommand -FilePath $Context.PacPath -Arguments $args -ThrowOnError $null = Invoke-PPDOExternalCommand -FilePath $Context.PacPath -Arguments @('auth', 'select', '--name', $Context.PacProfileName) -ThrowOnError } function Get-PPDODataverseAccessToken { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $ServerUrl, [pscustomobject] $Context ) if ($null -ne $Context -and (String-ContainsValue $Context.AccessToken)) { return $Context.AccessToken } $token = az account get-access-token --resource $ServerUrl --query accessToken --output tsv if (-not (String-ContainsValue $token)) { throw "Azure CLI did not return an access token for '$ServerUrl'." } if ($null -ne $Context) { $Context.AccessToken = $token } return $token } function Connect-PPDOServiceClient { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject] $Context, [bool] $VerboseLogging = $false ) if (-not (Get-Command Connect-Dataverse -ErrorAction SilentlyContinue)) { throw "Connect-Dataverse was not found." } switch ($Context.AuthType) { 'ServicePrincipal' { $connectionString = "AuthType=ClientSecret;" + "Url=$($Context.ServerUrl);" + "ClientId=$($Context.UserName);" + "ClientSecret=$($Context.Password)" Write-Verbose "Connecting using Service Principal" $conn = Connect-Dataverse ` -ConnectionString $connectionString ` -Verbose:$VerboseLogging } 'User' { if (-not [string]::IsNullOrWhiteSpace($Context.Password)) { $connectionString = "AuthType=OAuth;" + "Username=$($Context.UserName);" + "Password=$($Context.Password);" + "Url=$($Context.ServerUrl);" + "AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;" + "RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97;" + "LoginPrompt=Auto" Write-Verbose "Connecting using Username/Password" $conn = Connect-Dataverse ` -ConnectionString $connectionString ` -Verbose:$VerboseLogging } else { Write-Verbose "Connecting using Azure CLI token" $accessToken = Get-PPDODataverseAccessToken ` -ServerUrl $Context.ServerUrl ` -Context $Context $tokenScriptBlock = { param($ServerUrl) return $accessToken }.GetNewClosure() $conn = Connect-Dataverse ` -InstanceUrl $Context.ServerUrl ` -TokenProvider $tokenScriptBlock } } { $_ -in 'AzCredential', 'ManagedIdentity' } { Write-Verbose "Connecting using Azure CLI token" $accessToken = Get-PPDODataverseAccessToken ` -ServerUrl $Context.ServerUrl ` -Context $Context if ($VerboseLogging) { $claims = Get-PPDOJwtTokenClaims ` -JwtToken $accessToken Write-Verbose ( "Token aud={0}; appid={1}; oid={2}; tid={3}" -f ` $claims.aud, $claims.appid, $claims.oid, $claims.tid ) } $tokenScriptBlock = { param($ServerUrl) return $accessToken }.GetNewClosure() $conn = Connect-Dataverse ` -InstanceUrl $Context.ServerUrl ` -TokenProvider $tokenScriptBlock } default { throw "Unsupported AuthType '$($Context.AuthType)'" } } if ($null -eq $conn) { throw "Connect-Dataverse returned null." } if (-not $conn.IsReady) { Write-Verbose "LastCrmError: $($conn.LastCrmError)" if ($conn.LastCrmException) { Write-Verbose $conn.LastCrmException.Message } throw ("Dataverse ServiceClient is not ready. {0}" -f $conn.LastCrmError) } return $conn } function Test-PPDODataverseToken { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $ServerUrl, [pscustomobject] $Context ) $accessToken = Get-PPDODataverseAccessToken -ServerUrl $ServerUrl -Context $Context $headers = @{ Authorization = "Bearer $accessToken" Accept = 'application/json' 'OData-Version' = '4.0' 'OData-MaxVersion' = '4.0' } return Invoke-RestMethod -Method Get -Uri "$ServerUrl/api/data/v9.2/WhoAmI" -Headers $headers } function Connect-PPDODataverse { [CmdletBinding()] param( [string] $ServerUrl = $global:devops_ServerUrl, [string] $UserName, [string] $Password, [string] $TenantId = $global:devops_TenantID, [ValidateSet('Auto', 'ServicePrincipal', 'User', 'ManagedIdentity', 'AzCredential')] [string] $AuthType = 'Auto', [bool] $UseClientSecret = $false, [bool] $UseManagedIdentity = $false, [bool] $UseDeviceCode = $false, [bool] $ConnectAzureCli = $true, [bool] $ConnectPacCli = $true, [bool] $ConnectServiceClient = $true, [bool] $ValidateWhoAmI = $true, [bool] $VerboseLogging = $false, [string] $PacProfileName = 'ppdo', [string] $PacPath ) Write-Verbose "ServerUrl = $ServerUrl" $context = Resolve-PPDOAuthContext ` -ServerUrl $ServerUrl ` -UserName $UserName ` -Password $Password ` -TenantId $TenantId ` -AuthType $AuthType ` -UseClientSecret $UseClientSecret ` -UseManagedIdentity $UseManagedIdentity ` -PacProfileName $PacProfileName ` -PacPath $PacPath $global:devops_DataverseCredType = $context.AuthType.Substring(0, 1).ToLowerInvariant() + $context.AuthType.Substring(1) $global:devops_ServerUrl = $context.ServerUrl $global:devops_TenantID = $context.TenantId if ($context.AuthType -eq 'ServicePrincipal') { $global:devops_ClientID = $context.UserName $global:clientSecret = $context.Password } elseif ($context.AuthType -eq 'User') { $global:devops_DataverseEmail = $context.UserName $global:Password = $context.Password } if ($ConnectAzureCli) { Write-Verbose "Connecting Azure CLI" Connect-PPDOAzureCli -Context $context -UseDeviceCode $UseDeviceCode } if ($ConnectPacCli) { Write-Verbose "Connecting PAC" New-PPDOPacAuth -Context $context -UseDeviceCode $UseDeviceCode } $whoAmI = $null if ($ValidateWhoAmI) { Write-Verbose "Testing Token" $whoAmI = Test-PPDODataverseToken -ServerUrl $context.ServerUrl -Context $context } $connection = $null if ($ConnectServiceClient) { Write-Verbose "Connecting Service Client" $connection = Connect-PPDOServiceClient -Context $context -VerboseLogging $VerboseLogging } $global:devops_HasDataverseLogin = $true $ppdoConnection = [pscustomobject]@{ AuthType = $context.AuthType ServerUrl = $context.ServerUrl TenantId = $context.TenantId UserName = $context.UserName PacProfile = $context.PacProfileName WhoAmI = $whoAmI ServiceClient = $connection } return $ppdoConnection } function Get-PPDOJwtTokenClaims { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $JwtToken ) $tokenSplit = $JwtToken.Split('.') if ($tokenSplit.Count -lt 2) { throw 'The supplied token is not a valid JWT.' } $claimsSegment = $tokenSplit[1].Replace(' ', '+').Replace('-', '+').Replace('_', '/') $mod = $claimsSegment.Length % 4 if ($mod -gt 0) { $claimsSegment = $claimsSegment + ('=' * (4 - $mod)) } $decodedClaimsSegment = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($claimsSegment)) return ($decodedClaimsSegment | ConvertFrom-Json) } # Backwards-compatible wrapper for existing calls that only need PAC authentication. function New-PACAuth { [CmdletBinding()] param( [string] $ServerUrl = $global:devops_ServerUrl, [string] $UserName, [string] $Password, [string] $TenantId = $global:devops_TenantID, [bool] $UseManagedIdentity = $false, [bool] $UseClientSecret = $false, [bool] $UseDeviceCode = $false ) $null = Connect-PPDODataverse ` -ServerUrl $ServerUrl ` -UserName $UserName ` -Password $Password ` -TenantId $TenantId ` -AuthType Auto ` -UseManagedIdentity $UseManagedIdentity ` -UseClientSecret $UseClientSecret ` -UseDeviceCode $UseDeviceCode ` -ConnectAzureCli $false ` -ConnectPacCli $true ` -ConnectServiceClient $false } # Backwards-compatible wrapper for existing code that expects a Dataverse connection object. function Get-DataverseConnection { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $DeployServerUrl, [bool] $UseManagedIdentity = $false, [bool] $UseClientSecret = $false, [bool] $UseDeviceCode = $false, [bool] $ValidateWhoAmI = $false ) $result = Connect-PPDODataverse ` -ServerUrl $DeployServerUrl ` -AuthType Auto ` -UseManagedIdentity $UseManagedIdentity ` -UseClientSecret $UseClientSecret ` -UseDeviceCode $UseDeviceCode ` -ValidateWhoAmI $ValidateWhoAmI ` -ConnectAzureCli $true ` -ConnectPacCli $true ` -ConnectServiceClient $true return $result.ServiceClient } # Backwards-compatible wrapper for your previous Azure CLI token based ServiceClient path. function Get-DataverseConnection2 { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $ServerUrl, [bool] $VerboseLogging = $false ) $result = Connect-PPDODataverse ` -ServerUrl $ServerUrl ` -AuthType Auto ` -VerboseLogging $VerboseLogging ` -ConnectAzureCli $true ` -ConnectPacCli $false ` -ConnectServiceClient $true return $result.ServiceClient } # Backwards-compatible wrapper for the old Connect-Cli function. function Connect-Cli { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $UserName, [Parameter(Mandatory = $false)] [string] $Password = '', [Parameter(Mandatory = $true)] [string] $TenantId, [bool] $UseClientSecret = $false, [bool] $UseManagedIdentity = $false, [bool] $UseDeviceCode = $false ) $result = Connect-PPDODataverse ` -ServerUrl $global:devops_ServerUrl ` -UserName $UserName ` -Password $Password ` -TenantId $TenantId ` -AuthType Auto ` -UseClientSecret $UseClientSecret ` -UseManagedIdentity $UseManagedIdentity ` -UseDeviceCode $UseDeviceCode ` -ConnectAzureCli $true ` -ConnectPacCli $true ` -ConnectServiceClient $false return $result } function Get-AdoAccessToken { [CmdletBinding()] param() Write-Verbose "Getting Updated ADO Access Token" $azureDevopsResourceId = "499b84ac-1321-427f-aa17-267ca6975798" az account show --output none 2>$null if ($LASTEXITCODE -ne 0) { Write-Host "No active session found. Please sign in..." az login } $token = az account get-access-token ` --resource $azureDevopsResourceId | ConvertFrom-Json $env:AZURE_DEVOPS_EXT_PAT = $token.accessToken return $token } |