Skip to content

Commit

Permalink
support azure pipeline credential (#597)
Browse files Browse the repository at this point in the history
  • Loading branch information
ms-henglu authored Aug 30, 2024
1 parent 55188d1 commit fa9cd6e
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 50 deletions.
47 changes: 47 additions & 0 deletions .azdo/ado-oidc-pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
pool:
name: pool-ubuntu-2004

steps:
- task: GoTool@0
displayName: "Install correct version of Go"
inputs:
version: '1.21.4'
GOPATH: "$(Pipeline.Workspace)/gopath"
GOBIN: "$(GOPATH)/bin"

- task: AzureCLI@2
displayName: Acc Tests with OIDC Token
inputs:
azureSubscription: 'azapi-oidc-test'
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
$env:ARM_TENANT_ID = $env:tenantId
$env:ARM_CLIENT_ID = $env:servicePrincipalId
$env:ARM_OIDC_TOKEN = $env:idToken
$env:ARM_USE_OIDC = 'true'
$env:ARM_USE_CLI = 'false'
$env:TESTARGS = '-run TestAccAuth_oidc'
make acctests
addSpnToEnvironment: true
useGlobalConfig: true
failOnStandardError: true

- task: AzureCLI@2
displayName: Acc Tests with OIDC Azure Pipeline
inputs:
azureSubscription: 'azapi-oidc-test'
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
$env:ARM_TENANT_ID = $env:tenantId
$env:ARM_CLIENT_ID = $env:servicePrincipalId
$env:ARM_OIDC_REQUEST_TOKEN = "$(System.AccessToken)"
$env:ARM_OIDC_AZURE_SERVICE_CONNECTION_ID = "azapi-oidc-test"
$env:ARM_USE_OIDC = 'true'
$env:ARM_USE_CLI = 'false'
$env:TESTARGS = '-run TestAccAuth_oidc'
make acctests
addSpnToEnvironment: true
useGlobalConfig: true
failOnStandardError: true
23 changes: 3 additions & 20 deletions .github/workflows/acc-test.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
name: Acc Tests
name: OIDC Auth Test

on:
workflow_dispatch:
inputs:
ARM_USE_OIDC:
type: boolean
description: ARM_USE_OIDC
required: true
default: false
TESTARGS:
description: TESTARGS
required: false
type: string

permissions:
id-token: write
Expand All @@ -20,8 +10,6 @@ permissions:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: true
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
Expand All @@ -31,12 +19,7 @@ jobs:
- run: bash scripts/gogetcookie.sh
- run: make acctests
env:
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
ARM_TEST_LOCATION: ${{ secrets.ARM_TEST_LOCATION }}
ARM_TEST_LOCATION_ALT: ${{ secrets.ARM_TEST_LOCATION_ALT }}
ARM_TEST_LOCATION_ALT2: ${{ secrets.ARM_TEST_LOCATION_ALT2 }}
ARM_USE_OIDC: ${{ inputs.ARM_USE_OIDC }}
TESTARGS: ${{ inputs.TESTARGS }}
ARM_USE_OIDC: true
TESTARGS: '-run TestAccAuth_oidc'
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ENHANCEMENTS:
- `azapi` resources and data sources: Support `retry` field, which is used to specify the retry configuration.
- `azapi` resources and data sources: Support `headers` and `query_parameters` fields, which are used to specify the headers and query parameters.
- `azapi` resources and data sources: The `response_export_values` field supports JMESPath expressions.
- `azapi` provider: Support `oidc_azure_service_connection_id` field, which is used to specify the Azure Service Connection ID for OIDC authentication with Azure DevOps.

## v1.15.0

Expand Down
88 changes: 87 additions & 1 deletion docs/guides/service_principal_oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Where the body is:
"issuer":"https://token.actions.githubusercontent.com",
"subject":"repo:${REPO_OWNER}/${REPO_NAME}:refs:refs/heads/main",
"description":"${REPO_OWNER} PR",
"audiences":["api://AzureADTokenExchange"],
"audiences":["api://AzureADTokenExchange"]
}
```

Expand Down Expand Up @@ -126,6 +126,92 @@ provider "azapi" {
}
```

