Skip to content

Commit 9f993e9

Browse files
authored
Add ephemeral resource for approle_auth_backend_role_secret_id and add write only field for secret_id in vault_approle_auth_backend_login resource (#2745)
1 parent fb4abc3 commit 9f993e9

9 files changed

Lines changed: 683 additions & 55 deletions

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
FEATURES:
44

5-
* Add Kubernetes service account token ephemeral resource `vault_kubernetes_service_account_token`: ([#2712](https://github.com/hashicorp/terraform-provider-vault/pull/2712))
5+
* **New Ephemeral Resource**: `vault_approle_auth_backend_role_secret_id` - Generate AppRole SecretIDs on-demand with automatic cleanup. Requires Terraform 1.10+.([#2745](https://github.com/hashicorp/terraform-provider-vault/pull/2745))
6+
* **New Ephemeral Resource**: Add Kubernetes service account token ephemeral resource `vault_kubernetes_service_account_token`: ([#2712](https://github.com/hashicorp/terraform-provider-vault/pull/2712))
67

78
IMPROVEMENTS:
89

10+
* `vault_approle_auth_backend_login`: Add write-only fields `secret_id_wo` and `secret_id_wo_version` to support ephemeral SecretID values without persisting them in state.([#2745](https://github.com/hashicorp/terraform-provider-vault/pull/2745))
911
* `vault_mfa_totp`: Add support for `max_validation_attempts` field to configure the maximum number of consecutive failed validation attempts allowed. ([#2751](https://github.com/hashicorp/terraform-provider-vault/pull/2751))
1012
* `vault_mongodbatlas_secret_backend`: Add support for write-only private key fields (`private_key_wo`, `private_key_wo_version`) to prevent sensitive credentials from being stored in Terraform state. ([#2741](https://github.com/hashicorp/terraform-provider-vault/pull/2741))
1113
* `vault_consul_secret_backend`: Add support for write-only fields (`token_wo`, `token_wo_version`, `client_key_wo`, `client_key_wo_version`) to prevent sensitive credentials from being stored in Terraform state. ([#2730](https://github.com/hashicorp/terraform-provider-vault/pull/2730))

internal/consts/consts.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,8 @@ const (
464464
FieldIdentityTokenKey = "identity_token_key"
465465
FieldCIDRList = "cidr_list"
466466
FieldSecretID = "secret_id"
467+
FieldSecretIDWO = "secret_id_wo"
468+
FieldSecretIDWOVersion = "secret_id_wo_version"
467469
FieldWrappingToken = "wrapping_token"
468470
FieldWithWrappedAccessor = "with_wrapped_accessor"
469471
FieldExternalID = "external_id"

internal/provider/fwprovider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
1212
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
13+
ephemeralauth "github.com/hashicorp/terraform-provider-vault/internal/vault/auth/ephemeral"
1314
"github.com/hashicorp/terraform-provider-vault/internal/vault/auth/spiffe"
1415
"github.com/hashicorp/terraform-provider-vault/internal/vault/secrets/azure"
1516
ephemeralsecrets "github.com/hashicorp/terraform-provider-vault/internal/vault/secrets/ephemeral"
@@ -243,6 +244,7 @@ func (p *fwprovider) EphemeralResources(_ context.Context) []func() ephemeral.Ep
243244
ephemeralsecrets.NewGCPOAuth2AccessTokenEphemeralResource,
244245
ephemeralsecrets.NewAWSAccessCredentialsEphemeralSecretResource,
245246
ephemeralsecrets.NewAWSStaticAccessCredentialsEphemeralSecretResource,
247+
ephemeralauth.NewApproleAuthBackendRoleSecretIDEphemeralResource,
246248
ephemeralsecrets.NewKubernetesServiceAccountTokenEphemeralResource,
247249
}
248250

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package ephemeralauth
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
12+
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
15+
"github.com/hashicorp/terraform-provider-vault/internal/consts"
16+
"github.com/hashicorp/terraform-provider-vault/internal/framework/base"
17+
"github.com/hashicorp/terraform-provider-vault/internal/framework/client"
18+
"github.com/hashicorp/terraform-provider-vault/internal/framework/errutil"
19+
"github.com/hashicorp/terraform-provider-vault/internal/framework/model"
20+
)
21+
22+
// Ensure the implementation satisfies the ephemeral.EphemeralResource interface
23+
var _ ephemeral.EphemeralResource = &ApproleAuthBackendRoleSecretIDEphemeralResource{}
24+
25+
// NewApproleAuthBackendRoleSecretIDEphemeralResource returns the implementation for this resource to be
26+
// imported by the Terraform Plugin Framework provider
27+
var NewApproleAuthBackendRoleSecretIDEphemeralResource = func() ephemeral.EphemeralResource {
28+
return &ApproleAuthBackendRoleSecretIDEphemeralResource{}
29+
}
30+
31+
// ApproleAuthBackendRoleSecretIDEphemeralResource implements the methods that define this resource
32+
type ApproleAuthBackendRoleSecretIDEphemeralResource struct {
33+
base.EphemeralResourceWithConfigure
34+
}
35+
36+
// ApproleAuthBackendRoleSecretIDEphemeralModel describes the Terraform resource data model to match the
37+
// resource schema.
38+
type ApproleAuthBackendRoleSecretIDEphemeralModel struct {
39+
// common fields to all ephemeral resources
40+
base.BaseModelEphemeral
41+
42+
// fields specific to this resource
43+
Backend types.String `tfsdk:"backend"`
44+
RoleName types.String `tfsdk:"role_name"`
45+
CIDRList types.Set `tfsdk:"cidr_list"`
46+
Metadata types.String `tfsdk:"metadata"`
47+
TTL types.Int64 `tfsdk:"ttl"`
48+
NumUses types.Int64 `tfsdk:"num_uses"`
49+
SecretID types.String `tfsdk:"secret_id"`
50+
Accessor types.String `tfsdk:"accessor"`
51+
}
52+
53+
// ApproleAuthBackendRoleSecretIDAPIModel describes the Vault API data model.
54+
type ApproleAuthBackendRoleSecretIDAPIModel struct {
55+
SecretID string `json:"secret_id" mapstructure:"secret_id"`
56+
SecretIDAccessor string `json:"secret_id_accessor" mapstructure:"secret_id_accessor"`
57+
}
58+
59+
// Schema defines this resource's schema which is the data that is available in
60+
// the resource's configuration, plan, and state
61+
//
62+
// https://developer.hashicorp.com/terraform/plugin/framework/resources#schema-method
63+
func (r *ApproleAuthBackendRoleSecretIDEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
64+
resp.Schema = schema.Schema{
65+
Attributes: map[string]schema.Attribute{
66+
consts.FieldBackend: schema.StringAttribute{
67+
MarkdownDescription: "Unique name of the auth backend to configure.",
68+
Optional: true,
69+
Computed: true,
70+
},
71+
consts.FieldRoleName: schema.StringAttribute{
72+
MarkdownDescription: "Name of the role.",
73+
Required: true,
74+
},
75+
consts.FieldCIDRList: schema.SetAttribute{
76+
MarkdownDescription: "List of CIDR blocks that can log in using the SecretID.",
77+
ElementType: types.StringType,
78+
Optional: true,
79+
},
80+
consts.FieldMetadata: schema.StringAttribute{
81+
MarkdownDescription: "JSON-encoded secret data.",
82+
Optional: true,
83+
},
84+
consts.FieldTTL: schema.Int64Attribute{
85+
MarkdownDescription: "The TTL duration of the SecretID in seconds.",
86+
Optional: true,
87+
},
88+
consts.FieldNumUses: schema.Int64Attribute{
89+
MarkdownDescription: "The number of uses for the secret-id.",
90+
Optional: true,
91+
},
92+
consts.FieldSecretID: schema.StringAttribute{
93+
MarkdownDescription: "The generated SecretID.",
94+
Computed: true,
95+
Sensitive: true,
96+
},
97+
consts.FieldAccessor: schema.StringAttribute{
98+
MarkdownDescription: "The accessor for the SecretID.",
99+
Computed: true,
100+
},
101+
},
102+
MarkdownDescription: "Provides an ephemeral resource to generate an AppRole SecretID from Vault.",
103+
}
104+
105+
base.MustAddBaseEphemeralSchema(&resp.Schema)
106+
}
107+
108+
// Metadata sets the full name for this resource
109+
func (r *ApproleAuthBackendRoleSecretIDEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
110+
resp.TypeName = req.ProviderTypeName + "_approle_auth_backend_role_secret_id"
111+
}
112+
113+
func (r *ApproleAuthBackendRoleSecretIDEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
114+
var data ApproleAuthBackendRoleSecretIDEphemeralModel
115+
116+
// Read Terraform configuration data into the model
117+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
118+
119+
if resp.Diagnostics.HasError() {
120+
return
121+
}
122+
123+
// Set default backend if not provided
124+
if data.Backend.IsNull() || data.Backend.IsUnknown() {
125+
data.Backend = types.StringValue("approle")
126+
}
127+
128+
c, err := client.GetClient(ctx, r.Meta(), data.Namespace.ValueString())
129+
if err != nil {
130+
resp.Diagnostics.AddError(errutil.ClientConfigureErr(err))
131+
return
132+
}
133+
134+
backend := strings.Trim(data.Backend.ValueString(), "/")
135+
role := strings.Trim(data.RoleName.ValueString(), "/")
136+
path := fmt.Sprintf("auth/%s/role/%s/secret-id", backend, role)
137+
138+
// Build the request data
139+
requestData := make(map[string]interface{})
140+
141+
// Handle CIDR list
142+
if !data.CIDRList.IsNull() && !data.CIDRList.IsUnknown() {
143+
var cidrs []string
144+
resp.Diagnostics.Append(data.CIDRList.ElementsAs(ctx, &cidrs, false)...)
145+
if resp.Diagnostics.HasError() {
146+
return
147+
}
148+
if len(cidrs) > 0 {
149+
requestData[consts.FieldCIDRList] = strings.Join(cidrs, ",")
150+
}
151+
}
152+
153+
// Handle metadata
154+
if !data.Metadata.IsNull() && !data.Metadata.IsUnknown() {
155+
requestData[consts.FieldMetadata] = data.Metadata.ValueString()
156+
}
157+
158+
// Handle TTL
159+
if !data.TTL.IsNull() && !data.TTL.IsUnknown() {
160+
requestData[consts.FieldTTL] = data.TTL.ValueInt64()
161+
}
162+
163+
// Handle num_uses
164+
if !data.NumUses.IsNull() && !data.NumUses.IsUnknown() {
165+
requestData[consts.FieldNumUses] = data.NumUses.ValueInt64()
166+
}
167+
168+
secretResp, err := c.Logical().WriteWithContext(ctx, path, requestData)
169+
if err != nil {
170+
resp.Diagnostics.AddError(
171+
"Error generating AppRole SecretID",
172+
fmt.Sprintf("Could not generate SecretID at path %s: %s", path, err),
173+
)
174+
return
175+
}
176+
177+
if secretResp == nil || secretResp.Data == nil {
178+
resp.Diagnostics.AddError(
179+
"Empty response from Vault",
180+
fmt.Sprintf("No data returned when generating SecretID at path %s", path),
181+
)
182+
return
183+
}
184+
185+
var readResp ApproleAuthBackendRoleSecretIDAPIModel
186+
err = model.ToAPIModel(secretResp.Data, &readResp)
187+
if err != nil {
188+
resp.Diagnostics.AddError("Unable to translate Vault response data", err.Error())
189+
return
190+
}
191+
192+
data.SecretID = types.StringValue(readResp.SecretID)
193+
data.Accessor = types.StringValue(readResp.SecretIDAccessor)
194+
195+
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
196+
197+
// Store the accessor and backend info for cleanup in Close
198+
resp.Private.SetKey(ctx, consts.FieldAccessor, []byte(readResp.SecretIDAccessor))
199+
resp.Private.SetKey(ctx, consts.FieldBackend, []byte(backend))
200+
resp.Private.SetKey(ctx, consts.FieldRole, []byte(role))
201+
resp.Private.SetKey(ctx, consts.FieldNamespace, []byte(data.Namespace.ValueString()))
202+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package ephemeralauth_test
5+
6+
import (
7+
"fmt"
8+
"regexp"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
12+
"github.com/hashicorp/terraform-plugin-testing/echoprovider"
13+
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
14+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
15+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
16+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
17+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
18+
"github.com/hashicorp/terraform-provider-vault/acctestutil"
19+
"github.com/hashicorp/terraform-provider-vault/internal/consts"
20+
"github.com/hashicorp/terraform-provider-vault/internal/providertest"
21+
)
22+
23+
// TestAccApproleAuthBackendRoleSecretID confirms that a dynamic AppRole SecretID
24+
// can be generated from Vault for a created AppRole role
25+
//
26+
// Uses the Echo Provider to test values set in ephemeral resources
27+
// see documentation here for more details:
28+
// https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/ephemeral-resources#using-echo-provider-in-acceptance-tests
29+
func TestAccApproleAuthBackendRoleSecretID(t *testing.T) {
30+
acctestutil.SkipTestAcc(t)
31+
backend := acctest.RandomWithPrefix("approle")
32+
roleName := acctest.RandomWithPrefix("role")
33+
34+
// Regex to ensure secret_id and accessor are set to some value (UUIDs with hyphens)
35+
expectedSecretIDRegex, err := regexp.Compile("^[a-f0-9-]+$")
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
expectedAccessorRegex, err := regexp.Compile("^[a-f0-9-]+$")
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
44+
resource.Test(t, resource.TestCase{
45+
PreCheck: func() { acctestutil.TestAccPreCheck(t) },
46+
// Include the provider we want to test (v5)
47+
ProtoV5ProviderFactories: providertest.ProtoV5ProviderFactories,
48+
// Include `echo` as a v6 provider from `terraform-plugin-testing`
49+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
50+
"echo": echoprovider.NewProviderServer(),
51+
},
52+
Steps: []resource.TestStep{
53+
{
54+
Config: testApproleAuthBackendRoleSecretIDConfig(backend, roleName),
55+
ConfigStateChecks: []statecheck.StateCheck{
56+
statecheck.ExpectKnownValue("echo.test_approle", tfjsonpath.New("data").AtMapKey(consts.FieldSecretID), knownvalue.StringRegexp(expectedSecretIDRegex)),
57+
statecheck.ExpectKnownValue("echo.test_approle", tfjsonpath.New("data").AtMapKey(consts.FieldAccessor), knownvalue.StringRegexp(expectedAccessorRegex)),
58+
},
59+
},
60+
},
61+
})
62+
}
63+
64+
func testApproleAuthBackendRoleSecretIDConfig(backend, roleName string) string {
65+
return fmt.Sprintf(`
66+
resource "vault_auth_backend" "approle" {
67+
type = "approle"
68+
path = "%s"
69+
}
70+
71+
resource "vault_approle_auth_backend_role" "role" {
72+
backend = vault_auth_backend.approle.path
73+
role_name = "%s"
74+
}
75+
76+
ephemeral "vault_approle_auth_backend_role_secret_id" "secret" {
77+
backend = vault_auth_backend.approle.path
78+
role_name = vault_approle_auth_backend_role.role.role_name
79+
mount_id = vault_approle_auth_backend_role.role.id
80+
}
81+
82+
provider "echo" {
83+
data = ephemeral.vault_approle_auth_backend_role_secret_id.secret
84+
}
85+
86+
resource "echo" "test_approle" {}
87+
`, backend, roleName)
88+
}

0 commit comments

Comments
 (0)