Skip to content

Commit

Permalink
Support for AzureRm SPN with certificate for Azure PowerShell based t…
Browse files Browse the repository at this point in the history
…asks. (#7719)

* SPN with certificates: Initial commit

* SPN with certificate - updated tasks based up on AzurePS rest APIs

* SPN Certificate - code refactoring

* Updated password

* Resource localization

* Added L0 test for VstsAzureRestHelpers_

* Test file rename

* Added more L0 tests

* Remove unnecessary log messages

* Bug fix

* Incorporated review comments

* Fixing L0 test failure

* Added support for ADFS authentication

* Fixing L0 test failure

* Addressing review comments

* Review comments

* Added third party notice for VstsAzureRestHelpers_
  • Loading branch information
asranja committed Aug 1, 2018
1 parent 3c74c97 commit 8c77eb8
Show file tree
Hide file tree
Showing 39 changed files with 614 additions and 35 deletions.
4 changes: 4 additions & 0 deletions Tasks/AzureFileCopyV1/AzureFileCopy.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ if ($destination -eq "AzureBlob")
$storageContainerSaSToken = New-AzureStorageContainerSASToken -Container $containerName -Context $storageContext -Permission r -ExpiryTime (Get-Date).AddHours($defaultSasTokenTimeOutInHours)
Write-Host "##vso[task.setvariable variable=$outputStorageContainerSASToken;]$storageContainerSasToken"
}

Remove-EndpointSecrets
Write-Verbose "Completed Azure File Copy Task for Azure Blob Destination"

return
}

Expand Down Expand Up @@ -193,6 +196,7 @@ catch
finally
{
Remove-AzureContainer -containerName $containerName -storageContext $storageContext
Remove-EndpointSecrets
Write-Verbose "Completed Azure File Copy Task for Azure VMs Destination" -Verbose
Trace-VstsLeavingInvocation $MyInvocation
}
2 changes: 1 addition & 1 deletion Tasks/AzureFileCopyV1/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"version": {
"Major": 1,
"Minor": 0,
"Patch": 139
"Patch": 140

},
"demands": [
Expand Down
2 changes: 1 addition & 1 deletion Tasks/AzureFileCopyV1/task.loc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"version": {
"Major": 1,
"Minor": 0,
"Patch": 139
"Patch": 140
},
"demands": [
"azureps"
Expand Down
4 changes: 4 additions & 0 deletions Tasks/AzureFileCopyV2/AzureFileCopy.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,10 @@ if ($destination -eq "AzureBlob")
$storageContainerSaSToken = New-AzureStorageContainerSASToken -Container $containerName -Context $storageContext -Permission r -ExpiryTime (Get-Date).AddHours($defaultSasTokenTimeOutInHours)
Write-Host "##vso[task.setvariable variable=$outputStorageContainerSASToken;]$storageContainerSasToken"
}

Remove-EndpointSecrets
Write-Verbose "Completed Azure File Copy Task for Azure Blob Destination"

return
}

Expand Down Expand Up @@ -245,6 +248,7 @@ catch
finally
{
Remove-AzureContainer -containerName $containerName -storageContext $storageContext
Remove-EndpointSecrets
Write-Verbose "Completed Azure File Copy Task for Azure VMs Destination" -Verbose
Trace-VstsLeavingInvocation $MyInvocation
}
2 changes: 1 addition & 1 deletion Tasks/AzureFileCopyV2/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"version": {
"Major": 2,
"Minor": 0,
"Patch": 7
"Patch": 8

},
"preview": true,
Expand Down
2 changes: 1 addition & 1 deletion Tasks/AzureFileCopyV2/task.loc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"version": {
"Major": 2,
"Minor": 0,
"Patch": 7
"Patch": 8
},
"preview": true,
"demands": [
Expand Down
5 changes: 4 additions & 1 deletion Tasks/AzurePowerShellV3/AzurePowerShell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,7 @@ finally {
if ($__vstsAzPSInlineScriptPath -and (Test-Path -LiteralPath $__vstsAzPSInlineScriptPath) ) {
Remove-Item -LiteralPath $__vstsAzPSInlineScriptPath -ErrorAction 'SilentlyContinue'
}
}

Import-Module $PSScriptRoot\ps_modules\VstsAzureHelpers_
Remove-EndpointSecrets
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference
Register-Mock Get-VstsInput { $false } -- -Name FailOnStandardError
Register-Mock Update-PSModulePathForHostedAgent
Register-Mock Initialize-Azure
Register-Mock Remove-EndpointSecrets

# Act.
$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Register-Mock Get-VstsInput { "stop" } -- -Name errorActionPreference
Register-Mock Get-VstsInput { $false } -- -Name FailOnStandardError
Register-Mock Update-PSModulePathForHostedAgent
Register-Mock Initialize-Azure
Register-Mock Remove-EndpointSecrets

# Act.
$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 )
Expand Down
1 change: 1 addition & 0 deletions Tasks/AzurePowerShellV3/Tests/DoesNotUnravelOutput.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference
Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError
Register-Mock Update-PSModulePathForHostedAgent
Register-Mock Initialize-Azure
Register-Mock Remove-EndpointSecrets