When running Terraform in Azure Pipelines, there are two ways to authenticate using OIDC.

The first way is to use the OIDC token.
You can specify the OIDC token using the `oidc_token` or `oidc_token_file_path` provider arguments.
You can also specify the OIDC request token and URL using the environment variables `ARM_OIDC_TOKEN` and `ARM_OIDC_TOKEN_FILE_PATH`.

Here is an example of how to specify the OIDC token using the `oidc_token` provider argument:

```hcl
terraform {
required_providers {
azapi = {
source = "azure/azapi"
}
}
}
provider "azapi" {
oidc_token = "{OIDC Token}"
// or use oidc_token_file_path
// oidc_token_file_path = "{OIDC Token File Path}"
use_oidc = true
}
```

And here is an example of azure-pipelines.yml file:

```yaml
- task: AzureCLI@2
displayName: Acc Tests with OIDC Token
inputs:
azureSubscription: 'azapi-oidc-test' // Azure Service Connection ID
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
$env:ARM_TENANT_ID = $env:tenantId
$env:ARM_CLIENT_ID = $env:servicePrincipalId
$env:ARM_OIDC_TOKEN = $env:idToken
$env:ARM_USE_OIDC = 'true'
terraform plan
addSpnToEnvironment: true
```

The second way is to use the OIDC request token and URL. The provider will detect the `SYSTEM_OIDCREQUESTURI` environment variable set by the Azure Pipelines runtime and use it as the OIDC request URL.
You can specify the OIDC request token using the `oidc_request_token` provider argument or the environment variable `ARM_OIDC_REQUEST_TOKEN`.
And the Azure Service Connection ID must be specified using the `oidc_azure_service_connection_id` provider argument or the environment variable `ARM_OIDC_AZURE_SERVICE_CONNECTION_ID`.

Here is an example of how to specify the OIDC request token and URL using the `oidc_request_token` and `oidc_azure_service_connection_id` provider arguments:

```hcl
terraform {
required_providers {
azapi = {
source = "azure/azapi"
}
}
}
provider "azapi" {
oidc_request_token = "{OIDC Request Token}"
oidc_azure_service_connection_id = "{Azure Service Connection ID}"
use_oidc = true
}
```

And here is an example of azure-pipelines.yml file:

```yaml
- task: AzureCLI@2
displayName: Acc Tests with OIDC Azure Pipeline
inputs:
azureSubscription: 'azapi-oidc-test' // Azure Service Connection ID
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
$env:ARM_TENANT_ID = $env:tenantId
$env:ARM_CLIENT_ID = $env:servicePrincipalId
$env:ARM_OIDC_REQUEST_TOKEN = "$(System.AccessToken)"
$env:ARM_OIDC_AZURE_SERVICE_CONNECTION_ID = "azapi-oidc-test"
$env:ARM_USE_OIDC = 'true'
terraform plan
addSpnToEnvironment: true
```

