mirror of
https://github.com/opentofu/terraform-provider-vault.git
synced 2026-01-11 19:46:35 +00:00
Add new resource SPIFFE config (#2620)
* WIP: Add support for SPIFFE auth configuration * use api model technique * Address import issues * Rename spiffe object to SpiffeAuthConfigModel * Import without ID * Leverage API model again with StringNull setting * Rename and implement ResourceWithImportState * Fix comment * Remove support for parsing namespaces from import ID * WIP Add SPIFFE role resource * Add new generic token model for auth roles and new framework * godocs and various small tweaks * Rename resource name to match existing pattern and add docs * Add cl * Add ability to filter tests by Vault version * Revert "Add ability to filter tests by Vault version" This reverts commit 0a8c445a199230113ffac763171730fdaa8dfd9c. * Review feedback * Filter tests by Vault version - take 2 * Apply suggestions from code review Co-authored-by: vinay-gopalan <86625824+vinay-gopalan@users.noreply.github.com> * PR feedback, remove text pre checks and a bad comment * PR feedback: Rename helper methods names to match old names --------- Co-authored-by: vinay-gopalan <86625824+vinay-gopalan@users.noreply.github.com>
This commit is contained in:
parent
44df8b3a78
commit
08925e862f
16 changed files with 1843 additions and 1 deletions
|
|
@ -14,6 +14,8 @@ FEATURES:
|
|||
* Add support for `max_retries` parameter in `vault_aws_secret_backend` resource. ([#2623](https://github.com/hashicorp/terraform-provider-vault/pull/2623))
|
||||
|
||||
* Add retry configuration fields (`max_retries`, `retry_delay`, `max_retry_delay`) to `vault_azure_auth_backend_config` resource for Azure API request resilience ([#2629](https://github.com/hashicorp/terraform-provider-vault/pull/2629))
|
||||
* Add new resources `vault_spiffe_auth_backend_config` and `vault_spiffe_auth_backend_role` ([#2620](https://github.com/hashicorp/terraform-provider-vault/pull/2620))
|
||||
|
||||
|
||||
BUGS:
|
||||
|
||||
|
|
|
|||
75
acctestutil/acctestutil.go
Normal file
75
acctestutil/acctestutil.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package acctestutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/provider"
|
||||
vaultSchema "github.com/hashicorp/terraform-provider-vault/schema"
|
||||
"github.com/hashicorp/terraform-provider-vault/testutil"
|
||||
"github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
var (
|
||||
TestProvider *schema.Provider
|
||||
)
|
||||
|
||||
// testAccProviderConfigure ensures Provider is only configured once
|
||||
//
|
||||
// The PreCheck(t) function is invoked for every test and this prevents
|
||||
// extraneous reconfiguration to the same values each time. However, this does
|
||||
// not prevent reconfiguration that may happen should the address of
|
||||
// Provider be errantly reused in ProviderFactories.
|
||||
var testAccProviderConfigure sync.Once
|
||||
|
||||
func TestAccPreCheck(t *testing.T) {
|
||||
t.Helper()
|
||||
PreCheck(t)
|
||||
testutil.FatalTestEnvUnset(t, api.EnvVaultAddress, api.EnvVaultToken)
|
||||
}
|
||||
|
||||
func TestEntPreCheck(t *testing.T) {
|
||||
t.Helper()
|
||||
PreCheck(t)
|
||||
SkipTestAccEnt(t)
|
||||
TestAccPreCheck(t)
|
||||
}
|
||||
|
||||
func PreCheck(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
// only required when running acceptance tests
|
||||
if os.Getenv(resource.EnvTfAcc) == "" && os.Getenv(testutil.EnvVarTfAccEnt) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
testAccProviderConfigure.Do(func() {
|
||||
// TODO: Are the registries needed here?
|
||||
p := vaultSchema.NewProvider(provider.NewProvider(map[string]*provider.Description{}, map[string]*provider.Description{}))
|
||||
TestProvider = p.SchemaProvider()
|
||||
|
||||
rootProviderResource := &schema.Resource{
|
||||
Schema: p.SchemaProvider().Schema,
|
||||
}
|
||||
rootProviderData := rootProviderResource.TestResourceData()
|
||||
m, err := provider.NewProviderMeta(rootProviderData)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
TestProvider.SetMeta(m)
|
||||
})
|
||||
}
|
||||
|
||||
func SkipTestAccEnt(t *testing.T) {
|
||||
t.Helper()
|
||||
testutil.SkipTestEnvUnset(t, testutil.EnvVarTfAccEnt)
|
||||
}
|
||||
|
||||
func SkipTestAcc(t *testing.T) {
|
||||
t.Helper()
|
||||
testutil.SkipTestEnvUnset(t, resource.EnvTfAcc)
|
||||
}
|
||||
76
acctestutil/vaulthelper.go
Normal file
76
acctestutil/vaulthelper.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package acctestutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/provider"
|
||||
)
|
||||
|
||||
type CompareVaultVersionFunc func(*version.Version) bool
|
||||
|
||||
// SkipIfAPIVersionLT skips of the running vault version is less-than ver.
|
||||
func SkipIfAPIVersionLT(t *testing.T, ver *version.Version) {
|
||||
t.Helper()
|
||||
SkipIfAPIVersion(t, func(curVer *version.Version) bool {
|
||||
return curVer.LessThan(ver)
|
||||
}, "Vault version < %q", ver)
|
||||
}
|
||||
|
||||
// SkipIfAPIVersionLTE skips if the running vault version is less-than-or-equal to ver.
|
||||
func SkipIfAPIVersionLTE(t *testing.T, ver *version.Version) {
|
||||
t.Helper()
|
||||
SkipIfAPIVersion(t, func(curVer *version.Version) bool {
|
||||
return curVer.LessThanOrEqual(ver)
|
||||
}, "Vault version <= %q", ver)
|
||||
}
|
||||
|
||||
// SkipIfAPIVersionEQ skips if the running vault version is equal to ver.
|
||||
func SkipIfAPIVersionEQ(t *testing.T, ver *version.Version) {
|
||||
t.Helper()
|
||||
f := func(curVer *version.Version) bool {
|
||||
return curVer.Equal(ver)
|
||||
}
|
||||
SkipIfAPIVersion(t, f, "Vault version == %q", ver)
|
||||
}
|
||||
|
||||
// SkipIfAPIVersionGT skips if the running vault version is greater-than ver.
|
||||
func SkipIfAPIVersionGT(t *testing.T, ver *version.Version) {
|
||||
t.Helper()
|
||||
f := func(curVer *version.Version) bool {
|
||||
return curVer.GreaterThan(ver)
|
||||
}
|
||||
SkipIfAPIVersion(t, f, "Vault version > %q", ver)
|
||||
}
|
||||
|
||||
// SkipIfAPIVersionGTE skips if the running vault version is greater-than-or-equal to ver.
|
||||
func SkipIfAPIVersionGTE(t *testing.T, ver *version.Version) {
|
||||
t.Helper()
|
||||
f := func(curVer *version.Version) bool {
|
||||
return curVer.GreaterThanOrEqual(ver)
|
||||
}
|
||||
SkipIfAPIVersion(t, f, "Vault version >= %q", ver)
|
||||
}
|
||||
|
||||
func SkipIfAPIVersion(t *testing.T, cmp CompareVaultVersionFunc, format string, args ...interface{}) {
|
||||
t.Helper()
|
||||
|
||||
if TestProvider == nil {
|
||||
t.Fatalf("Provider is nil")
|
||||
}
|
||||
|
||||
pm, ok := TestProvider.Meta().(*provider.ProviderMeta)
|
||||
if !ok {
|
||||
t.Fatalf("expected provider meta to be of type *provider.ProviderMeta, got %T", TestProvider.Meta())
|
||||
}
|
||||
|
||||
curVersion := pm.GetVaultVersion()
|
||||
if curVersion == nil {
|
||||
t.Fatalf("vault version not set on %T", pm)
|
||||
}
|
||||
|
||||
t.Logf("Vault server version %q", curVersion)
|
||||
if cmp(curVersion) {
|
||||
t.Skipf(format, args...)
|
||||
}
|
||||
}
|
||||
1
go.mod
1
go.mod
|
|
@ -14,6 +14,7 @@ require (
|
|||
github.com/denisenkom/go-mssqldb v0.12.3
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gosimple/slug v1.15.0
|
||||
github.com/hashicorp/consul/api v1.32.1
|
||||
|
|
|
|||
|
|
@ -645,6 +645,7 @@ const (
|
|||
VaultVersion1185 = "1.18.5"
|
||||
VaultVersion119 = "1.19.0"
|
||||
VaultVersion120 = "1.20.0"
|
||||
VaultVersion121 = "1.21.0"
|
||||
|
||||
/*
|
||||
Vault auth methods
|
||||
|
|
|
|||
234
internal/framework/token/token.go
Normal file
234
internal/framework/token/token.go
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/framework/base"
|
||||
)
|
||||
|
||||
// TokenModel provides a base struct for auth backend roles that contain the common token
|
||||
// fields. Note that this model does not include any of the deprecated token fields.
|
||||
type TokenModel struct {
|
||||
base.BaseModel
|
||||
|
||||
TokenTTL types.Int64 `tfsdk:"token_ttl"`
|
||||
TokenMaxTTL types.Int64 `tfsdk:"token_max_ttl"`
|
||||
TokenPolicies types.Set `tfsdk:"token_policies"`
|
||||
TokenBoundCIDRs types.Set `tfsdk:"token_bound_cidrs"`
|
||||
TokenExplicitMaxTTL types.Int64 `tfsdk:"token_explicit_max_ttl"`
|
||||
TokenNoDefaultPolicy types.Bool `tfsdk:"token_no_default_policy"`
|
||||
TokenNumUses types.Int64 `tfsdk:"token_num_uses"`
|
||||
TokenPeriod types.Int64 `tfsdk:"token_period"`
|
||||
TokenType types.String `tfsdk:"token_type"`
|
||||
AliasMetadata types.Map `tfsdk:"alias_metadata"`
|
||||
}
|
||||
|
||||
// TokenAPIModel represents all the common auth token fields from an API perspective. Note none
|
||||
// of the deprecated token fields are included.
|
||||
type TokenAPIModel struct {
|
||||
TokenTTL int64 `json:"token_ttl" mapstructure:"token_ttl"`
|
||||
TokenMaxTTL int64 `json:"token_max_ttl" mapstructure:"token_max_ttl"`
|
||||
TokenPolicies []string `json:"token_policies" mapstructure:"token_policies"`
|
||||
TokenBoundCIDRs []string `json:"token_bound_cidrs" mapstructure:"token_bound_cidrs"`
|
||||
TokenExplicitMaxTTL int64 `json:"token_explicit_max_ttl" mapstructure:"token_explicit_max_ttl"`
|
||||
TokenNoDefaultPolicy bool `json:"token_no_default_policy" mapstructure:"token_no_default_policy"`
|
||||
TokenNumUses int64 `json:"token_num_uses" mapstructure:"token_num_uses"`
|
||||
TokenPeriod int64 `json:"token_period" mapstructure:"token_period"`
|
||||
TokenType string `json:"token_type" mapstructure:"token_type"`
|
||||
AliasMetadata map[string]string `json:"alias_metadata" mapstructure:"alias_metadata"`
|
||||
}
|
||||
|
||||
// MustAddBaseAndTokenSchemas adds the schema fields that are required for all net new
|
||||
// resources and data sources built with the TF Plugin Framework extending the
|
||||
// TokenModel base model.
|
||||
//
|
||||
// This should be called from a resources or data source's Schema() method.
|
||||
func MustAddBaseAndTokenSchemas(s *schema.Schema) {
|
||||
base.MustAddBaseSchema(s)
|
||||
|
||||
for k, v := range tokenSchema() {
|
||||
if _, ok := s.Attributes[k]; ok {
|
||||
panic(fmt.Sprintf("cannot add schema field %q, already exists in the Schema map", k))
|
||||
}
|
||||
|
||||
s.Attributes[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// PopulateTokenAPIFromModel copies the data from a TokenModel into a TokenAPIModel, useful for
|
||||
// translating TF data into the Vault API data model.
|
||||
func PopulateTokenAPIFromModel(ctx context.Context, model *TokenModel, apiModel *TokenAPIModel) diag.Diagnostics {
|
||||
if apiModel == nil {
|
||||
return diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("cannot populate nil api model", ""),
|
||||
}
|
||||
}
|
||||
if model == nil {
|
||||
return diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("cannot populate api model from nil token model", ""),
|
||||
}
|
||||
}
|
||||
|
||||
apiModel.TokenTTL = model.TokenTTL.ValueInt64()
|
||||
apiModel.TokenMaxTTL = model.TokenMaxTTL.ValueInt64()
|
||||
|
||||
var tokenPolicies []string
|
||||
if err := model.TokenPolicies.ElementsAs(ctx, &tokenPolicies, false); err.HasError() {
|
||||
return err
|
||||
}
|
||||
apiModel.TokenPolicies = tokenPolicies
|
||||
|
||||
var tokenBoundCIDRs []string
|
||||
if err := model.TokenBoundCIDRs.ElementsAs(ctx, &tokenBoundCIDRs, false); err.HasError() {
|
||||
return err
|
||||
}
|
||||
apiModel.TokenBoundCIDRs = tokenBoundCIDRs
|
||||
|
||||
apiModel.TokenExplicitMaxTTL = model.TokenExplicitMaxTTL.ValueInt64()
|
||||
apiModel.TokenNoDefaultPolicy = model.TokenNoDefaultPolicy.ValueBool()
|
||||
apiModel.TokenNumUses = model.TokenNumUses.ValueInt64()
|
||||
apiModel.TokenPeriod = model.TokenPeriod.ValueInt64()
|
||||
apiModel.TokenType = model.TokenType.ValueString()
|
||||
|
||||
var aliasMetadata map[string]string
|
||||
if err := model.AliasMetadata.ElementsAs(ctx, &aliasMetadata, false); err.HasError() {
|
||||
return err
|
||||
}
|
||||
apiModel.AliasMetadata = aliasMetadata
|
||||
|
||||
return diag.Diagnostics{}
|
||||
}
|
||||
|
||||
// PopulateTokenModelFromAPI copies the data from a TokenAPIModel into a TokenModel, useful for
|
||||
// translating Vault API data into the TF data model.
|
||||
func PopulateTokenModelFromAPI(ctx context.Context, model *TokenModel, apiModel *TokenAPIModel) diag.Diagnostics {
|
||||
if apiModel == nil {
|
||||
return diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("cannot populate token model from nil api model", ""),
|
||||
}
|
||||
}
|
||||
if model == nil {
|
||||
return diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("cannot populate nil model", ""),
|
||||
}
|
||||
}
|
||||
|
||||
model.TokenTTL = types.Int64Null()
|
||||
if apiModel.TokenTTL != 0 {
|
||||
model.TokenTTL = types.Int64Value(apiModel.TokenTTL)
|
||||
}
|
||||
|
||||
model.TokenMaxTTL = types.Int64Null()
|
||||
if apiModel.TokenMaxTTL != 0 {
|
||||
model.TokenMaxTTL = types.Int64Value(apiModel.TokenMaxTTL)
|
||||
}
|
||||
|
||||
model.TokenPolicies = types.SetNull(types.StringType)
|
||||
if len(apiModel.TokenPolicies) > 0 {
|
||||
policies, err := types.SetValueFrom(ctx, types.StringType, apiModel.TokenPolicies)
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
model.TokenPolicies = policies
|
||||
}
|
||||
|
||||
model.TokenBoundCIDRs = types.SetNull(types.StringType)
|
||||
if len(apiModel.TokenBoundCIDRs) > 0 {
|
||||
cidrs, err := types.SetValueFrom(ctx, types.StringType, apiModel.TokenBoundCIDRs)
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
model.TokenBoundCIDRs = cidrs
|
||||
}
|
||||
|
||||
model.TokenExplicitMaxTTL = types.Int64Null()
|
||||
if apiModel.TokenExplicitMaxTTL != 0 {
|
||||
model.TokenExplicitMaxTTL = types.Int64Value(apiModel.TokenExplicitMaxTTL)
|
||||
}
|
||||
|
||||
model.TokenNoDefaultPolicy = types.BoolNull()
|
||||
if apiModel.TokenNoDefaultPolicy {
|
||||
model.TokenNoDefaultPolicy = types.BoolValue(apiModel.TokenNoDefaultPolicy)
|
||||
}
|
||||
|
||||
model.TokenNumUses = types.Int64Null()
|
||||
if apiModel.TokenNumUses != 0 {
|
||||
model.TokenNumUses = types.Int64Value(apiModel.TokenNumUses)
|
||||
}
|
||||
|
||||
model.TokenPeriod = types.Int64Null()
|
||||
if apiModel.TokenPeriod != 0 {
|
||||
model.TokenPeriod = types.Int64Value(apiModel.TokenPeriod)
|
||||
}
|
||||
|
||||
model.TokenType = types.StringNull()
|
||||
if apiModel.TokenType != "" {
|
||||
model.TokenType = types.StringValue(apiModel.TokenType)
|
||||
}
|
||||
|
||||
model.AliasMetadata = types.MapNull(types.StringType)
|
||||
if len(apiModel.AliasMetadata) > 0 {
|
||||
metadata, err := types.MapValueFrom(ctx, types.StringType, apiModel.AliasMetadata)
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
model.AliasMetadata = metadata
|
||||
}
|
||||
|
||||
return diag.Diagnostics{}
|
||||
}
|
||||
|
||||
func tokenSchema() map[string]schema.Attribute {
|
||||
return map[string]schema.Attribute{
|
||||
"token_ttl": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "The initial ttl of the token to generate in seconds",
|
||||
},
|
||||
"token_max_ttl": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "The maximum lifetime of the generated token",
|
||||
},
|
||||
"token_policies": schema.SetAttribute{
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
Description: "Generated Token's Policies",
|
||||
},
|
||||
"token_bound_cidrs": schema.SetAttribute{
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
Description: "Specifies the blocks of IP addresses which are allowed to use the generated token",
|
||||
},
|
||||
"token_explicit_max_ttl": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "Generated Token's Explicit Maximum TTL in seconds",
|
||||
},
|
||||
"token_no_default_policy": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "If true, the 'default' policy will not automatically be added to generated tokens",
|
||||
},
|
||||
"token_num_uses": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "The maximum number of times a token may be used, a value of zero means unlimited",
|
||||
},
|
||||
"token_period": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "Generated Token's Period",
|
||||
},
|
||||
"token_type": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "The type of token to generate, service or batch",
|
||||
Default: stringdefault.StaticString("default"),
|
||||
},
|
||||
"alias_metadata": schema.MapAttribute{
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
Description: "A map of string to string that will be set as metadata on the identity alias",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,12 @@ package fwprovider
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/vault/auth/spiffe"
|
||||
ephemeralsecrets "github.com/hashicorp/terraform-provider-vault/internal/vault/secrets/ephemeral"
|
||||
"regexp"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
|
|
@ -223,6 +225,8 @@ func (p *fwprovider) Configure(ctx context.Context, req provider.ConfigureReques
|
|||
// the Metadata method. All resources must have unique names.
|
||||
func (p *fwprovider) Resources(ctx context.Context) []func() resource.Resource {
|
||||
return []func() resource.Resource{
|
||||
spiffe.NewSpiffeAuthConfigResource,
|
||||
spiffe.NewSpiffeAuthRoleResource,
|
||||
sys.NewPasswordPolicyResource,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ var (
|
|||
VaultVersion1185 = version.Must(version.NewSemver(consts.VaultVersion1185))
|
||||
VaultVersion119 = version.Must(version.NewSemver(consts.VaultVersion119))
|
||||
VaultVersion120 = version.Must(version.NewSemver(consts.VaultVersion120))
|
||||
VaultVersion121 = version.Must(version.NewSemver(consts.VaultVersion121))
|
||||
|
||||
TokenTTLMinRecommended = time.Minute * 15
|
||||
)
|
||||
|
|
|
|||
400
internal/vault/auth/spiffe/spiffe_config_resource.go
Normal file
400
internal/vault/auth/spiffe/spiffe_config_resource.go
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
package spiffe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
spiffeConfigPath = "config"
|
||||
)
|
||||
|
||||
var backendNameRegexp = regexp.MustCompile("^auth/(.+)/config$")
|
||||
|
||||
// Ensure the implementation satisfies the resource.ResourceWithImportState interface
|
||||
var _ resource.ResourceWithImportState = &SpiffeAuthConfigResource{}
|
||||
|
||||
// NewSpiffeAuthConfigResource returns the implementation for this resource to be
|
||||
// imported by the Terraform Plugin Framework provider
|
||||
func NewSpiffeAuthConfigResource() resource.Resource {
|
||||
return &SpiffeAuthConfigResource{}
|
||||
}
|
||||
|
||||
// SpiffeAuthConfigResource implements the methods that define this resource
|
||||
type SpiffeAuthConfigResource struct {
|
||||
base.ResourceWithConfigure
|
||||
}
|
||||
|
||||
type SpiffeAuthConfigModel struct {
|
||||
base.BaseModel
|
||||
|
||||
Mount types.String `tfsdk:"mount"`
|
||||
TrustDomain types.String `tfsdk:"trust_domain"`
|
||||
Profile types.String `tfsdk:"profile"`
|
||||
EndPointUrl types.String `tfsdk:"endpoint_url"`
|
||||
EndpointSpiffeId types.String `tfsdk:"endpoint_spiffe_id"`
|
||||
EndpointRootCaTrustStorePem types.String `tfsdk:"endpoint_root_ca_truststore_pem"`
|
||||
Bundle types.String `tfsdk:"bundle"`
|
||||
DeferBundleFetch types.Bool `tfsdk:"defer_bundle_fetch"`
|
||||
Audience types.List `tfsdk:"audience"`
|
||||
}
|
||||
|
||||
type SpiffeConfigAPIModel struct {
|
||||
TrustDomain string `json:"trust_domain" mapstructure:"trust_domain"`
|
||||
Profile string `json:"profile" mapstructure:"profile"`
|
||||
EndPointUrl string `json:"endpoint_url" mapstructure:"endpoint_url"`
|
||||
EndpointSpiffeId string `json:"endpoint_spiffe_id" mapstructure:"endpoint_spiffe_id"`
|
||||
EndpointRootCaTrustStorePem string `json:"endpoint_root_ca_truststore_pem" mapstructure:"endpoint_root_ca_truststore_pem"`
|
||||
Bundle string `json:"bundle" mapstructure:"bundle"`
|
||||
DeferBundleFetch bool `json:"defer_bundle_fetch" mapstructure:"defer_bundle_fetch"`
|
||||
Audience []string `json:"audience" mapstructure:"audience"`
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_spiffe_auth_backend_config"
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
consts.FieldMount: schema.StringAttribute{
|
||||
MarkdownDescription: "Mount path for the SPIFFE auth engine in Vault.",
|
||||
Required: true,
|
||||
},
|
||||
"trust_domain": schema.StringAttribute{
|
||||
MarkdownDescription: "The SPIFFE trust domain for this backend.",
|
||||
Required: true,
|
||||
},
|
||||
"profile": schema.StringAttribute{
|
||||
MarkdownDescription: "The mechanism to fetch or embed the trust bundle to use.",
|
||||
Required: true,
|
||||
},
|
||||
"endpoint_url": schema.StringAttribute{
|
||||
MarkdownDescription: `The URI to be used when profile is 'https_web_bundle' or 'https_spiffe_bundle'`,
|
||||
Optional: true,
|
||||
},
|
||||
"endpoint_spiffe_id": schema.StringAttribute{
|
||||
MarkdownDescription: `The server's SPIFFE ID to validate when profile is 'https_spiffe_bundle'`,
|
||||
Optional: true,
|
||||
},
|
||||
"endpoint_root_ca_truststore_pem": schema.StringAttribute{
|
||||
MarkdownDescription: `PEM-encoded CA certificate(s) to validate the server reached by 'endpoint_url', if set this will override the default TLS trust store`,
|
||||
Optional: true,
|
||||
},
|
||||
"bundle": schema.StringAttribute{
|
||||
MarkdownDescription: `When profile is 'https_spiffe_bundle', the bootstrapping bundle in SPIFFE format; when profile is 'static', either a bundle in SPIFFE format or PEM-encoded CA certificate(s)`,
|
||||
Optional: true,
|
||||
},
|
||||
"defer_bundle_fetch": schema.BoolAttribute{
|
||||
MarkdownDescription: `Don't attempt to fetch a bundle immediately; only applies when profile != static`,
|
||||
Optional: true,
|
||||
WriteOnly: true,
|
||||
},
|
||||
"audience": schema.ListAttribute{
|
||||
ElementType: types.StringType,
|
||||
MarkdownDescription: `A list of audience values allowed to match claims in JWT-SVIDs`,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
base.MustAddBaseSchema(&resp.Schema)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var data SpiffeAuthConfigModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
vaultClient, err := client.GetClient(ctx, s.Meta(), data.Namespace.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
deferBundleFetch, diagErr := s.readDeferBundleFetchConfig(ctx, req.Config)
|
||||
if diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
vaultRequest, diagErr := s.getApiModel(ctx, &data, deferBundleFetch)
|
||||
if diagErr != nil {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
// vault returns a nil response on success
|
||||
mountPath := s.path(data.Mount.ValueString())
|
||||
confResp, err := vaultClient.Logical().WriteWithContext(ctx, mountPath, vaultRequest)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultCreateErr(err))
|
||||
return
|
||||
}
|
||||
if confResp == nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultReadResponseNil())
|
||||
return
|
||||
}
|
||||
|
||||
if diagErr := s.populateDataModelFromApi(ctx, &data, confResp); diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
// Save data into Terraform state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var data SpiffeAuthConfigModel
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
vaultClient, err := client.GetClient(ctx, s.Meta(), data.Namespace.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
policyResp, err := vaultClient.Logical().ReadWithContext(ctx, s.path(data.Mount.ValueString()))
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultReadErr(err))
|
||||
return
|
||||
}
|
||||
if policyResp == nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultReadResponseNil())
|
||||
return
|
||||
}
|
||||
|
||||
if diagErr := s.populateDataModelFromApi(ctx, &data, policyResp); diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var data SpiffeAuthConfigModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
vaultClient, err := client.GetClient(ctx, s.Meta(), data.Namespace.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
|
||||
return
|
||||
}
|
||||
deferBundleFetch, diagErr := s.readDeferBundleFetchConfig(ctx, req.Config)
|
||||
if diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
vaultRequest, diagErr := s.getApiModel(ctx, &data, deferBundleFetch)
|
||||
if diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
mountPath := s.path(data.Mount.ValueString())
|
||||
confResp, err := vaultClient.Logical().WriteWithContext(ctx, mountPath, vaultRequest)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultCreateErr(err))
|
||||
return
|
||||
}
|
||||
if confResp == nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultReadResponseNil())
|
||||
return
|
||||
}
|
||||
|
||||
if diagErr := s.populateDataModelFromApi(ctx, &data, confResp); diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
// Save data into Terraform state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
||||
// API does not support delete, so just remove from state
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root(consts.FieldMount), req, resp)
|
||||
|
||||
mount, err := extractSpiffeConfigMountFromID(req.ID)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
"Error parsing import identifier",
|
||||
fmt.Sprintf("The import identifier '%s' is not valid: %s", req.ID, err.Error()),
|
||||
)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(consts.FieldMount), mount)...)
|
||||
|
||||
ns := os.Getenv(consts.EnvVarVaultNamespaceImport)
|
||||
if ns != "" {
|
||||
tflog.Info(
|
||||
ctx,
|
||||
fmt.Sprintf("Environment variable %s set, attempting TF state import", consts.EnvVarVaultNamespaceImport),
|
||||
map[string]any{consts.FieldNamespace: ns},
|
||||
)
|
||||
resp.Diagnostics.Append(
|
||||
resp.State.SetAttribute(ctx, path.Root(consts.FieldNamespace), ns)...,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) path(mount string) string {
|
||||
return fmt.Sprintf("auth/%s/%s", mount, spiffeConfigPath)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) readDeferBundleFetchConfig(ctx context.Context, config tfsdk.Config) (bool, diag.Diagnostics) {
|
||||
var deferBundleFetch *bool
|
||||
if diagErr := config.GetAttribute(ctx, path.Root("defer_bundle_fetch"), &deferBundleFetch); diagErr.HasError() {
|
||||
return false, diagErr
|
||||
}
|
||||
if deferBundleFetch == nil {
|
||||
return false, diag.Diagnostics{}
|
||||
}
|
||||
return *deferBundleFetch, diag.Diagnostics{}
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) getApiModel(ctx context.Context, data *SpiffeAuthConfigModel, deferBundleFetch bool) (map[string]any, diag.Diagnostics) {
|
||||
// Note: defer bundle fetch is marked as write-only so it is never
|
||||
// part of the plan which the data model is built from
|
||||
apiModel := SpiffeConfigAPIModel{
|
||||
TrustDomain: data.TrustDomain.ValueString(),
|
||||
Profile: data.Profile.ValueString(),
|
||||
EndPointUrl: data.EndPointUrl.ValueString(),
|
||||
EndpointSpiffeId: data.EndpointSpiffeId.ValueString(),
|
||||
EndpointRootCaTrustStorePem: data.EndpointRootCaTrustStorePem.ValueString(),
|
||||
Bundle: data.Bundle.ValueString(),
|
||||
DeferBundleFetch: deferBundleFetch,
|
||||
}
|
||||
|
||||
var audienceVals []string
|
||||
if diagErr := data.Audience.ElementsAs(ctx, &audienceVals, false); diagErr.HasError() {
|
||||
return nil, diagErr
|
||||
}
|
||||
apiModel.Audience = audienceVals
|
||||
|
||||
var vaultRequest map[string]any
|
||||
if err := mapstructure.Decode(apiModel, &vaultRequest); err != nil {
|
||||
return nil, diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("Failed to decode SPIFFE config API model to map", err.Error()),
|
||||
}
|
||||
}
|
||||
|
||||
return vaultRequest, nil
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthConfigResource) populateDataModelFromApi(ctx context.Context, data *SpiffeAuthConfigModel, resp *api.Secret) diag.Diagnostics {
|
||||
if resp == nil || resp.Data == nil {
|
||||
return diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("Missing data in API response", "The API response or response data was nil."),
|
||||
}
|
||||
}
|
||||
|
||||
var readResp SpiffeConfigAPIModel
|
||||
if err := model.ToAPIModel(resp.Data, &readResp); err != nil {
|
||||
return diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("Unable to translate Vault response data", err.Error()),
|
||||
}
|
||||
}
|
||||
data.Profile = types.StringValue(readResp.Profile)
|
||||
data.TrustDomain = types.StringValue(readResp.TrustDomain)
|
||||
|
||||
data.EndpointSpiffeId = types.StringNull()
|
||||
if readResp.EndpointSpiffeId != "" {
|
||||
data.EndpointSpiffeId = types.StringValue(readResp.EndpointSpiffeId)
|
||||
}
|
||||
|
||||
data.EndPointUrl = types.StringNull()
|
||||
if readResp.EndPointUrl != "" {
|
||||
data.EndPointUrl = types.StringValue(readResp.EndPointUrl)
|
||||
}
|
||||
|
||||
data.EndpointRootCaTrustStorePem = types.StringNull()
|
||||
if readResp.EndpointRootCaTrustStorePem != "" {
|
||||
data.EndpointRootCaTrustStorePem = types.StringValue(readResp.EndpointRootCaTrustStorePem)
|
||||
}
|
||||
|
||||
data.Bundle = types.StringNull()
|
||||
if readResp.Bundle != "" {
|
||||
data.Bundle = types.StringValue(readResp.Bundle)
|
||||
}
|
||||
|
||||
// Note that DeferBundleFetch influences how the API is run, and is not returned from the API endpoint
|
||||
|
||||
if len(readResp.Audience) > 0 {
|
||||
aud, listErr := types.ListValueFrom(ctx, types.StringType, readResp.Audience)
|
||||
if listErr != nil {
|
||||
return listErr
|
||||
}
|
||||
data.Audience = aud
|
||||
}
|
||||
|
||||
return diag.Diagnostics{}
|
||||
}
|
||||
|
||||
// extractSpiffeConfigMountFromID extracts the mount path from the given import ID provided
|
||||
// by the terraform import CLI command.
|
||||
func extractSpiffeConfigMountFromID(id string) (string, error) {
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("import identifier cannot be empty")
|
||||
}
|
||||
// Trim leading slash if present
|
||||
id = strings.Trim(id, "/")
|
||||
|
||||
if !backendNameRegexp.MatchString(id) {
|
||||
return "", fmt.Errorf("import identifier must be of the form 'auth/<mount>/config', "+
|
||||
"namespace can be specified using the env var %s", consts.EnvVarVaultNamespaceImport)
|
||||
}
|
||||
|
||||
matches := backendNameRegexp.FindStringSubmatch(id)
|
||||
if len(matches) != 2 {
|
||||
return "", fmt.Errorf("import identifier must be of the form 'auth/<mount>/config', "+
|
||||
"namespace can be specified using the env var %s", consts.EnvVarVaultNamespaceImport)
|
||||
}
|
||||
|
||||
mount := strings.TrimSpace(matches[1])
|
||||
if mount == "" {
|
||||
return "", fmt.Errorf("mount cannot be empty")
|
||||
}
|
||||
|
||||
return mount, nil
|
||||
}
|
||||
248
internal/vault/auth/spiffe/spiffe_config_resource_test.go
Normal file
248
internal/vault/auth/spiffe/spiffe_config_resource_test.go
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
package spiffe_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
"github.com/hashicorp/terraform-plugin-testing/terraform"
|
||||
"github.com/hashicorp/terraform-provider-vault/acctestutil"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/provider"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/providertest"
|
||||
"github.com/hashicorp/terraform-provider-vault/testutil"
|
||||
)
|
||||
|
||||
// TestAccSpiffeAuthConfig tests the spiffe auth config resource
|
||||
func TestAccSpiffeAuthConfig(t *testing.T) {
|
||||
mount := acctest.RandomWithPrefix("spiffe-mount")
|
||||
caBytes, _, _ := testutil.GenerateCA()
|
||||
ca := strings.Trim(string(caBytes), "\n")
|
||||
resourceAddress := "vault_spiffe_auth_backend_config.spiffe_config"
|
||||
|
||||
spiffeBundle := `
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"use": "jwt-svid",
|
||||
"kty": "EC",
|
||||
"kid": "ZxKvdYWv1ZcSAUOQ0zxNmyvgm8eKKgIb",
|
||||
"crv": "P-256",
|
||||
"x": "UU_Z5vjB272LtPsRxemPskh8fVhEvfy7xzg3tsIyas0",
|
||||
"y": "0B8DIXslvTqYTVSxzuGyGzVVKTUOHcJMzjOfmmR3kaE"
|
||||
}
|
||||
],
|
||||
"spiffe_sequence": 1
|
||||
}
|
||||
`
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() {
|
||||
acctestutil.TestEntPreCheck(t)
|
||||
acctestutil.SkipIfAPIVersionLT(t, provider.VaultVersion121)
|
||||
},
|
||||
ProtoV5ProviderFactories: providertest.ProtoV5ProviderFactories,
|
||||
|
||||
Steps: []resource.TestStep{
|
||||
// Test the simplest form of config
|
||||
{
|
||||
Config: staticBundleSpiffeConfig(mount, ca),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr(resourceAddress, "trust_domain", "example.org"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "profile", "static"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "bundle", ca+"\n"),
|
||||
),
|
||||
},
|
||||
// Test we can set the audience list
|
||||
{
|
||||
Config: staticBundleSpiffeConfigWithAudience(mount, ca, []string{"vault", "vault-core"}),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr(resourceAddress, "trust_domain", "example.org"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "profile", "static"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "bundle", ca+"\n"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "audience"+".#", "2"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "audience"+".0", "vault"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "audience"+".1", "vault-core"),
|
||||
),
|
||||
},
|
||||
// Test we can clear the audience list
|
||||
{
|
||||
Config: staticBundleSpiffeConfigWithAudience(mount, ca, []string{}),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr(resourceAddress, "trust_domain", "example.org"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "profile", "static"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "bundle", ca+"\n"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "audience"+".#", "0"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: httpsWebPemSpiffeConfig(mount, ca),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr(resourceAddress, "trust_domain", "example.org"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "profile", "https_web_pem"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "endpoint_url", "https://dadgarcorp.com/spiffe-ca"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "endpoint_root_ca_truststore_pem", ca+"\n"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "bundle"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: webBundleSpiffeConfig(mount),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr(resourceAddress, "trust_domain", "example.org"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "profile", "https_web_bundle"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "endpoint_url", "https://dadgarcorp.com/spiffe-ca"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "bundle"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: spiffeBundleSpiffeConfig(mount, spiffeBundle),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr(resourceAddress, "trust_domain", "example.org"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "profile", "https_spiffe_bundle"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "endpoint_url", "https://dadgarcorp.com/spiffe-ca"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "endpoint_spiffe_id", "spiffe://dadgarcorp.com/spire"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "bundle", spiffeBundle+"\n"),
|
||||
),
|
||||
},
|
||||
// Test importing
|
||||
{
|
||||
ResourceName: resourceAddress,
|
||||
ImportState: true,
|
||||
ImportStateIdFunc: testAccSpiffeAuthConfigImportStateIdFunc(resourceAddress),
|
||||
ImportStateVerify: true,
|
||||
ImportStateVerifyIdentifierAttribute: "mount",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccSpiffeAuthConfigImportStateIdFunc(resourceName string) resource.ImportStateIdFunc {
|
||||
return func(s *terraform.State) (string, error) {
|
||||
rs, ok := s.RootModule().Resources[resourceName]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("not found: %s", resourceName)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("auth/%s/config", rs.Primary.Attributes["mount"]), nil
|
||||
}
|
||||
}
|
||||
|
||||
func staticBundleSpiffeConfig(mount string, ca string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "vault_auth_backend" "spiffe_mount" {
|
||||
type = "spiffe"
|
||||
path = "%s"
|
||||
|
||||
tune {
|
||||
passthrough_request_headers = ["Authorization"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_spiffe_auth_backend_config" "spiffe_config" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
trust_domain = "example.org"
|
||||
profile = "static"
|
||||
bundle = <<EOC
|
||||
%s
|
||||
EOC
|
||||
}
|
||||
`, mount, ca)
|
||||
}
|
||||
|
||||
func staticBundleSpiffeConfigWithAudience(mount string, ca string, audiences []string) string {
|
||||
var formattedAudiences string
|
||||
if len(audiences) > 0 {
|
||||
formattedAudiences = "\"" + strings.Join(audiences, "\", \"") + "\""
|
||||
}
|
||||
return fmt.Sprintf(`
|
||||
resource "vault_auth_backend" "spiffe_mount" {
|
||||
type = "spiffe"
|
||||
path = "%s"
|
||||
|
||||
tune {
|
||||
passthrough_request_headers = ["Authorization"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_spiffe_auth_backend_config" "spiffe_config" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
trust_domain = "example.org"
|
||||
profile = "static"
|
||||
bundle = <<EOC
|
||||
%s
|
||||
EOC
|
||||
audience = [%s]
|
||||
}
|
||||
`, mount, ca, formattedAudiences)
|
||||
}
|
||||
|
||||
func httpsWebPemSpiffeConfig(mount string, trustCa string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "vault_auth_backend" "spiffe_mount" {
|
||||
type = "spiffe"
|
||||
path = "%s"
|
||||
|
||||
tune {
|
||||
passthrough_request_headers = ["Authorization"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_spiffe_auth_backend_config" "spiffe_config" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
trust_domain = "example.org"
|
||||
profile = "https_web_pem"
|
||||
endpoint_url = "https://dadgarcorp.com/spiffe-ca"
|
||||
defer_bundle_fetch = true
|
||||
endpoint_root_ca_truststore_pem = <<EOC
|
||||
%s
|
||||
EOC
|
||||
}
|
||||
`, mount, trustCa)
|
||||
}
|
||||
|
||||
func webBundleSpiffeConfig(mount string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "vault_auth_backend" "spiffe_mount" {
|
||||
type = "spiffe"
|
||||
path = "%s"
|
||||
|
||||
tune {
|
||||
passthrough_request_headers = ["Authorization"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_spiffe_auth_backend_config" "spiffe_config" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
trust_domain = "example.org"
|
||||
profile = "https_web_bundle"
|
||||
endpoint_url = "https://dadgarcorp.com/spiffe-ca"
|
||||
defer_bundle_fetch = true
|
||||
}
|
||||
`, mount)
|
||||
}
|
||||
|
||||
func spiffeBundleSpiffeConfig(mount string, bundle string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "vault_auth_backend" "spiffe_mount" {
|
||||
type = "spiffe"
|
||||
path = "%s"
|
||||
|
||||
tune {
|
||||
passthrough_request_headers = ["Authorization"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_spiffe_auth_backend_config" "spiffe_config" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
trust_domain = "example.org"
|
||||
profile = "https_spiffe_bundle"
|
||||
endpoint_url = "https://dadgarcorp.com/spiffe-ca"
|
||||
endpoint_spiffe_id = "spiffe://dadgarcorp.com/spire"
|
||||
defer_bundle_fetch = true
|
||||
bundle = <<EOB
|
||||
%s
|
||||
EOB
|
||||
}
|
||||
`, mount, bundle)
|
||||
}
|
||||
368
internal/vault/auth/spiffe/spiffe_role_resource.go
Normal file
368
internal/vault/auth/spiffe/spiffe_role_resource.go
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
package spiffe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"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/terraform-provider-vault/internal/framework/token"
|
||||
"github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
var roleNameRegexp = regexp.MustCompile("^auth/(.+)/role/(.+)$")
|
||||
|
||||
// Ensure the implementation satisfies the resource.ResourceWithImportState interface
|
||||
var _ resource.ResourceWithImportState = &SpiffeAuthRoleResource{}
|
||||
|
||||
// NewSpiffeAuthRoleResource returns the implementation for this resource to be
|
||||
// imported by the Terraform Plugin Framework provider
|
||||
func NewSpiffeAuthRoleResource() resource.Resource {
|
||||
return &SpiffeAuthRoleResource{}
|
||||
}
|
||||
|
||||
// SpiffeAuthRoleResource implements the methods that define this resource
|
||||
type SpiffeAuthRoleResource struct {
|
||||
base.ResourceWithConfigure
|
||||
}
|
||||
|
||||
type SpiffeAuthRoleModel struct {
|
||||
token.TokenModel
|
||||
|
||||
Mount types.String `tfsdk:"mount"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
DisplayName types.String `tfsdk:"display_name"`
|
||||
WorkloadIDPatterns types.List `tfsdk:"workload_id_patterns"`
|
||||
}
|
||||
|
||||
type SpiffeRoleAPIModel struct {
|
||||
token.TokenAPIModel `mapstructure:",squash"`
|
||||
|
||||
DisplayName string `json:"display_name" mapstructure:"display_name"`
|
||||
WorkloadIDPatterns []string `json:"workload_id_patterns" mapstructure:"workload_id_patterns"`
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_spiffe_auth_backend_role"
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
|
||||
response.Schema = schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
consts.FieldMount: schema.StringAttribute{
|
||||
Description: "Mount path for the SPIFFE auth engine in Vault.",
|
||||
Required: true,
|
||||
},
|
||||
consts.FieldName: schema.StringAttribute{
|
||||
Description: "Name of the SPIFFE auth role.",
|
||||
Required: true,
|
||||
},
|
||||
consts.FieldDisplayName: schema.StringAttribute{
|
||||
Description: "A display name for the role. This is only used for display " +
|
||||
"purposes in Vault, if not provided it will default to the role name.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"workload_id_patterns": schema.ListAttribute{
|
||||
ElementType: types.StringType,
|
||||
Description: "A comma separated list of patterns that match an incoming workload " +
|
||||
"id to this role. A workload id is the part that remains after stripping the trust domain prefix " +
|
||||
"and the slash separator from a spiffe id.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
token.MustAddBaseAndTokenSchemas(&response.Schema)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var data SpiffeAuthRoleModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
vaultClient, err := client.GetClient(ctx, s.Meta(), data.Namespace.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
vaultRequest, diagErr := s.getApiModel(ctx, &data)
|
||||
if diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
mountPath, err := s.path(&data)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error determining role path", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
roleResp, err := vaultClient.Logical().WriteWithContext(ctx, mountPath, vaultRequest)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultCreateErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
if roleResp == nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultReadResponseNil())
|
||||
return
|
||||
}
|
||||
|
||||
if diagErr := s.populateDataModelFromApi(ctx, &data, roleResp); diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
// Save data into Terraform state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var data SpiffeAuthRoleModel
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
vaultClient, err := client.GetClient(ctx, s.Meta(), data.Namespace.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
mountPath, err := s.path(&data)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error determining role path", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
roleResp, err := vaultClient.Logical().ReadWithContext(ctx, mountPath)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultReadErr(err))
|
||||
return
|
||||
}
|
||||
if roleResp == nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultReadResponseNil())
|
||||
return
|
||||
}
|
||||
|
||||
if diagErr := s.populateDataModelFromApi(ctx, &data, roleResp); diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var data SpiffeAuthRoleModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
vaultClient, err := client.GetClient(ctx, s.Meta(), data.Namespace.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
vaultRequest, diagErr := s.getApiModel(ctx, &data)
|
||||
if diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
mountPath, err := s.path(&data)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error determining role path", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
roleResp, err := vaultClient.Logical().WriteWithContext(ctx, mountPath, vaultRequest)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultCreateErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
if roleResp == nil {
|
||||
resp.Diagnostics.AddError(errutil.VaultReadResponseNil())
|
||||
return
|
||||
}
|
||||
|
||||
if diagErr := s.populateDataModelFromApi(ctx, &data, roleResp); diagErr.HasError() {
|
||||
resp.Diagnostics.Append(diagErr...)
|
||||
return
|
||||
}
|
||||
|
||||
// Save data into Terraform state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var data SpiffeAuthRoleModel
|
||||
|
||||
// Read Terraform state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
vaultClient, err := client.GetClient(ctx, s.Meta(), data.Namespace.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
mountPath, err := s.path(&data)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error determining role path", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = vaultClient.Logical().Delete(mountPath); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting role", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
mount, roleName, err := s.extractSpiffeRoleIdentifiers(req.ID)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
"Error parsing import identifier",
|
||||
fmt.Sprintf("The import identifier '%s' is not valid: %s", req.ID, err.Error()),
|
||||
)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(consts.FieldMount), mount)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(consts.FieldName), roleName)...)
|
||||
|
||||
ns := os.Getenv(consts.EnvVarVaultNamespaceImport)
|
||||
if ns != "" {
|
||||
tflog.Info(
|
||||
ctx,
|
||||
fmt.Sprintf("Environment variable %s set, attempting TF state import", consts.EnvVarVaultNamespaceImport),
|
||||
map[string]any{consts.FieldNamespace: ns},
|
||||
)
|
||||
resp.Diagnostics.Append(
|
||||
resp.State.SetAttribute(ctx, path.Root(consts.FieldNamespace), ns)...,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) path(data *SpiffeAuthRoleModel) (string, error) {
|
||||
mount := data.Mount.ValueString()
|
||||
name := data.Name.ValueString()
|
||||
if mount == "" || name == "" {
|
||||
return "", fmt.Errorf("mount and name are required fields got mount: %q name: %q", mount, name)
|
||||
}
|
||||
return fmt.Sprintf("auth/%s/role/%s", mount, name), nil
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) getApiModel(ctx context.Context, data *SpiffeAuthRoleModel) (map[string]any, diag.Diagnostics) {
|
||||
apiModel := SpiffeRoleAPIModel{}
|
||||
|
||||
var workloadIdPatterns []string
|
||||
if err := data.WorkloadIDPatterns.ElementsAs(ctx, &workloadIdPatterns, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiModel.WorkloadIDPatterns = workloadIdPatterns
|
||||
|
||||
apiModel.DisplayName = data.DisplayName.ValueString()
|
||||
|
||||
if diagErr := token.PopulateTokenAPIFromModel(ctx, &data.TokenModel, &apiModel.TokenAPIModel); diagErr.HasError() {
|
||||
return nil, diagErr
|
||||
}
|
||||
|
||||
var vaultRequest map[string]any
|
||||
if err := mapstructure.Decode(apiModel, &vaultRequest); err != nil {
|
||||
return nil, diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("Failed to decode SPIFFE role API model to map", err.Error()),
|
||||
}
|
||||
}
|
||||
|
||||
return vaultRequest, nil
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) populateDataModelFromApi(ctx context.Context, role *SpiffeAuthRoleModel, resp *api.Secret) diag.Diagnostics {
|
||||
if resp == nil || resp.Data == nil {
|
||||
return diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("Missing data in API response", "The API response or response data was nil."),
|
||||
}
|
||||
}
|
||||
|
||||
var readResp SpiffeRoleAPIModel
|
||||
if err := model.ToAPIModel(resp.Data, &readResp); err != nil {
|
||||
return diag.Diagnostics{
|
||||
diag.NewErrorDiagnostic("Unable to translate Vault response data", err.Error()),
|
||||
}
|
||||
}
|
||||
|
||||
if len(readResp.WorkloadIDPatterns) > 0 {
|
||||
wkldIdPatterns, listErr := types.ListValueFrom(ctx, types.StringType, readResp.WorkloadIDPatterns)
|
||||
if listErr != nil {
|
||||
return listErr
|
||||
}
|
||||
role.WorkloadIDPatterns = wkldIdPatterns
|
||||
}
|
||||
|
||||
role.DisplayName = types.StringValue(readResp.DisplayName)
|
||||
|
||||
return token.PopulateTokenModelFromAPI(ctx, &role.TokenModel, &readResp.TokenAPIModel)
|
||||
}
|
||||
|
||||
func (s *SpiffeAuthRoleResource) extractSpiffeRoleIdentifiers(id string) (string, string, error) {
|
||||
if id == "" {
|
||||
return "", "", fmt.Errorf("import identifier cannot be empty")
|
||||
}
|
||||
// Trim leading slash if present
|
||||
id = strings.Trim(id, "/")
|
||||
|
||||
if !roleNameRegexp.MatchString(id) {
|
||||
return "", "", fmt.Errorf("import identifier must be of the form 'auth/<mount>/role/<rolename>', "+
|
||||
"namespace can be specified using the env var %s", consts.EnvVarVaultNamespaceImport)
|
||||
}
|
||||
|
||||
matches := roleNameRegexp.FindStringSubmatch(id)
|
||||
if len(matches) != 3 {
|
||||
return "", "", fmt.Errorf("import identifier must be of the form 'auth/<mount>/role/<rolename>', "+
|
||||
"namespace can be specified using the env var %s", consts.EnvVarVaultNamespaceImport)
|
||||
}
|
||||
|
||||
mount := strings.TrimSpace(matches[1])
|
||||
if mount == "" {
|
||||
return "", "", fmt.Errorf("mount cannot be empty")
|
||||
}
|
||||
|
||||
roleName := strings.TrimSpace(matches[2])
|
||||
if roleName == "" {
|
||||
return "", "", fmt.Errorf("role name cannot be empty")
|
||||
}
|
||||
|
||||
return mount, roleName, nil
|
||||
}
|
||||
202
internal/vault/auth/spiffe/spiffe_role_resource_test.go
Normal file
202
internal/vault/auth/spiffe/spiffe_role_resource_test.go
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
package spiffe_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
"github.com/hashicorp/terraform-plugin-testing/plancheck"
|
||||
"github.com/hashicorp/terraform-plugin-testing/terraform"
|
||||
"github.com/hashicorp/terraform-provider-vault/acctestutil"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/provider"
|
||||
"github.com/hashicorp/terraform-provider-vault/internal/providertest"
|
||||
)
|
||||
|
||||
// TestAccSpiffeAuthRole tests the spiffe auth role resource
|
||||
func TestAccSpiffeAuthRole(t *testing.T) {
|
||||
mount := acctest.RandomWithPrefix("spiffe-mount")
|
||||
resourceAddress := "vault_spiffe_auth_backend_role.spiffe_role"
|
||||
|
||||
workloadIds := []string{"/+/test/*", "/example/*"}
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() {
|
||||
acctestutil.TestEntPreCheck(t)
|
||||
acctestutil.SkipIfAPIVersionLT(t, provider.VaultVersion121)
|
||||
},
|
||||
ProtoV5ProviderFactories: providertest.ProtoV5ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
// Test the simplest form of a role
|
||||
{
|
||||
Config: spiffeRoleConfig(mount, workloadIds),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet(resourceAddress, "mount"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "name", "example-role"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.#", "2"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.0", workloadIds[0]),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.1", workloadIds[1]),
|
||||
),
|
||||
},
|
||||
// Test updating the role to have a different workload id patterns (workload id can't be empty)
|
||||
{
|
||||
Config: spiffeRoleConfig(mount, []string{workloadIds[1]}),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet(resourceAddress, "mount"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "name", "example-role"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.#", "1"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.0", workloadIds[1]),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_ttl"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_max_ttl"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_policies"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_bound_cidrs"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_explicit_max_ttl"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_no_default_policy"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_num_uses"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_period"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_type", "default"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "alias_metadata"),
|
||||
),
|
||||
},
|
||||
// Test updating all the token fields
|
||||
{
|
||||
Config: spiffeRoleWithTokenConfig(mount, []string{workloadIds[1]}),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet(resourceAddress, "mount"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "name", "example-role"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.#", "1"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.0", workloadIds[1]),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_ttl", "3600"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_max_ttl", "7200"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_policies.#", "1"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_policies.0", "example"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_bound_cidrs.#", "1"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_bound_cidrs.0", "127.0.0.1"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_explicit_max_ttl", "10800"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_no_default_policy", "true"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_num_uses", "3"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_period", "60"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_type", "service"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "alias_metadata.%", "2"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "alias_metadata.spiffe_workload", "my-workload-id"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "alias_metadata.metadata-key", "metadata-value"),
|
||||
),
|
||||
},
|
||||
// Test that we flush back to a simpler role
|
||||
{
|
||||
Config: spiffeRoleConfig(mount, workloadIds),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet(resourceAddress, "mount"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "name", "example-role"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.#", "2"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.0", workloadIds[0]),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "workload_id_patterns.1", workloadIds[1]),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_ttl"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_max_ttl"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_policies"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_bound_cidrs"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_explicit_max_ttl"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_no_default_policy"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_num_uses"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "token_period"),
|
||||
resource.TestCheckResourceAttr(resourceAddress, "token_type", "default"),
|
||||
resource.TestCheckNoResourceAttr(resourceAddress, "alias_metadata"),
|
||||
),
|
||||
},
|
||||
// Test importing
|
||||
{
|
||||
ResourceName: resourceAddress,
|
||||
ImportState: true,
|
||||
ImportStateIdFunc: testAccSpiffeAuthRoleImportStateIdFunc(resourceAddress),
|
||||
ImportStateVerify: true,
|
||||
ImportStateVerifyIdentifierAttribute: "mount",
|
||||
},
|
||||
// Test deleting the role
|
||||
{
|
||||
Config: spiffeRoleConfigNoRole(mount),
|
||||
ConfigPlanChecks: resource.ConfigPlanChecks{
|
||||
PreApply: []plancheck.PlanCheck{
|
||||
plancheck.ExpectNonEmptyPlan(),
|
||||
plancheck.ExpectResourceAction(resourceAddress, plancheck.ResourceActionDestroy),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccSpiffeAuthRoleImportStateIdFunc(resourceName string) resource.ImportStateIdFunc {
|
||||
return func(s *terraform.State) (string, error) {
|
||||
rs, ok := s.RootModule().Resources[resourceName]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("not found: %s", resourceName)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("auth/%s/role/%s", rs.Primary.Attributes["mount"], rs.Primary.Attributes["name"]), nil
|
||||
}
|
||||
}
|
||||
|
||||
func spiffeRoleConfigNoRole(mount string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "vault_auth_backend" "spiffe_mount" {
|
||||
type = "spiffe"
|
||||
path = "%s"
|
||||
|
||||
tune {
|
||||
passthrough_request_headers = ["Authorization"]
|
||||
}
|
||||
}
|
||||
`, mount)
|
||||
}
|
||||
|
||||
func spiffeRoleConfig(mount string, workloadIds []string) string {
|
||||
var formattedWorkload string
|
||||
if len(workloadIds) > 0 {
|
||||
formattedWorkload = "\"" + strings.Join(workloadIds, "\", \"") + "\""
|
||||
}
|
||||
|
||||
baseMountTf := spiffeRoleConfigNoRole(mount)
|
||||
|
||||
return fmt.Sprintf(`
|
||||
%s
|
||||
|
||||
resource "vault_spiffe_auth_backend_role" "spiffe_role" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
name = "example-role"
|
||||
workload_id_patterns = [%s]
|
||||
}
|
||||
`, baseMountTf, formattedWorkload)
|
||||
}
|
||||
|
||||
func spiffeRoleWithTokenConfig(mount string, workloadIds []string) string {
|
||||
var formattedWorkload string
|
||||
if len(workloadIds) > 0 {
|
||||
formattedWorkload = "\"" + strings.Join(workloadIds, "\", \"") + "\""
|
||||
}
|
||||
|
||||
baseMountTf := spiffeRoleConfigNoRole(mount)
|
||||
|
||||
return fmt.Sprintf(`
|
||||
%s
|
||||
|
||||
resource "vault_spiffe_auth_backend_role" "spiffe_role" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
name = "example-role"
|
||||
workload_id_patterns = [%s]
|
||||
token_ttl = 3600
|
||||
token_max_ttl = 7200
|
||||
token_policies = ["example"]
|
||||
token_bound_cidrs = ["127.0.0.1"]
|
||||
token_explicit_max_ttl = 10800
|
||||
token_no_default_policy = true
|
||||
token_num_uses = 3
|
||||
token_period = 60
|
||||
token_type = "service"
|
||||
alias_metadata = {
|
||||
"spiffe_workload" = "my-workload-id",
|
||||
"metadata-key" = "metadata-value"
|
||||
}
|
||||
}
|
||||
`, baseMountTf, formattedWorkload)
|
||||
}
|
||||
|
|
@ -45,22 +45,30 @@ const (
|
|||
EnvVarTfAccEnt = "TF_ACC_ENTERPRISE"
|
||||
)
|
||||
|
||||
// Deprecated: use acctestutil.TestAccPreCheck instead, this is here for
|
||||
// backwards compatibility.
|
||||
func TestAccPreCheck(t *testing.T) {
|
||||
t.Helper()
|
||||
FatalTestEnvUnset(t, api.EnvVaultAddress, api.EnvVaultToken)
|
||||
}
|
||||
|
||||
// Deprecated: use acctestutil.TestEntPreCheck instead, this is here for
|
||||
// backwards compatibility.
|
||||
func TestEntPreCheck(t *testing.T) {
|
||||
t.Helper()
|
||||
SkipTestAccEnt(t)
|
||||
TestAccPreCheck(t)
|
||||
}
|
||||
|
||||
// Deprecated: use acctestutil.SkipTestAcc instead, this is here for
|
||||
// backwards compatibility.
|
||||
func SkipTestAcc(t *testing.T) {
|
||||
t.Helper()
|
||||
SkipTestEnvUnset(t, resource.EnvTfAcc)
|
||||
}
|
||||
|
||||
// Deprecated: use acctestutil.SkipTestAccEnt instead, this is here for
|
||||
// backwards compatibility.
|
||||
func SkipTestAccEnt(t *testing.T) {
|
||||
t.Helper()
|
||||
SkipTestEnvUnset(t, EnvVarTfAccEnt)
|
||||
|
|
|
|||
91
website/docs/r/spiffe_auth_backend_config.html.md
Normal file
91
website/docs/r/spiffe_auth_backend_config.html.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
layout: "vault"
|
||||
page_title: "Vault: vault_spiffe_auth_backend_config resource"
|
||||
sidebar_current: "docs-vault-resource-vault_spiffe_auth_backend_config"
|
||||
description: |-
|
||||
Update the main configuration of the SPIFFE auth backend in Vault.
|
||||
---
|
||||
|
||||
# vault\_spiffe\_auth\_backend\_config
|
||||
|
||||
Configure the SPIFFE trust domain and associated trust bundle for the backend
|
||||
|
||||
~> **Important** All data provided in the resource configuration will be
|
||||
written in cleartext to state and plan files generated by Terraform, and
|
||||
will appear in the console output when Terraform runs. Protect these
|
||||
artifacts accordingly. See
|
||||
[the main provider documentation](../index.html)
|
||||
for more details.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```hcl
|
||||
resource "vault_auth_backend" "spiffe_mount" {
|
||||
type = "spiffe"
|
||||
path = "spiffe"
|
||||
|
||||
tune {
|
||||
passthrough_request_headers = ["Authorization"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_spiffe_auth_backend_config" "spiffe_config" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
trust_domain = "example.org"
|
||||
profile = "https_web_bundle"
|
||||
endpoint_url = "https://example.org:8443/"
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `namespace` - (Optional) The namespace to provision the resource in.
|
||||
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*.
|
||||
|
||||
* `mount` - (Required) The PKI secret backend the resource belongs to.
|
||||
|
||||
* `trust_domain` - (Required) The SPIFFE trust domain used by the backend.
|
||||
|
||||
* `profile` - (Required) Sets the profile type and fetch mechanism for the profile
|
||||
used to fetch the trust bundle. Must be one of:
|
||||
* https_spiffe_bundle - fetch the trust bundle in JWKS format from a SPIFFE endpoint.
|
||||
* https_web_bundle - fetch the trust bundle in JWKS format from an HTTPS endpoint.
|
||||
* https_web_pem - fetch a valid X.509 certificate as the trust bundle from an HTTPS endpoint.
|
||||
* static - use the trust bundle explicitly configured in the profile definition.
|
||||
|
||||
* `audience` - (Optional) A list of allowed audience values for JWT based SVIDs.
|
||||
|
||||
* `defer_bundle_fetch` - (Optional) If true, tells Vault not to fetch the remote trust
|
||||
bundle to validate the configuration
|
||||
|
||||
* `bundle` - (Optional) A PEM encoded X.509 certificate or a JWKS document based on what
|
||||
the `profile` argument is set.
|
||||
|
||||
* `endpoint_url` - (Optional) The URL to fetch the trust bundle from. Required if the
|
||||
`profile` argument is set to `https_spiffe_bundle`, `https_web_bundle`, or
|
||||
`https_web_pem`.
|
||||
|
||||
* `endpoint_root_ca_truststore_pem` - (Optional) A PEM encoded CA certificate to use
|
||||
to validate the TLS connection to the `endpoint_url` when the `profile` argument
|
||||
is set to `https_web_bundle` or `https_web_pem`.
|
||||
|
||||
* `endpoint_spiffe_id` - (Optional) The SPIFFE ID to expect in the TLS certificate when
|
||||
the `profile` argument is set to `https_spiffe_bundle`.
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
No additional attributes are exported by this resource.
|
||||
|
||||
## Import
|
||||
|
||||
The SPIFFE config can be imported using the resource's `id`.
|
||||
In the case of the example above the `id` would be `auth/spiffe/config`,
|
||||
where the `spiffe` component is the resource's `mount`, e.g.
|
||||
|
||||
```
|
||||
$ terraform import vault_spiffe_auth_backend_config.example auth/spiffe/config
|
||||
```
|
||||
125
website/docs/r/spiffe_auth_backend_role.html.md
Normal file
125
website/docs/r/spiffe_auth_backend_role.html.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
layout: "vault"
|
||||
page_title: "Vault: vault_spiffe_auth_backend_role resource"
|
||||
sidebar_current: "docs-vault-resource-vault_spiffe_auth_backend_role"
|
||||
description: |-
|
||||
Manage a named role within a SPIFFE auth backend that maps SPIFFE IDs to Vault policies.
|
||||
---
|
||||
|
||||
# vault\_spiffe\_auth\_backend\_role
|
||||
|
||||
Manage a named role within a SPIFFE auth backend. The role defines a mapping
|
||||
from SPIFFE IDs to Vault policies along with other parameters that influence
|
||||
the token that gets created upon successful authentication.
|
||||
|
||||
~> **Important** All data provided in the resource configuration will be
|
||||
written in cleartext to state and plan files generated by Terraform, and
|
||||
will appear in the console output when Terraform runs. Protect these
|
||||
artifacts accordingly. See
|
||||
[the main provider documentation](../index.html)
|
||||
for more details.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```hcl
|
||||
resource "vault_auth_backend" "spiffe_mount" {
|
||||
type = "spiffe"
|
||||
path = "spiffe"
|
||||
|
||||
tune {
|
||||
passthrough_request_headers = ["Authorization"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_spiffe_auth_backend_role" "spiffe_role" {
|
||||
mount = vault_auth_backend.spiffe_mount.path
|
||||
name = "example-role"
|
||||
workload_id_patterns = ["/env/+/svc/web", "/env/+/svc/db"]
|
||||
token_ttl = 3600
|
||||
token_max_ttl = 7200
|
||||
token_policies = ["example"]
|
||||
token_bound_cidrs = ["127.0.0.1"]
|
||||
token_explicit_max_ttl = 10800
|
||||
token_no_default_policy = true
|
||||
token_num_uses = 1
|
||||
token_period = 60
|
||||
token_type = "service"
|
||||
alias_metadata = {
|
||||
"metadata-key" = "metadata-value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `namespace` - (Optional) The namespace to provision the resource in.
|
||||
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*.
|
||||
|
||||
* `mount` - (Required) The PKI secret backend the resource belongs to.
|
||||
|
||||
* `workload_id_patterns` - (Required) A comma separated list of patterns that match an
|
||||
incoming workload ID within the SVID document presented by the client.
|
||||
|
||||
* `display_name` - (Optional) The human-readable name for tokens issued when
|
||||
authenticating against the role. Defaults to the value provided for `mount`.
|
||||
|
||||
### Common Token Arguments
|
||||
|
||||
These arguments are common across several Authentication Token resources since Vault 1.2.
|
||||
|
||||
* `token_ttl` - (Optional) The incremental lifetime for generated tokens in number of seconds.
|
||||
Its current value will be referenced at renewal time.
|
||||
|
||||
* `token_max_ttl` - (Optional) The maximum lifetime for generated tokens in number of seconds.
|
||||
Its current value will be referenced at renewal time.
|
||||
|
||||
* `token_period` - (Optional) If set, indicates that the
|
||||
token generated using this role should never expire. The token should be renewed within the
|
||||
duration specified by this value. At each renewal, the token's TTL will be set to the
|
||||
value of this field. Specified in seconds.
|
||||
|
||||
* `token_policies` - (Optional) List of policies to encode onto generated tokens. Depending
|
||||
on the auth method, this list may be supplemented by user/group/other values.
|
||||
|
||||
* `token_bound_cidrs` - (Optional) List of CIDR blocks; if set, specifies blocks of IP
|
||||
addresses which can authenticate successfully, and ties the resulting token to these blocks
|
||||
as well.
|
||||
|
||||
* `token_explicit_max_ttl` - (Optional) If set, will encode an
|
||||
[explicit max TTL](https://www.vaultproject.io/docs/concepts/tokens.html#token-time-to-live-periodic-tokens-and-explicit-max-ttls)
|
||||
onto the token in number of seconds. This is a hard cap even if `token_ttl` and
|
||||
`token_max_ttl` would otherwise allow a renewal.
|
||||
|
||||
* `token_no_default_policy` - (Optional) If set, the default policy will not be set on
|
||||
generated tokens; otherwise it will be added to the policies set in token_policies.
|
||||
|
||||
* `token_num_uses` - (Optional) The [maximum number](https://developer.hashicorp.com/vault/api-docs/auth/saml#token_num_uses)
|
||||
of times a generated token may be used (within its lifetime); 0 means unlimited.
|
||||
|
||||
* `token_type` - (Optional) The type of token that should be generated. Can be `service`,
|
||||
`batch`, or `default` to use the mount's tuned default (which unless changed will be
|
||||
`service` tokens). For token store roles, there are two additional possibilities:
|
||||
`default-service` and `default-batch` which specify the type to return unless the client
|
||||
requests a different type at generation time.
|
||||
|
||||
* `alias_metadata` - (Optional) A mapping of string key-value pairs that will be set as
|
||||
metadata on the token's alias. This can be used to store information about the
|
||||
authenticated entity.
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
No additional attributes are exported by this resource.
|
||||
|
||||
## Import
|
||||
|
||||
The SPIFFE role can be imported using the resource's `id`.
|
||||
In the case of the example above the `id` would be `auth/spiffe/role/example-role`,
|
||||
where the `spiffe` component is the resource's `mount`, e.g.
|
||||
|
||||
```
|
||||
$ terraform import vault_spiffe_auth_backend_role.example-role auth/spiffe/role/example-role
|
||||
```
|
||||
|
|
@ -775,7 +775,13 @@
|
|||
<a href="/docs/providers/vault/r/secrets_sync_association.html">vault_secrets_sync_association</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-vault-resource-vault_spiffe_auth_backend_config") %>>
|
||||
<a href="/docs/providers/vault/r/spiffe_auth_backend_config.html">vault_spiffe_auth_backend_config</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-vault-resource-vault_spiffe_auth_backend_role") %>>
|
||||
<a href="/docs/providers/vault/r/spiffe_auth_backend_role.html">vault_spiffe_auth_backend_role</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue