mirror of
https://github.com/opentofu/terraform-provider-vault.git
synced 2026-01-11 19:46:35 +00:00
added vault_azure_access_credentials ephemeral resource (#2654)
* added azure_access_credentials ephemeral resource * minor refactor * added doc * added changelog entry * minor fix * fixed test * added retry with backoff and removed changelog * rename var and use consts in schema * checking status code as well for retry * minor refactor * reduced defaultNumSequentialSuccesses to 4 * fix doc
This commit is contained in:
parent
148e86b088
commit
1f4beca5bd
5 changed files with 725 additions and 0 deletions
|
|
@ -98,6 +98,10 @@ const (
|
|||
FieldClientID = "client_id"
|
||||
FieldClientSecret = "client_secret"
|
||||
FieldEnvironment = "environment"
|
||||
FieldValidateCreds = "validate_creds"
|
||||
FieldNumSequentialSuccesses = "num_sequential_successes"
|
||||
FieldNumSecondsBetweenTests = "num_seconds_between_tests"
|
||||
FieldMaxCredValidationSeconds = "max_cred_validation_seconds"
|
||||
FieldVaultName = "vault_name"
|
||||
FieldKeyName = "key_name"
|
||||
FieldResource = "resource"
|
||||
|
|
@ -218,6 +222,7 @@ const (
|
|||
FieldRenewMinLease = "renew_min_lease"
|
||||
FieldRenewIncrement = "renew_increment"
|
||||
FieldLeaseStarted = "lease_started"
|
||||
FieldLeaseStartTime = "lease_start_time"
|
||||
FieldClientToken = "client_token"
|
||||
FieldWrappedToken = "wrapped_token"
|
||||
FieldOrphan = "orphan"
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ func (p *fwprovider) EphemeralResources(_ context.Context) []func() ephemeral.Ep
|
|||
ephemeralsecrets.NewKVV2EphemeralSecretResource,
|
||||
ephemeralsecrets.NewDBEphemeralSecretResource,
|
||||
ephemeralsecrets.NewAzureStaticCredsEphemeralSecretResource,
|
||||
ephemeralsecrets.NewAzureAccessCredentialsEphemeralResource,
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
499
internal/vault/secrets/ephemeral/azure_access_credentials.go
Normal file
499
internal/vault/secrets/ephemeral/azure_access_credentials.go
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ephemeralsecrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
|
||||
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/consts"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/framework/base"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/framework/client"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/framework/errutil"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/framework/model"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/sdk/helper/pointerutil"
|
||||
)
|
||||
|
||||
// https://learn.microsoft.com/en-us/graph/sdks/national-clouds
|
||||
const (
|
||||
azurePublicCloudEnvName = "AZUREPUBLICCLOUD"
|
||||
azureChinaCloudEnvName = "AZURECHINACLOUD"
|
||||
azureUSGovCloudEnvName = "AZUREUSGOVERNMENTCLOUD"
|
||||
|
||||
// Default values for credential validation
|
||||
defaultNumSecondsBetweenTests = 1
|
||||
defaultMaxCredValidationSeconds = 300
|
||||
defaultNumSequentialSuccesses = 4
|
||||
)
|
||||
|
||||
var azureCloudConfigMap = map[string]cloud.Configuration{
|
||||
azureChinaCloudEnvName: cloud.AzureChina,
|
||||
azurePublicCloudEnvName: cloud.AzurePublic,
|
||||
azureUSGovCloudEnvName: cloud.AzureGovernment,
|
||||
}
|
||||
|
||||
// Ensure the implementation satisfies the ephemeral.EphemeralResource interface
|
||||
var _ ephemeral.EphemeralResource = &AzureAccessCredentialsEphemeralResource{}
|
||||
var _ ephemeral.EphemeralResourceWithClose = &AzureAccessCredentialsEphemeralResource{}
|
||||
|
||||
// NewAzureAccessCredentialsEphemeralResource returns the implementation for this resource to be
|
||||
// imported by the Terraform Plugin Framework provider
|
||||
var NewAzureAccessCredentialsEphemeralResource = func() ephemeral.EphemeralResource {
|
||||
return &AzureAccessCredentialsEphemeralResource{}
|
||||
}
|
||||
|
||||
// AzureAccessCredentialsEphemeralResource implements the methods that define this resource
|
||||
type AzureAccessCredentialsEphemeralResource struct {
|
||||
base.EphemeralResourceWithConfigure
|
||||
}
|
||||
|
||||
// AzureAccessCredentialsPrivateData stores data needed for cleanup in Close
|
||||
type AzureAccessCredentialsPrivateData struct {
|
||||
LeaseID string `json:"lease_id"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// AzureAccessCredentialsAPIModel describes the Vault API data model.
|
||||
type AzureAccessCredentialsAPIModel struct {
|
||||
ClientID string `json:"client_id" mapstructure:"client_id"`
|
||||
ClientSecret string `json:"client_secret" mapstructure:"client_secret"`
|
||||
}
|
||||
|
||||
// AzureAccessCredentialsModel describes the Terraform resource data model to match the
|
||||
// resource schema.
|
||||
type AzureAccessCredentialsModel struct {
|
||||
// common fields to all ephemeral resources
|
||||
base.BaseModelEphemeral
|
||||
|
||||
// fields specific to this resource
|
||||
Backend types.String `tfsdk:"backend"`
|
||||
Role types.String `tfsdk:"role"`
|
||||
ValidateCreds types.Bool `tfsdk:"validate_creds"`
|
||||
NumSequentialSuccesses types.Int64 `tfsdk:"num_sequential_successes"`
|
||||
NumSecondsBetweenTests types.Int64 `tfsdk:"num_seconds_between_tests"`
|
||||
MaxCredValidationSeconds types.Int64 `tfsdk:"max_cred_validation_seconds"`
|
||||
SubscriptionID types.String `tfsdk:"subscription_id"`
|
||||
TenantID types.String `tfsdk:"tenant_id"`
|
||||
Environment types.String `tfsdk:"environment"`
|
||||
|
||||
// computed fields
|
||||
ClientID types.String `tfsdk:"client_id"`
|
||||
ClientSecret types.String `tfsdk:"client_secret"`
|
||||
LeaseID types.String `tfsdk:"lease_id"`
|
||||
LeaseDuration types.Int64 `tfsdk:"lease_duration"`
|
||||
LeaseStartTime types.String `tfsdk:"lease_start_time"`
|
||||
LeaseRenewable types.Bool `tfsdk:"lease_renewable"`
|
||||
}
|
||||
|
||||
// Schema defines this resource's schema which is the data that is available in
|
||||
// the resource's configuration, plan, and state
|
||||
func (r *AzureAccessCredentialsEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
consts.FieldBackend: schema.StringAttribute{
|
||||
MarkdownDescription: "Azure Secret Backend to read credentials from.",
|
||||
Required: true,
|
||||
},
|
||||
consts.FieldRole: schema.StringAttribute{
|
||||
MarkdownDescription: "Azure Secret Role to read credentials from.",
|
||||
Required: true,
|
||||
},
|
||||
consts.FieldValidateCreds: schema.BoolAttribute{
|
||||
MarkdownDescription: "Whether generated credentials should be validated before being returned.",
|
||||
Optional: true,
|
||||
},
|
||||
consts.FieldNumSequentialSuccesses: schema.Int64Attribute{
|
||||
MarkdownDescription: "If 'validate_creds' is true, the number of sequential successes required to validate generated credentials.",
|
||||
Optional: true,
|
||||
},
|
||||
consts.FieldNumSecondsBetweenTests: schema.Int64Attribute{
|
||||
MarkdownDescription: "If 'validate_creds' is true, the number of seconds to wait between each test of generated credentials.",
|
||||
Optional: true,
|
||||
},
|
||||
consts.FieldMaxCredValidationSeconds: schema.Int64Attribute{
|
||||
MarkdownDescription: "If 'validate_creds' is true, the number of seconds after which to give up validating credentials.",
|
||||
Optional: true,
|
||||
},
|
||||
consts.FieldSubscriptionID: schema.StringAttribute{
|
||||
MarkdownDescription: "The subscription ID to use during credential validation. Defaults to the subscription ID configured in the Vault backend.",
|
||||
Optional: true,
|
||||
},
|
||||
consts.FieldTenantID: schema.StringAttribute{
|
||||
MarkdownDescription: "The tenant ID to use during credential validation. Defaults to the tenant ID configured in the Vault backend.",
|
||||
Optional: true,
|
||||
},
|
||||
consts.FieldEnvironment: schema.StringAttribute{
|
||||
MarkdownDescription: "The Azure environment to use during credential validation. Defaults to the Azure Public Cloud. Some possible values: AzurePublicCloud, AzureUSGovernmentCloud.",
|
||||
Optional: true,
|
||||
},
|
||||
consts.FieldClientID: schema.StringAttribute{
|
||||
MarkdownDescription: "The client id for credentials to query the Azure APIs.",
|
||||
Computed: true,
|
||||
},
|
||||
consts.FieldClientSecret: schema.StringAttribute{
|
||||
MarkdownDescription: "The client secret for credentials to query the Azure APIs.",
|
||||
Computed: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
consts.FieldLeaseID: schema.StringAttribute{
|
||||
MarkdownDescription: "Lease identifier assigned by vault.",
|
||||
Computed: true,
|
||||
},
|
||||
consts.FieldLeaseDuration: schema.Int64Attribute{
|
||||
MarkdownDescription: "Lease duration in seconds relative to the time in lease_start_time.",
|
||||
Computed: true,
|
||||
},
|
||||
consts.FieldLeaseStartTime: schema.StringAttribute{
|
||||
MarkdownDescription: "Time at which the lease was read, using the clock of the system where Terraform was running.",
|
||||
Computed: true,
|
||||
},
|
||||
consts.FieldLeaseRenewable: schema.BoolAttribute{
|
||||
MarkdownDescription: "True if the duration of this lease can be extended through renewal.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
MarkdownDescription: "Provides an ephemeral resource to read Azure access credentials from Vault.",
|
||||
}
|
||||
|
||||
base.MustAddBaseEphemeralSchema(&resp.Schema)
|
||||
}
|
||||
|
||||
// Metadata sets the full name for this resource
|
||||
func (r *AzureAccessCredentialsEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_azure_access_credentials"
|
||||
}
|
||||
|
||||
func (r *AzureAccessCredentialsEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
|
||||
var data AzureAccessCredentialsModel
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
c, err := client.GetClient(ctx, r.Meta(), data.Namespace.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
backend := data.Backend.ValueString()
|
||||
role := data.Role.ValueString()
|
||||
credsPath := backend + "/creds/" + role
|
||||
|
||||
// Retry logic for reading credentials from Vault with exponential backoff
|
||||
// Azure can return rate limit errors generating credentials when multiple
|
||||
// requests are made during plan,apply,refresh in quick succession
|
||||
var secret *api.Secret
|
||||
bo := backoff.NewExponentialBackOff()
|
||||
bo.InitialInterval = 2 * time.Second
|
||||
bo.MaxInterval = 30 * time.Second
|
||||
bo.MaxElapsedTime = 5 * time.Minute
|
||||
|
||||
err = backoff.RetryNotify(
|
||||
func() error {
|
||||
var readErr error
|
||||
secret, readErr = c.Logical().ReadWithContext(ctx, credsPath)
|
||||
if readErr != nil {
|
||||
if respErr, ok := readErr.(*api.ResponseError); ok {
|
||||
if respErr.StatusCode == 500 && strings.Contains(respErr.Error(), "concurrent requests being made") {
|
||||
// Retryable error
|
||||
return respErr
|
||||
}
|
||||
// Non-retryable error
|
||||
return backoff.Permanent(respErr)
|
||||
}
|
||||
// Non-retryable error
|
||||
return backoff.Permanent(readErr)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
bo,
|
||||
func(err error, duration time.Duration) {
|
||||
log.Printf("[WARN] Azure rate limit error reading credentials, retrying in %s: %s", duration, err)
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
"Error reading from Vault",
|
||||
fmt.Sprintf("Error reading Azure credentials from path %q: %s", credsPath, err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if secret == nil {
|
||||
resp.Diagnostics.AddError(
|
||||
"No role found",
|
||||
fmt.Sprintf("No role found at path %q", credsPath),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Read %q from Vault", credsPath)
|
||||
|
||||
var apiResp AzureAccessCredentialsAPIModel
|
||||
if err := model.ToAPIModel(secret.Data, &apiResp); err != nil {
|
||||
resp.Diagnostics.AddError("Unable to translate Vault response data", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set the basic credential fields
|
||||
data.ClientID = types.StringValue(apiResp.ClientID)
|
||||
data.ClientSecret = types.StringValue(apiResp.ClientSecret)
|
||||
data.LeaseID = types.StringValue(secret.LeaseID)
|
||||
data.LeaseDuration = types.Int64Value(int64(secret.LeaseDuration))
|
||||
data.LeaseStartTime = types.StringValue(time.Now().Format(time.RFC3339))
|
||||
data.LeaseRenewable = types.BoolValue(secret.Renewable)
|
||||
|
||||
// Store lease information in private data for cleanup in Close
|
||||
if secret.LeaseID != "" {
|
||||
privateData, err := json.Marshal(AzureAccessCredentialsPrivateData{
|
||||
LeaseID: secret.LeaseID,
|
||||
Namespace: data.Namespace.ValueString(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to marshal private data: %s", err)
|
||||
} else {
|
||||
resp.Private.SetKey(ctx, "lease_data", privateData)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not supposed to validate creds, we're done
|
||||
if !data.ValidateCreds.ValueBool() {
|
||||
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
|
||||
return
|
||||
}
|
||||
|
||||
// Credential validation logic
|
||||
configPath := backend + "/config"
|
||||
var config *api.Secret
|
||||
getConfigData := func() (map[string]interface{}, error) {
|
||||
if config == nil {
|
||||
configSecret, err := c.Logical().ReadWithContext(ctx, configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from Vault: %w", err)
|
||||
}
|
||||
if configSecret == nil {
|
||||
return nil, fmt.Errorf("config not found at %q", configPath)
|
||||
}
|
||||
config = configSecret
|
||||
}
|
||||
return config.Data, nil
|
||||
}
|
||||
|
||||
subscriptionID := data.SubscriptionID.ValueString()
|
||||
if subscriptionID == "" {
|
||||
configData, err := getConfigData()
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading backend config", err.Error())
|
||||
return
|
||||
}
|
||||
if v, ok := configData["subscription_id"]; ok {
|
||||
subscriptionID = v.(string)
|
||||
}
|
||||
}
|
||||
|
||||
if subscriptionID == "" {
|
||||
resp.Diagnostics.AddError(
|
||||
"Missing subscription_id",
|
||||
"subscription_id cannot be empty when validate_creds is true",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := data.TenantID.ValueString()
|
||||
if tenantID == "" {
|
||||
configData, err := getConfigData()
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading backend config", err.Error())
|
||||
return
|
||||
}
|
||||
if v, ok := configData["tenant_id"]; ok {
|
||||
tenantID = v.(string)
|
||||
}
|
||||
}
|
||||
|
||||
if tenantID == "" {
|
||||
resp.Diagnostics.AddError(
|
||||
"Missing tenant_id",
|
||||
"tenant_id cannot be empty when validate_creds is true",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
environment := data.Environment.ValueString()
|
||||
if environment == "" {
|
||||
configData, err := getConfigData()
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading backend config", err.Error())
|
||||
return
|
||||
}
|
||||
if v, ok := configData["environment"]; ok && v.(string) != "" {
|
||||
environment = v.(string)
|
||||
}
|
||||
}
|
||||
|
||||
cloudConfig, err := getAzureCloudConfigFromName(environment)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Invalid Azure environment", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Default validation parameters
|
||||
delay := time.Duration(defaultNumSecondsBetweenTests) * time.Second
|
||||
if !data.NumSecondsBetweenTests.IsNull() {
|
||||
delay = time.Duration(data.NumSecondsBetweenTests.ValueInt64()) * time.Second
|
||||
}
|
||||
|
||||
maxValidationSeconds := int64(defaultMaxCredValidationSeconds)
|
||||
if !data.MaxCredValidationSeconds.IsNull() {
|
||||
maxValidationSeconds = data.MaxCredValidationSeconds.ValueInt64()
|
||||
}
|
||||
|
||||
wantSuccessCount := int64(defaultNumSequentialSuccesses)
|
||||
if !data.NumSequentialSuccesses.IsNull() {
|
||||
wantSuccessCount = data.NumSequentialSuccesses.ValueInt64()
|
||||
}
|
||||
|
||||
endTime := time.Now().Add(time.Duration(maxValidationSeconds) * time.Second)
|
||||
var successCount int64
|
||||
|
||||
// Credential validation retry loop
|
||||
for {
|
||||
creds, err := azidentity.NewClientSecretCredential(
|
||||
tenantID, apiResp.ClientID, apiResp.ClientSecret, &azidentity.ClientSecretCredentialOptions{})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
"Failed to create credentials",
|
||||
fmt.Sprintf("Failed to create new credential during validation: %s", err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
providerClient, err := armresources.NewProvidersClient(subscriptionID, creds, &arm.ClientOptions{
|
||||
ClientOptions: policy.ClientOptions{
|
||||
Cloud: cloudConfig,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
"Failed to create Azure client",
|
||||
fmt.Sprintf("Failed to create new provider client during validation: %s", err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
pager := providerClient.NewListPager(&armresources.ProvidersClientListOptions{
|
||||
Expand: pointerutil.StringPtr("metadata"),
|
||||
})
|
||||
|
||||
hasError := false
|
||||
for pager.More() {
|
||||
var rawResponse *http.Response
|
||||
ctxWithResp := runtime.WithCaptureResponse(ctx, &rawResponse)
|
||||
|
||||
_, err := pager.NextPage(ctxWithResp)
|
||||
if err != nil {
|
||||
hasError = true
|
||||
log.Printf("[WARN] Provider Client List request failed err=%s", err)
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Provider Client List response %+v", rawResponse)
|
||||
}
|
||||
|
||||
if !hasError {
|
||||
successCount++
|
||||
log.Printf("[DEBUG] Credential validation succeeded on try %d/%d", successCount, wantSuccessCount)
|
||||
if successCount >= wantSuccessCount {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
log.Printf("[WARN] Credential validation failed, retrying in %s", delay)
|
||||
successCount = 0
|
||||
}
|
||||
|
||||
if time.Now().After(endTime) {
|
||||
resp.Diagnostics.AddError(
|
||||
"Credential validation timeout",
|
||||
fmt.Sprintf("validation failed after max_cred_validation_seconds of %d, giving up; now=%s, endTime=%s",
|
||||
maxValidationSeconds, time.Now().String(), endTime.String()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
// Close revokes the credentials lease when the ephemeral resource is no longer needed
|
||||
func (r *AzureAccessCredentialsEphemeralResource) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {
|
||||
privateBytes, diags := req.Private.GetKey(ctx, "lease_data")
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// If no private data, nothing to clean up
|
||||
if len(privateBytes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var privateData AzureAccessCredentialsPrivateData
|
||||
if err := json.Unmarshal(privateBytes, &privateData); err != nil {
|
||||
log.Printf("[WARN] Failed to unmarshal private data: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if privateData.LeaseID == "" {
|
||||
// No lease to revoke
|
||||
return
|
||||
}
|
||||
|
||||
c, err := client.GetClient(ctx, r.Meta(), privateData.Namespace)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error configuring Vault client for revoke", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to revoke the lease
|
||||
err = c.Sys().Revoke(privateData.LeaseID)
|
||||
if err != nil {
|
||||
// Log but do not fail resource close
|
||||
log.Printf("[WARN] Failed to revoke lease %q: %s", privateData.LeaseID, err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully revoked lease %q", privateData.LeaseID)
|
||||
}
|
||||
}
|
||||
|
||||
func getAzureCloudConfigFromName(name string) (cloud.Configuration, error) {
|
||||
if name == "" {
|
||||
return cloud.AzurePublic, nil
|
||||
}
|
||||
if c, ok := azureCloudConfigMap[strings.ToUpper(name)]; !ok {
|
||||
return c, fmt.Errorf("unsupported Azure cloud name %q", name)
|
||||
} else {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ephemeralsecrets_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
|
||||
"github.com/hashicorp/terraform-plugin-testing/echoprovider"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
|
||||
"github.com/hashicorp/terraform-plugin-testing/statecheck"
|
||||
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
|
||||
"github.com/hashicorp/terraform-provider-vault/acctestutil"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/providertest"
|
||||
"github.com/hashicorp/terraform-provider-vault/testutil"
|
||||
)
|
||||
|
||||
// TestAccAzureAccessCredentialsEphemeralResource_basic tests the creation of dynamic
|
||||
// Azure service principal credentials using ephemeral resource.
|
||||
func TestAccAzureAccessCredentialsEphemeralResource_basic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
validateCreds bool
|
||||
}{
|
||||
{
|
||||
name: "without validation",
|
||||
validateCreds: false,
|
||||
},
|
||||
{
|
||||
name: "with validation",
|
||||
validateCreds: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.validateCreds && testing.Short() {
|
||||
t.Skip("skipping test with credential validation overhead in short mode")
|
||||
}
|
||||
|
||||
conf := testutil.GetTestAzureConfExistingSP(t)
|
||||
backend := acctest.RandomWithPrefix("tf-test-azure")
|
||||
role := acctest.RandomWithPrefix("tf-role")
|
||||
nonEmpty := regexp.MustCompile(`^.+$`)
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() {
|
||||
acctestutil.TestEntPreCheck(t)
|
||||
},
|
||||
ProtoV5ProviderFactories: providertest.ProtoV5ProviderFactories,
|
||||
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
|
||||
"echo": echoprovider.NewProviderServer(),
|
||||
},
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccAzureAccessCredentialsEphemeralResourceConfig_basic(backend, role, conf, tt.validateCreds),
|
||||
ConfigStateChecks: []statecheck.StateCheck{
|
||||
statecheck.ExpectKnownValue("echo.test_azure",
|
||||
tfjsonpath.New("data").AtMapKey("client_id"),
|
||||
knownvalue.StringExact(conf.ClientID)),
|
||||
statecheck.ExpectKnownValue("echo.test_azure",
|
||||
tfjsonpath.New("data").AtMapKey("client_secret"),
|
||||
knownvalue.StringRegexp(nonEmpty)),
|
||||
statecheck.ExpectKnownValue("echo.test_azure",
|
||||
tfjsonpath.New("data").AtMapKey("lease_id"),
|
||||
knownvalue.StringRegexp(nonEmpty)),
|
||||
statecheck.ExpectKnownValue("echo.test_azure",
|
||||
tfjsonpath.New("data").AtMapKey("lease_duration"),
|
||||
knownvalue.NotNull()),
|
||||
statecheck.ExpectKnownValue("echo.test_azure",
|
||||
tfjsonpath.New("data").AtMapKey("lease_start_time"),
|
||||
knownvalue.StringRegexp(nonEmpty)),
|
||||
statecheck.ExpectKnownValue("echo.test_azure",
|
||||
tfjsonpath.New("data").AtMapKey("lease_renewable"),
|
||||
knownvalue.NotNull()),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testAccAzureAccessCredentialsEphemeralResourceConfig_basic(backend, role string, conf *testutil.AzureTestConf, validateCreds bool) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "vault_azure_secret_backend" "azure" {
|
||||
subscription_id = "%s"
|
||||
tenant_id = "%s"
|
||||
client_id = "%s"
|
||||
client_secret = "%s"
|
||||
path = "%s"
|
||||
}
|
||||
|
||||
resource "vault_azure_secret_backend_role" "role" {
|
||||
backend = vault_azure_secret_backend.azure.path
|
||||
role = "%s"
|
||||
ttl = 3600
|
||||
max_ttl = 7200
|
||||
application_object_id = "%s"
|
||||
}
|
||||
|
||||
ephemeral "vault_azure_access_credentials" "cred" {
|
||||
backend = vault_azure_secret_backend.azure.path
|
||||
role = vault_azure_secret_backend_role.role.role
|
||||
mount_id = vault_azure_secret_backend_role.role.id
|
||||
validate_creds = %t
|
||||
num_sequential_successes = 2
|
||||
}
|
||||
|
||||
provider "echo" {
|
||||
data = ephemeral.vault_azure_access_credentials.cred
|
||||
}
|
||||
|
||||
resource "echo" "test_azure" {}
|
||||
`,
|
||||
conf.SubscriptionID,
|
||||
conf.TenantID,
|
||||
conf.ClientID,
|
||||
conf.ClientSecret,
|
||||
backend,
|
||||
role,
|
||||
conf.AppObjectID,
|
||||
validateCreds,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
layout: "vault"
|
||||
page_title: "Vault: ephemeral vault_azure_access_credentials resource"
|
||||
sidebar_current: "docs-vault-ephemeral-azure-access-credentials"
|
||||
description: |-
|
||||
Read an ephemeral dynamic secret from the Vault Azure Secrets engine
|
||||
|
||||
---
|
||||
|
||||
# vault_azure_access_credentials (Ephemeral)
|
||||
|
||||
Reads ephemeral dynamic Azure credentials for a role managed by the Azure Secrets Engine.
|
||||
These credentials are not stored in Terraform state.
|
||||
|
||||
For more information, refer to
|
||||
the [Vault Azure Secrets Engine documentation](https://developer.hashicorp.com/vault/docs/secrets/azure).
|
||||
|
||||
## Example Usage
|
||||
|
||||
```hcl
|
||||
resource "vault_azure_secret_backend" "azure" {
|
||||
subscription_id = var.subscription_id
|
||||
tenant_id = var.tenant_id
|
||||
client_id = var.client_id
|
||||
client_secret = var.client_secret
|
||||
path = "azure"
|
||||
}
|
||||
|
||||
resource "vault_azure_secret_backend_role" "azurerole" {
|
||||
backend = vault_azure_secret_backend.azure.path
|
||||
role = "azurerole"
|
||||
ttl = 3600
|
||||
max_ttl = 7200
|
||||
application_object_id = "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
|
||||
ephemeral "vault_azure_access_credentials" "creds" {
|
||||
mount_id = vault_azure_secret_backend.azure.id
|
||||
backend = vault_azure_secret_backend.azure.path
|
||||
role = vault_azure_secret_backend_role.azurerole.role
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `mount_id` - (Optional) If value is set, will defer provisioning the ephemeral resource until
|
||||
`terraform apply`. For more details, please refer to the official documentation around
|
||||
[using ephemeral resources in the Vault Provider](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/guides/using_ephemeral_resources).
|
||||
|
||||
* `namespace` - (Optional) The namespace of the target resource.
|
||||
The value should not contain leading or trailing forward slashes.
|
||||
The `namespace` is always relative to the provider's
|
||||
configured [namespace](/docs/providers/vault/index.html#namespace).
|
||||
*Available only for Vault Enterprise*.
|
||||
|
||||
* `backend` - (Required) Path to the mounted Azure Secrets Engine where the role resides.
|
||||
|
||||
* `role` - (Required) The name of the Azure role to generate credentials for.
|
||||
|
||||
* `validate_creds` - (Optional) Whether generated credentials should be validated before being returned.
|
||||
|
||||
* `num_sequential_successes` - (Optional) If 'validate_creds' is true, the number of sequential successes required to validate generated credentials. Defaults to 4.
|
||||
|
||||
* `num_seconds_between_tests` - (Optional) If 'validate_creds' is true, the number of seconds to wait between each test of generated credentials. Defaults to 1.
|
||||
|
||||
* `max_cred_validation_seconds` - (Optional) If 'validate_creds' is true, the number of seconds after which to give up validating credentials. Defaults to 300.
|
||||
|
||||
* `subscription_id` - (Optional) The subscription ID to use during credential validation. Defaults to the subscription ID configured in the Vault backend.
|
||||
|
||||
* `tenant_id` - (Optional) The tenant ID to use during credential validation. Defaults to the tenant ID configured in the Vault backend.
|
||||
|
||||
* `environment` - (Optional) The Azure environment to use during credential validation. Defaults to the Azure Public Cloud. Some possible values: AzurePublicCloud, AzureUSGovernmentCloud.
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
In addition to the arguments above, the following attributes are exported:
|
||||
|
||||
* `client_id` - The Azure AD Application's client ID.
|
||||
|
||||
* `client_secret` - The client secret for the Azure AD Application.
|
||||
|
||||
* `lease_id` - The lease identifier assigned by Vault.
|
||||
|
||||
* `lease_duration` - The duration of the secret lease in seconds.
|
||||
|
||||
* `lease_start_time` - The time when the lease was read, in RFC3339 format.
|
||||
|
||||
* `lease_renewable` - True if the lease can be renewed.
|
||||
Loading…
Add table
Reference in a new issue