More information on [the fields supported in the Provider block can be found here](../index.html#argument-reference).

At this point running either `terraform plan` or `terraform apply` should allow Terraform to run using the Service Principal to authenticate.
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ provider "azapi" {
- `disable_terraform_partner_id` (Boolean) Disable sending the Terraform Partner ID if a custom `partner_id` isn't specified, which allows Microsoft to better understand the usage of Terraform. The Partner ID does not give HashiCorp any direct access to usage information. This can also be sourced from the `ARM_DISABLE_TERRAFORM_PARTNER_ID` environment variable. Defaults to `false`.
- `endpoint` (Attributes List) The Azure API Endpoint Configuration. (see [below for nested schema](#nestedatt--endpoint))
- `environment` (String) The Cloud Environment which should be used. Possible values are `public`, `usgovernment` and `china`. Defaults to `public`. This can also be sourced from the `ARM_ENVIRONMENT` Environment Variable.
- `oidc_azure_service_connection_id` (String) The Azure Pipelines Service Connection ID to use for authentication. This can also be sourced from the `ARM_OIDC_AZURE_SERVICE_CONNECTION_ID` environment variable.
- `oidc_request_token` (String) The bearer token for the request to the OIDC provider. This can also be sourced from the `ARM_OIDC_REQUEST_TOKEN` or `ACTIONS_ID_TOKEN_REQUEST_TOKEN` Environment Variables.
- `oidc_request_url` (String) The URL for the OIDC provider from which to request an ID token. This can also be sourced from the `ARM_OIDC_REQUEST_URL` or `ACTIONS_ID_TOKEN_REQUEST_URL` Environment Variables.
- `oidc_token` (String) The ID token when authenticating using OpenID Connect (OIDC). This can also be sourced from the `ARM_OIDC_TOKEN` environment Variable.
Expand Down
93 changes: 65 additions & 28 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,35 @@ type Provider struct {
}

type providerData struct {
SubscriptionID types.String `tfsdk:"subscription_id"`
ClientID types.String `tfsdk:"client_id"`
ClientIDFilePath types.String `tfsdk:"client_id_file_path"`
TenantID types.String `tfsdk:"tenant_id"`
AuxiliaryTenantIDs types.List `tfsdk:"auxiliary_tenant_ids"`
Endpoint types.List `tfsdk:"endpoint"`
Environment types.String `tfsdk:"environment"`
ClientCertificate types.String `tfsdk:"client_certificate"`
ClientCertificatePath types.String `tfsdk:"client_certificate_path"`
ClientCertificatePassword types.String `tfsdk:"client_certificate_password"`
ClientSecret types.String `tfsdk:"client_secret"`
ClientSecretFilePath types.String `tfsdk:"client_secret_file_path"`
SkipProviderRegistration types.Bool `tfsdk:"skip_provider_registration"`
OIDCRequestToken types.String `tfsdk:"oidc_request_token"`
OIDCRequestURL types.String `tfsdk:"oidc_request_url"`
OIDCToken types.String `tfsdk:"oidc_token"`
OIDCTokenFilePath types.String `tfsdk:"oidc_token_file_path"`
UseOIDC types.Bool `tfsdk:"use_oidc"`
UseCLI types.Bool `tfsdk:"use_cli"`
UseMSI types.Bool `tfsdk:"use_msi"`
UseAKSWorkloadIdentity types.Bool `tfsdk:"use_aks_workload_identity"`
PartnerID types.String `tfsdk:"partner_id"`
CustomCorrelationRequestID types.String `tfsdk:"custom_correlation_request_id"`
DisableCorrelationRequestID types.Bool `tfsdk:"disable_correlation_request_id"`
DisableTerraformPartnerID types.Bool `tfsdk:"disable_terraform_partner_id"`
DefaultName types.String `tfsdk:"default_name"`
DefaultLocation types.String `tfsdk:"default_location"`
DefaultTags types.Map `tfsdk:"default_tags"`
SubscriptionID types.String `tfsdk:"subscription_id"`
ClientID types.String `tfsdk:"client_id"`
ClientIDFilePath types.String `tfsdk:"client_id_file_path"`
TenantID types.String `tfsdk:"tenant_id"`
AuxiliaryTenantIDs types.List `tfsdk:"auxiliary_tenant_ids"`
Endpoint types.List `tfsdk:"endpoint"`
Environment types.String `tfsdk:"environment"`
ClientCertificate types.String `tfsdk:"client_certificate"`
ClientCertificatePath types.String `tfsdk:"client_certificate_path"`
ClientCertificatePassword types.String `tfsdk:"client_certificate_password"`
ClientSecret types.String `tfsdk:"client_secret"`
ClientSecretFilePath types.String `tfsdk:"client_secret_file_path"`
SkipProviderRegistration types.Bool `tfsdk:"skip_provider_registration"`
OIDCRequestToken types.String `tfsdk:"oidc_request_token"`
OIDCRequestURL types.String `tfsdk:"oidc_request_url"`
OIDCToken types.String `tfsdk:"oidc_token"`
OIDCTokenFilePath types.String `tfsdk:"oidc_token_file_path"`
OIDCAzureServiceConnectionID types.String `tfsdk:"oidc_azure_service_connection_id"`
UseOIDC types.Bool `tfsdk:"use_oidc"`
UseCLI types.Bool `tfsdk:"use_cli"`
UseMSI types.Bool `tfsdk:"use_msi"`
UseAKSWorkloadIdentity types.Bool `tfsdk:"use_aks_workload_identity"`
PartnerID types.String `tfsdk:"partner_id"`
CustomCorrelationRequestID types.String `tfsdk:"custom_correlation_request_id"`
DisableCorrelationRequestID types.Bool `tfsdk:"disable_correlation_request_id"`
DisableTerraformPartnerID types.Bool `tfsdk:"disable_terraform_partner_id"`
DefaultName types.String `tfsdk:"default_name"`
DefaultLocation types.String `tfsdk:"default_location"`
DefaultTags types.Map `tfsdk:"default_tags"`
}

func (model providerData) GetClientId() (*string, error) {
Expand Down Expand Up @@ -266,6 +267,11 @@ func (p Provider) Schema(ctx context.Context, request provider.SchemaRequest, re
MarkdownDescription: "The path to a file containing an ID token when authenticating using OpenID Connect (OIDC). This can also be sourced from the `ARM_OIDC_TOKEN_FILE_PATH` environment Variable.",
},

"oidc_azure_service_connection_id": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The Azure Pipelines Service Connection ID to use for authentication. This can also be sourced from the `ARM_OIDC_AZURE_SERVICE_CONNECTION_ID` environment variable.",
},

"use_oidc": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Should OIDC be used for Authentication? This can also be sourced from the `ARM_USE_OIDC` Environment Variable. Defaults to `false`.",
Expand Down Expand Up @@ -474,6 +480,12 @@ func (p Provider) Configure(ctx context.Context, request provider.ConfigureReque
}
}

if model.OIDCAzureServiceConnectionID.IsNull() {
if v := os.Getenv("ARM_OIDC_AZURE_SERVICE_CONNECTION_ID"); v != "" {
model.OIDCAzureServiceConnectionID = types.StringValue(v)
}
}

if model.UseOIDC.IsNull() {
if v := os.Getenv("ARM_USE_OIDC"); v != "" {
model.UseOIDC = types.BoolValue(v == "true")
Expand Down Expand Up @@ -721,6 +733,13 @@ func buildChainedTokenCredential(model providerData, options azidentity.DefaultA
} else {
log.Printf("[DEBUG] failed to initialize oidc credential: %v", err)
}

log.Printf("[DEBUG] azure pipelines credential enabled")
if cred, err := buildAzurePipelinesCredential(model, options); err == nil {
creds = append(creds, cred)
} else {
log.Printf("[DEBUG] failed to initialize azure pipelines credential: %v", err)
}
}

if cred, err := buildClientSecretCredential(model, options); err == nil {
Expand Down Expand Up @@ -802,6 +821,10 @@ func buildClientCertificateCredential(model providerData, options azidentity.Def
}
}

if len(certData) == 0 {
return nil, fmt.Errorf("no certificate data provided")
}

var password []byte
if v := model.ClientCertificatePassword.ValueString(); v != "" {
password = []byte(v)
Expand Down Expand Up @@ -864,6 +887,20 @@ func buildAzureCLICredential(options azidentity.DefaultAzureCredentialOptions) (
return azidentity.NewAzureCLICredential(o)
}

func buildAzurePipelinesCredential(model providerData, options azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) {
log.Printf("[DEBUG] building azure pipeline credential")
o := &azidentity.AzurePipelinesCredentialOptions{
ClientOptions: options.ClientOptions,
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
clientId, err := model.GetClientId()
if err != nil {
return nil, err
}
return azidentity.NewAzurePipelinesCredential(options.TenantID, *clientId, model.OIDCAzureServiceConnectionID.ValueString(), model.OIDCRequestToken.ValueString(), o)
}

func decodeCertificate(clientCertificate string) ([]byte, error) {
var pfx []byte
if clientCertificate != "" {
Expand Down
Loading

0 comments on commit fa9cd6e

Please sign in to comment.