# Act.
$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Register-Mock Get-VstsInput { "silentlyContinue" } -- -Name errorActionPreferenc
Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError
Register-Mock Update-PSModulePathForHostedAgent
Register-Mock Initialize-Azure
Register-Mock Remove-EndpointSecrets

# Act.
$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 )
Expand Down
1 change: 1 addition & 0 deletions Tasks/AzurePowerShellV3/Tests/PerformsBasicFlow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError
Register-Mock Update-PSModulePathForHostedAgent
Register-Mock Initialize-Azure
Register-Mock Get-VstsEndpoint { @{auth = @{ scheme = "ServicePrincipal" }} }
Register-Mock Remove-EndpointSecrets

# Act.
$actual = & $PSScriptRoot\..\AzurePowerShell.ps1
Expand Down
1 change: 1 addition & 0 deletions Tasks/AzurePowerShellV3/Tests/RedirectsErrors.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference
Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError
Register-Mock Update-PSModulePathForHostedAgent
Register-Mock Initialize-Azure
Register-Mock Remove-EndpointSecrets

# Act.
$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs
Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference
Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError
Register-Mock Update-PSModulePathForHostedAgent
Register-Mock Remove-EndpointSecrets

# Arrange the mock task SDK module.
New-Module -Name VstsTaskSdk -ScriptBlock {
Expand Down
1 change: 1 addition & 0 deletions Tasks/AzurePowerShellV3/Tests/ValidateInlineScriptFlow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference
Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError
Register-Mock Update-PSModulePathForHostedAgent
Register-Mock Initialize-Azure
Register-Mock Remove-EndpointSecrets

# Act.
$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 )
Expand Down
146 changes: 130 additions & 16 deletions Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
function Add-Certificate {
[CmdletBinding()]
param([Parameter(Mandatory=$true)]$Endpoint)
param(
[Parameter(Mandatory=$true)] $Endpoint,
[Switch] $ServicePrincipal
)

# Add the certificate to the cert store.
$bytes = [System.Convert]::FromBase64String($Endpoint.Auth.Parameters.Certificate)
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$certificate.Import($bytes)

if ($ServicePrincipal) {
$pemFileContent = $Endpoint.Auth.Parameters.ServicePrincipalCertificate
$pfxFilePath, $pfxFilePassword = ConvertTo-Pfx -pemFileContent $pemFileContent

$certificate.Import($pfxFilePath, $pfxFilePassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet)
}
else {
$bytes = [System.Convert]::FromBase64String($Endpoint.Auth.Parameters.Certificate)
$certificate.Import($bytes)
}

$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
([System.Security.Cryptography.X509Certificates.StoreName]::My),
([System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser))
([System.Security.Cryptography.X509Certificates.StoreName]::My),
([System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser))
$store.Open(([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite))
$store.Add($certificate)
$store.Close()

#store the thumbprint in a global variable which will be used to remove the certificate later on
$script:Endpoint_Authentication_Certificate = $certificate.Thumbprint
Write-Verbose "Added certificate to the certificate store."
return $certificate
}

Expand Down Expand Up @@ -125,10 +142,18 @@ function Initialize-AzureSubscription {
if ($script:azureRMProfileModule) {
Set-CurrentAzureRMSubscription -SubscriptionId $Endpoint.Data.SubscriptionId
}
} elseif ($Endpoint.Auth.Scheme -eq 'ServicePrincipal') {
$psCredential = New-Object System.Management.Automation.PSCredential(
$Endpoint.Auth.Parameters.ServicePrincipalId,
(ConvertTo-SecureString $Endpoint.Auth.Parameters.ServicePrincipalKey -AsPlainText -Force))
}
elseif ($Endpoint.Auth.Scheme -eq 'ServicePrincipal') {

if ($Endpoint.Auth.Parameters.AuthenticationType -eq 'SPNCertificate') {
$servicePrincipalCertificate = Add-Certificate -Endpoint $Endpoint -ServicePrincipal
}
else {
$psCredential = New-Object System.Management.Automation.PSCredential(
$Endpoint.Auth.Parameters.ServicePrincipalId,
(ConvertTo-SecureString $Endpoint.Auth.Parameters.ServicePrincipalKey -AsPlainText -Force))
}

if ($script:azureModule -and $script:azureModule.Version -lt ([version]'0.9.9')) {
# Service principals arent supported from 0.9.9 and greater in the Azure module.
try {
Expand All @@ -147,6 +172,7 @@ function Initialize-AzureSubscription {
throw (Get-VstsLocString -Key "AZ_ServicePrincipalAuthNotSupportedAzureVersion0" -ArgumentList $script:azureModule.Version)
} else {
# Else, this is AzureRM.

try {
if(Get-Command -Name "Clear-AzureRmContext" -ErrorAction "SilentlyContinue"){
Write-Host "##[command]Clear-AzureRmContext -Scope Process"
Expand All @@ -156,19 +182,39 @@ function Initialize-AzureSubscription {
}
if (Get-Command -Name "Add-AzureRmAccount" -ErrorAction "SilentlyContinue") {
if (CmdletHasMember -cmdlet "Add-AzureRMAccount" -memberName "EnvironmentName") {
Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -EnvironmentName $environmentName"
$null = Add-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -EnvironmentName $environmentName

if ($Endpoint.Auth.Parameters.AuthenticationType -eq "SPNCertificate") {
Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -CertificateThumbprint ****** -ApplicationId $($Endpoint.Auth.Parameters.ServicePrincipalId) -EnvironmentName $environmentName"
$null = Add-AzureRmAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -CertificateThumbprint $servicePrincipalCertificate.Thumbprint -ApplicationId $Endpoint.Auth.Parameters.ServicePrincipalId -EnvironmentName $environmentName
}
else {
Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -EnvironmentName $environmentName"
$null = Add-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -EnvironmentName $environmentName
}
}
else {
Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -Environment $environmentName"
$null = Add-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -Environment $environmentName
if ($Endpoint.Auth.Parameters.AuthenticationType -eq "SPNCertificate") {
Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -CertificateThumbprint ****** -ApplicationId $($Endpoint.Auth.Parameters.ServicePrincipalId) -Environment $environmentName"
$null = Add-AzureRmAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -CertificateThumbprint $servicePrincipalCertificate.Thumbprint -ApplicationId $Endpoint.Auth.Parameters.ServicePrincipalId -Environment $environmentName
}
else {
Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -Environment $environmentName"
$null = Add-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -Environment $environmentName
}
}
}
else {
Write-Host "##[command]Connect-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -Environment $environmentName"
$null = Connect-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -Environment $environmentName
if ($Endpoint.Auth.Parameters.AuthenticationType -eq "SPNCertificate") {
Write-Host "##[command]Connect-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -CertificateThumbprint ****** -ApplicationId $($Endpoint.Auth.Parameters.ServicePrincipalId) -Environment $environmentName"
$null = Connect-AzureRmAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -CertificateThumbprint $servicePrincipalCertificate.Thumbprint -ApplicationId $Endpoint.Auth.Parameters.ServicePrincipalId -Environment $environmentName
}
else {
Write-Host "##[command]Connect-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -Environment $environmentName"
$null = Connect-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -Environment $environmentName
}
}
} catch {
}
catch {
# Provide an additional, custom, credentials-related error message.
Write-VstsTaskError -Message $_.Exception.Message
Assert-TlsError -exception $_.Exception
Expand Down Expand Up @@ -500,3 +546,71 @@ function Get-ProxyUri

return $proxyUri
}

function ConvertTo-Pfx {
param(
[String][Parameter(Mandatory = $true)] $pemFileContent
)

if ($ENV:Agent_TempDirectory) {
$pemFilePath = "$ENV:Agent_TempDirectory\clientcertificate.pem"
$pfxFilePath = "$ENV:Agent_TempDirectory\clientcertificate.pfx"
$pfxPasswordFilePath = "$ENV:Agent_TempDirectory\clientcertificatepassword.txt"
}
else {
$pemFilePath = "$ENV:System_DefaultWorkingDirectory\clientcertificate.pem"
$pfxFilePath = "$ENV:System_DefaultWorkingDirectory\clientcertificate.pfx"
$pfxPasswordFilePath = "$ENV:System_DefaultWorkingDirectory\clientcertificatepassword.txt"
}

# save the PEM certificate to a PEM file
Set-Content -Path $pemFilePath -Value $pemFileContent

# use openssl to convert the PEM file to a PFX file
$pfxFilePassword = [System.Guid]::NewGuid().ToString()
Set-Content -Path $pfxPasswordFilePath -Value $pfxFilePassword -NoNewline

$openSSLExePath = "$PSScriptRoot\openssl\openssl.exe"
$openSSLArgs = "pkcs12 -export -in $pemFilePath -out $pfxFilePath -password file:`"$pfxPasswordFilePath`""

Invoke-VstsTool -FileName $openSSLExePath -Arguments $openSSLArgs -RequireExitCodeZero

return $pfxFilePath, $pfxFilePassword
}

function Remove-EndpointSecrets {
# remove any certificate files
if (Test-Path -Path "$ENV:System_DefaultWorkingDirectory\clientcertificate.pem") {
Write-Verbose "Removing file $ENV:System_DefaultWorkingDirectory\clientcertificate.pem"
Remove-Item -Path "$ENV:System_DefaultWorkingDirectory\clientcertificate.pem"
}

if (Test-Path -Path "$ENV:System_DefaultWorkingDirectory\clientcertificate.pfx") {
Write-Verbose "Removing file $ENV:System_DefaultWorkingDirectory\clientcertificate.pfx"
Remove-Item -Path "$ENV:System_DefaultWorkingDirectory\clientcertificate.pfx"
}

if (Test-Path -Path "$ENV:System_DefaultWorkingDirectory\clientcertificatepassword.txt") {
Write-Verbose "Removing file $ENV:System_DefaultWorkingDirectory\clientcertificatepassword.txt"
Remove-Item -Path "$ENV:System_DefaultWorkingDirectory\clientcertificatepassword.txt"
}

if ($script:Endpoint_Authentication_Certificate) {
# remove the certificate from certificate store
$certificateStore = New-Object System.Security.Cryptography.X509Certificates.X509Store(
([System.Security.Cryptography.X509Certificates.StoreName]::My),
([System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser))

$certificateStore.Open(([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite))

$certificates = $certificateStore.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $script:Endpoint_Authentication_Certificate, $false)

foreach ($certificate in $certificates) {
$certificateStore.Remove($certificate)
}

$certificateStore.Close()

Write-Verbose "Removed certificate from certificate store."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[CmdletBinding()]
param()

# Arrange.
. $PSScriptRoot\..\..\..\..\Tests\lib\Initialize-Test.ps1

$endpoint = @{
Url = "https://management.azure.com"
Auth = @{
Parameters = @{
TenantId = 'Tenant Id'
ServicePrincipalId = 'Service Principal Id 1'
AuthenticationType = 'SPNCertificate'
ServicePrincipalCertificate = 'Service Principal Certificate'
}
Scheme = 'ServicePrincipal'
}
Data = @{
SubscriptionId = 'Subscription ID'
SubscriptionName = 'Subscription name'
Environment = "AzureCloud"
ActiveDirectoryServiceEndpointResourceId = "https://management.azure.com"
}
}

Register-Mock Add-Tls12InSession { }
Register-Mock Add-AzureRMAccount { 'Add-AzureRmAccount' }
Register-Mock Set-CurrentAzureRMSubscription { 'Set-CurrentAzureRMSubscription' }
Register-Mock Set-UserAgent { }
Register-Mock Add-Certificate { }

$module = Microsoft.PowerShell.Core\Import-Module $PSScriptRoot\.. -PassThru
$result = & $module Initialize-AzureSubscription -Endpoint $endpoint

Assert-WasCalled Add-AzureRMAccount
3 changes: 3 additions & 0 deletions Tasks/Common/VstsAzureHelpers_/Tests/L0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ describe('Common-VstsAzureHelpers_ Suite', function () {
it('(Initialize-AzureSubscription) passes values when cert auth and environment', (done) => {
psr.run(path.join(__dirname, 'Initialize-AzureSubscription.PassesValuesWhenCertAuthAndEnvironment.ps1'), done);
})
it('(Initialize-AzureSubscription) passes values when cert auth with service principal scheme', (done) => {
psr.run(path.join(__dirname, 'Initialize-AzureSubscription.PassesValuesWhenSPNCertAuth.ps1'), done);
})
it('(Initialize-AzureSubscription) throws when SP auth and classic 0.9.9', (done) => {
psr.run(path.join(__dirname, 'Initialize-AzureSubscription.ThrowsWhenSPAuthAndClassic099.ps1'), done);
})
Expand Down
3 changes: 2 additions & 1 deletion Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ function Initialize-Azure {

# Export only the public function.
Export-ModuleMember -Function Initialize-Azure
Export-ModuleMember -Function CmdletHasMember
Export-ModuleMember -Function CmdletHasMember
Export-ModuleMember -Function Remove-EndpointSecrets
Loading

0 comments on commit 8c77eb8

Please sign in to comment.