diff --git a/Tasks/AzurePowerShell/Utility.ps1 b/Tasks/AzurePowerShell/Utility.ps1 index a7e67876b6fd..b385ac9ba042 100644 --- a/Tasks/AzurePowerShell/Utility.ps1 +++ b/Tasks/AzurePowerShell/Utility.ps1 @@ -30,7 +30,7 @@ function Update-PSModulePathForHostedAgent { $hostedAgentAzureModulePath = Get-LatestModule -patternToMatch "^azure_[0-9]+\.[0-9]+\.[0-9]+$" -patternToExtract "[0-9]+\.[0-9]+\.[0-9]+$" -Classic:$true } - if($authScheme -eq 'ServicePrincipal' -or $authScheme -eq '') + if($authScheme -eq 'ServicePrincipal' -or $authScheme -eq 'ManagedServiceIdentity' -or $authScheme -eq '') { $env:PSModulePath = $hostedAgentAzureModulePath + ";" + $env:PSModulePath $env:PSModulePath = $env:PSModulePath.TrimStart(';') diff --git a/Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1 b/Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1 index 015787aa84ec..f3869c94d630 100644 --- a/Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1 +++ b/Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1 @@ -161,11 +161,108 @@ function Initialize-AzureSubscription { Set-CurrentAzureRMSubscription -SubscriptionId $Endpoint.Data.SubscriptionId -TenantId $Endpoint.Auth.Parameters.TenantId } - } else { + } elseif ($Endpoint.Auth.Scheme -eq 'ManagedServiceIdentity') { + $accountId = $env:BUILD_BUILDID + if($env:RELEASE_RELEASEID){ + $accountId = $env:RELEASE_RELEASEID + } + $date = Get-Date -Format o + $accountId = -join($accountId, "-", $date) + $access_token = Get-MsiAccessToken $Endpoint 0 0 + try { + Write-Host "##[command]Add-AzureRmAccount -AccessToken ****** -AccountId $accountId " + $null = Add-AzureRmAccount -AccessToken $access_token -AccountId $accountId + } catch { + # Provide an additional, custom, credentials-related error message. + Write-VstsTaskError -Message $_.Exception.Message + throw (New-Object System.Exception((Get-VstsLocString -Key AZ_MsiFailure), $_.Exception)) + } + + Set-CurrentAzureRMSubscription -SubscriptionId $Endpoint.Data.SubscriptionId -TenantId $Endpoint.Auth.Parameters.TenantId + }else { throw (Get-VstsLocString -Key AZ_UnsupportedAuthScheme0 -ArgumentList $Endpoint.Auth.Scheme) + } +} + + +# Get the Bearer Access Token from the Endpoint +function Get-MsiAccessToken { + [CmdletBinding()] + param([Parameter(Mandatory=$true)] $endpoint, + [Parameter(Mandatory=$true)] $retryCount, + [Parameter(Mandatory=$true)] $timeToWait) + + $msiClientId = ""; + if($endpoint.Data.msiClientId){ + $msiClientId = "&client_id=" + $endpoint.Data.msiClientId; + } + $tenantId = $endpoint.Auth.Parameters.TenantId + + # Prepare contents for GET + $method = "GET" + $apiVersion = "2018-02-01"; + $authUri = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=" + $apiVersion + "&resource=" + $endpoint.Url + $msiClientId; + + # Call Rest API to fetch AccessToken + Write-Verbose "Fetching Access Token For MSI" + + try + { + $retryLimit = 5; + $proxyUri = Get-ProxyUri $authUri + if ($proxyUri -eq $null) + { + Write-Verbose "No proxy settings" + $response = Invoke-WebRequest -Uri $authUri -Method $method -Headers @{Metadata="true"} -UseBasicParsing + } + else + { + Write-Verbose "Using Proxy settings" + $response = Invoke-WebRequest -Uri $authUri -Method $method -Headers @{Metadata="true"} -UseDefaultCredentials -Proxy $proxyUri -ProxyUseDefaultCredentials -UseBasicParsing + } + + # Action on the based of response + if(($response.StatusCode -eq 429) -or ($response.StatusCode -eq 500)) + { + if($retryCount -lt $retryLimit) + { + $retryCount += 1 + $waitedTime = 2000 + $timeToWait * 2 + Start-Sleep -m $waitedTime + Get-MsiAccessToken $endpoint $retryCount $waitedTime + } + else + { + throw (Get-VstsLocString -Key AZ_MsiAccessTokenFetchFailure -ArgumentList $response.StatusCode, $response.StatusDescription) + } + } + elseif ($response.StatusCode -eq 200) + { + $accessToken = $response.Content | ConvertFrom-Json + return $accessToken.access_token + } + else + { + throw (Get-VstsLocString -Key AZ_MsiAccessNotConfiguredProperlyFailure -ArgumentList $response.StatusCode, $response.StatusDescription) + } + + } + catch + { + $exceptionMessage = $_.Exception.Message.ToString() + Write-Verbose "ExceptionMessage: $exceptionMessage (in function: Get-MsiAccessToken)" + if($exceptionMessage -match "400") + { + throw (Get-VstsLocString -Key AZ_MsiAccessNotConfiguredProperlyFailure -ArgumentList $response.StatusCode, $response.StatusDescription) + } + else + { + throw $_.Exception + } } } + function Set-CurrentAzureSubscription { [CmdletBinding()] param( diff --git a/Tasks/Common/VstsAzureHelpers_/Strings/resources.resjson/en-US/resources.resjson b/Tasks/Common/VstsAzureHelpers_/Strings/resources.resjson/en-US/resources.resjson index 7704f009e32b..e7b175ca7567 100644 --- a/Tasks/Common/VstsAzureHelpers_/Strings/resources.resjson/en-US/resources.resjson +++ b/Tasks/Common/VstsAzureHelpers_/Strings/resources.resjson/en-US/resources.resjson @@ -8,5 +8,8 @@ "loc.messages.AZ_ServicePrincipalAuthNotSupportedAzureVersion0": "Service principal authentication is not supported in version '{0}' of the Azure module.", "loc.messages.AZ_UnsupportedAuthScheme0": "Unsupported authentication scheme '{0}' for Azure endpoint.", "loc.messages.AZ_AvailableModules": "The list of available {0} modules:", - "loc.messages.AZ_InvalidARMEndpoint": "Specified AzureRM endpoint is invalid." + "loc.messages.AZ_InvalidARMEndpoint": "Specified AzureRM endpoint is invalid.", + "loc.messages.AZ_MsiAccessNotConfiguredProperlyFailure": "Could not fetch access token for Managed Service Principal. Please configure Managed Service Identity (MSI) for virtual machine 'https://aka.ms/azure-msi-docs'. Status code: '{0}', status message: {1}", + "loc.messages.AZ_MsiAccessTokenFetchFailure": "Could not fetch access token for Managed Service Principal. Status code: '{0}', status message: {1}", + "loc.messages.AZ_MsiFailure": "Could not fetch access token for Managed Service Principal. {0}" } \ No newline at end of file diff --git a/Tasks/Common/VstsAzureHelpers_/Tests/Initialize-AzureSubscription.ManagedServiceIdentity.ps1 b/Tasks/Common/VstsAzureHelpers_/Tests/Initialize-AzureSubscription.ManagedServiceIdentity.ps1 new file mode 100644 index 000000000000..f68bf4c033dd --- /dev/null +++ b/Tasks/Common/VstsAzureHelpers_/Tests/Initialize-AzureSubscription.ManagedServiceIdentity.ps1 @@ -0,0 +1,55 @@ +[CmdletBinding()] +param() + +# Arrange. +. $PSScriptRoot\..\..\..\..\Tests\lib\Initialize-Test.ps1 +Microsoft.PowerShell.Core\Import-Module Microsoft.PowerShell.Security +Unregister-Mock Import-Module +Register-Mock Write-VstsTaskError +$module = Microsoft.PowerShell.Core\Import-Module $PSScriptRoot\.. -PassThru + +$endpoint = @{ + Auth = @{ + Parameters = @{ + ServicePrincipalId = 'Some service principal ID' + ServicePrincipalKey = 'Some service principal key' + TenantId = 'Some tenant ID' + } + Scheme = 'ManagedServiceIdentity' + } + Data = @{ + SubscriptionId = 'Some subscription ID' + SubscriptionName = 'Some subscription name' + } +} + +$content = @" + {"access_token" : "Dummy Token" } +"@ + +$response = @{ + Content = $content + StatusCode = 200 + StatusDescription = 'OK' +}; + +$variableSets = @( + @{ StorageAccount = 'Some storage account' } +) +foreach ($variableSet in $variableSets) { + Write-Verbose ('-' * 80) + Unregister-Mock Add-AzureRMAccount + Unregister-Mock Set-CurrentAzureRMSubscription + Unregister-Mock Invoke-WebRequest + Unregister-Mock Set-UserAgent + Register-Mock Add-AzureRMAccount { 'some output' } + Register-Mock Set-CurrentAzureRMSubscription + Register-Mock Set-UserAgent + Register-Mock Invoke-WebRequest { $response } + + # Act. + $result = & $module Initialize-AzureSubscription -Endpoint $endpoint -StorageAccount $variableSet.StorageAccount + + Assert-AreEqual $null $result + Assert-WasCalled Set-CurrentAzureRMSubscription -- -SubscriptionId $endpoint.Data.SubscriptionId -TenantId $endpoint.Auth.Parameters.TenantId +} \ No newline at end of file diff --git a/Tasks/Common/VstsAzureHelpers_/Tests/L0.ts b/Tasks/Common/VstsAzureHelpers_/Tests/L0.ts index ec8b910669a7..1bf2b7b9b098 100644 --- a/Tasks/Common/VstsAzureHelpers_/Tests/L0.ts +++ b/Tasks/Common/VstsAzureHelpers_/Tests/L0.ts @@ -63,6 +63,9 @@ describe('Common-VstsAzureHelpers_ Suite', function () { it('(Initialize-Azure) throws when service name is null', (done) => { psr.run(path.join(__dirname, 'Initialize-Azure.ThrowsWhenServiceNameIsNull.ps1'), done); }) + it('(Initialize-AzureSubscription) manged service identity should pass ', (done) => { + psr.run(path.join(__dirname, 'Initialize-AzureSubscription.ManagedServiceIdentity.ps1'), done); + }) it('(Initialize-AzureSubscription) passes values when cert auth', (done) => { psr.run(path.join(__dirname, 'Initialize-AzureSubscription.PassesValuesWhenCertAuth.ps1'), done); }) diff --git a/Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1 b/Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1 index 9e8852a545f2..70e09d45d403 100644 --- a/Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1 +++ b/Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1 @@ -38,7 +38,7 @@ function Initialize-Azure { # Determine which modules are preferred. $preferredModules = @( ) - if ($endpoint.Auth.Scheme -eq 'ServicePrincipal') { + if (($endpoint.Auth.Scheme -eq 'ServicePrincipal') -or ($endpoint.Auth.Scheme -eq 'ManagedServiceIdentity')) { $preferredModules += 'AzureRM' } elseif ($endpoint.Auth.Scheme -eq 'UserNamePassword' -and $strict -eq $false) { $preferredModules += 'Azure' diff --git a/Tasks/Common/VstsAzureHelpers_/module.json b/Tasks/Common/VstsAzureHelpers_/module.json index 4f6793a1b6a6..002b72a12346 100644 --- a/Tasks/Common/VstsAzureHelpers_/module.json +++ b/Tasks/Common/VstsAzureHelpers_/module.json @@ -9,6 +9,9 @@ "AZ_ServicePrincipalAuthNotSupportedAzureVersion0": "Service principal authentication is not supported in version '{0}' of the Azure module.", "AZ_UnsupportedAuthScheme0": "Unsupported authentication scheme '{0}' for Azure endpoint.", "AZ_AvailableModules": "The list of available {0} modules:", - "AZ_InvalidARMEndpoint": "Specified AzureRM endpoint is invalid." + "AZ_InvalidARMEndpoint": "Specified AzureRM endpoint is invalid.", + "AZ_MsiAccessNotConfiguredProperlyFailure": "Could not fetch access token for Managed Service Principal. Please configure Managed Service Identity (MSI) for virtual machine 'https://aka.ms/azure-msi-docs'. Status code: '{0}', status message: {1}", + "AZ_MsiAccessTokenFetchFailure": "Could not fetch access token for Managed Service Principal. Status code: '{0}', status message: {1}" , + "AZ_MsiFailure": "Could not fetch access token for Managed Service Principal. {0}" } }