Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support azure pipeline credential #597

Merged
merged 1 commit into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading