Skip to content

Commit ee1bfbf

Browse files
siyer-corpZlaticanin
authored andcommitted
Update Azure Static Secrets to use new import endpoint (#2756) (#2884)
* Update Azure Static Secrets to use new import endpoint * update ttl change Co-authored-by: Zlaticanin <60530402+Zlaticanin@users.noreply.github.com>
1 parent d08c8a4 commit ee1bfbf

4 files changed

Lines changed: 81 additions & 33 deletions

File tree

internal/consts/consts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,7 @@ const (
687687
FieldAudience = "audience"
688688
FieldTokenMaxTTL = "token_max_ttl"
689689
FieldTokenPeriod = "token_period"
690+
FieldDeferInitialCreds = "defer_initial_creds"
690691
FieldTokenExplicitMaxTTL = "token_explicit_max_ttl"
691692
FieldTokenNoDefaultPolicy = "token_no_default_policy"
692693
FieldTokenDefaultAudiences = "token_default_audiences"

internal/vault/secrets/azure/azure_static_role_resource.go

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010
"strconv"
1111
"time"
1212

13+
"github.com/hashicorp/terraform-plugin-framework/attr"
1314
"github.com/hashicorp/terraform-plugin-framework/diag"
1415
"github.com/hashicorp/terraform-plugin-framework/path"
1516
"github.com/hashicorp/terraform-plugin-framework/resource"
1617
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault"
1719
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1820
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1921
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -55,6 +57,7 @@ type AzureStaticRoleModel struct {
5557
ClientSecret types.String `tfsdk:"client_secret"`
5658
Expiration types.String `tfsdk:"expiration"`
5759
SkipImportRotation types.Bool `tfsdk:"skip_import_rotation"`
60+
DeferInitialCreds types.Bool `tfsdk:"defer_initial_creds"`
5861
}
5962

6063
// AzureStaticRoleAPIModel describes the Vault API data model.
@@ -87,14 +90,16 @@ func (r *AzureSecretsStaticRoleResource) Schema(_ context.Context, _ resource.Sc
8790
Required: true,
8891
},
8992
consts.FieldTTL: schema.Int64Attribute{
90-
MarkdownDescription: "Timespan of 1 year (31536000) or more during which the role credentials are valid.",
93+
MarkdownDescription: "Timespan of 1 month or more during which the role credentials are valid.",
9194
Optional: true,
9295
Computed: true,
9396
},
9497
consts.FieldMetadata: schema.MapAttribute{
9598
MarkdownDescription: "A map of string key/value pairs that will be stored as metadata on the secret.",
9699
ElementType: types.StringType,
97100
Optional: true,
101+
Computed: true,
102+
Default: mapdefault.StaticValue(types.MapValueMust(types.StringType, map[string]attr.Value{})),
98103
},
99104
consts.FieldSecretID: schema.StringAttribute{
100105
MarkdownDescription: "The secret ID of the Azure password credential you want to import.",
@@ -104,15 +109,22 @@ func (r *AzureSecretsStaticRoleResource) Schema(_ context.Context, _ resource.Sc
104109
MarkdownDescription: "The plaintext secret value of the credential you want to import.",
105110
Optional: true,
106111
Sensitive: true,
112+
WriteOnly: true, // Prevents the value from being shown in state
107113
},
108114
consts.FieldExpiration: schema.StringAttribute{
109115
MarkdownDescription: "A future expiration time for the imported credential, in RFC3339 format.",
110116
Optional: true,
117+
DeprecationMessage: "This field is deprecated and will be removed in a future release. " +
118+
"Vault will always read the expiration from Azure.",
111119
},
112120
consts.FieldSkipImportRotation: schema.BoolAttribute{
113121
MarkdownDescription: "If true, skip rotation of the client secret on import.",
114122
Optional: true,
115123
},
124+
consts.FieldDeferInitialCreds: schema.BoolAttribute{
125+
MarkdownDescription: "If true, the initial creation of credentials will be deferred until first static-creds read.",
126+
Optional: true,
127+
},
116128
},
117129
MarkdownDescription: "Manage Azure static roles.",
118130
}
@@ -123,7 +135,13 @@ func (r *AzureSecretsStaticRoleResource) Schema(_ context.Context, _ resource.Sc
123135
//
124136
// https://developer.hashicorp.com/terraform/plugin/framework/resources/create
125137
func (r *AzureSecretsStaticRoleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
126-
var data AzureStaticRoleModel
138+
var (
139+
path string
140+
vaultRequest map[string]any
141+
diags diag.Diagnostics
142+
data AzureStaticRoleModel
143+
)
144+
127145
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
128146
if resp.Diagnostics.HasError() {
129147
return
@@ -137,9 +155,19 @@ func (r *AzureSecretsStaticRoleResource) Create(ctx context.Context, req resourc
137155

138156
backend := data.Backend.ValueString()
139157
role := data.Role.ValueString()
140-
path := fmt.Sprintf("%s/%s/%s", backend, staticRolesAffix, role)
141158

142-
vaultRequest, diags := buildVaultRequestFromModel(ctx, &data, true)
159+
// if secretID is set, it's an import create operation using import endpoint
160+
// otherwise, it's a standard create operation
161+
if !data.SecretID.IsNull() && data.SecretID.ValueString() != "" {
162+
// <backend>/static-roles/<role>/import
163+
path = fmt.Sprintf("%s/%s/%s/import", backend, staticRolesAffix, role)
164+
vaultRequest, diags = buildVaultRequestForImportCreate(ctx, &data)
165+
} else {
166+
// <backend>/static-roles/<role>
167+
path = fmt.Sprintf("%s/%s/%s", backend, staticRolesAffix, role)
168+
vaultRequest, diags = buildVaultRequestFromModel(ctx, &data)
169+
}
170+
143171
resp.Diagnostics.Append(diags...)
144172
if resp.Diagnostics.HasError() {
145173
return
@@ -229,7 +257,7 @@ func (r *AzureSecretsStaticRoleResource) Update(ctx context.Context, req resourc
229257
role := data.Role.ValueString()
230258
path := fmt.Sprintf("%s/%s/%s", backend, staticRolesAffix, role)
231259

232-
vaultRequest, diags := buildVaultRequestFromModel(ctx, &data, false)
260+
vaultRequest, diags := buildVaultRequestFromModel(ctx, &data)
233261
resp.Diagnostics.Append(diags...)
234262
if resp.Diagnostics.HasError() {
235263
return
@@ -244,27 +272,52 @@ func (r *AzureSecretsStaticRoleResource) Update(ctx context.Context, req resourc
244272
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
245273
}
246274

247-
func buildVaultRequestFromModel(ctx context.Context, data *AzureStaticRoleModel, includeSkipImport bool) (map[string]any, diag.Diagnostics) {
275+
func buildVaultRequestFromModel(ctx context.Context, data *AzureStaticRoleModel) (map[string]any, diag.Diagnostics) {
248276
var diags diag.Diagnostics
249277

250278
vaultRequest := map[string]any{
251279
consts.FieldApplicationObjectID: data.ApplicationObjectID.ValueString(),
252280
}
253281

254-
fieldMap := map[string]any{
255-
consts.FieldSecretID: data.SecretID.ValueString(),
256-
consts.FieldClientSecret: data.ClientSecret.ValueString(),
257-
consts.FieldExpiration: data.Expiration.ValueString(),
282+
if !data.TTL.IsNull() {
283+
vaultRequest[consts.FieldTTL] = data.TTL.ValueInt64()
258284
}
259285

260-
for k, v := range fieldMap {
261-
if s, ok := v.(string); ok && s != "" {
262-
vaultRequest[k] = s
286+
if !data.Metadata.IsNull() && !data.Metadata.IsUnknown() {
287+
var meta map[string]string
288+
if mdDiags := data.Metadata.ElementsAs(ctx, &meta, false); mdDiags.HasError() {
289+
diags.Append(mdDiags...)
290+
return nil, diags
263291
}
292+
vaultRequest[consts.FieldMetadata] = meta
293+
}
294+
295+
// Prefer defer_initial_creds if true
296+
deferInitial := !data.DeferInitialCreds.IsNull() && data.DeferInitialCreds.ValueBool()
297+
// Legacy alias (back comp) skip_import_rotation == true means defer_initial_creds == true
298+
legacyAlias := !data.SkipImportRotation.IsNull() && data.SkipImportRotation.ValueBool()
299+
300+
if deferInitial || legacyAlias {
301+
vaultRequest[consts.FieldDeferInitialCreds] = true
302+
}
303+
304+
return vaultRequest, diags
305+
}
306+
307+
func buildVaultRequestForImportCreate(ctx context.Context, data *AzureStaticRoleModel) (map[string]any, diag.Diagnostics) {
308+
var diags diag.Diagnostics
309+
310+
req := map[string]any{
311+
consts.FieldApplicationObjectID: data.ApplicationObjectID.ValueString(),
312+
consts.FieldSecretID: data.SecretID.ValueString(),
313+
}
314+
315+
if !data.ClientSecret.IsNull() && data.ClientSecret.ValueString() != "" {
316+
req[consts.FieldClientSecret] = data.ClientSecret.ValueString()
264317
}
265318

266319
if !data.TTL.IsNull() {
267-
vaultRequest[consts.FieldTTL] = data.TTL.ValueInt64()
320+
req[consts.FieldTTL] = data.TTL.ValueInt64()
268321
}
269322

270323
if !data.Metadata.IsNull() && !data.Metadata.IsUnknown() {
@@ -273,15 +326,14 @@ func buildVaultRequestFromModel(ctx context.Context, data *AzureStaticRoleModel,
273326
diags.Append(mdDiags...)
274327
return nil, diags
275328
}
276-
vaultRequest[consts.FieldMetadata] = meta
329+
req[consts.FieldMetadata] = meta
277330
}
278331

279-
// only include on create and only when true
280-
if includeSkipImport && data.SkipImportRotation.ValueBool() {
281-
vaultRequest[consts.FieldSkipImportRotation] = true
332+
if !data.SkipImportRotation.IsNull() && data.SkipImportRotation.ValueBool() {
333+
req[consts.FieldSkipImportRotation] = true
282334
}
283335

284-
return vaultRequest, diags
336+
return req, diags
285337
}
286338

287339
func (r *AzureSecretsStaticRoleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {

internal/vault/secrets/azure/azure_static_role_resource_test.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ func TestAccAzureStaticRole_import(t *testing.T) {
124124

125125
secretID := os.Getenv("AZURE_IMPORT_SECRET_ID")
126126
clientSecret := os.Getenv("AZURE_IMPORT_CLIENT_SECRET")
127-
expiration := os.Getenv("AZURE_IMPORT_EXPIRATION")
128127

129128
if secretID == "" {
130129
t.Skip("AZURE_IMPORT_SECRET_ID must be set to run import workflow test")
@@ -147,27 +146,21 @@ func TestAccAzureStaticRole_import(t *testing.T) {
147146
},
148147
Steps: []resource.TestStep{
149148
{
150-
Config: testAzureStaticRole_importConfig(backend, roleName, conf, secretID, clientSecret, expiration),
149+
Config: testAzureStaticRole_importConfig(backend, roleName, conf, secretID, clientSecret),
151150
ConfigStateChecks: []statecheck.StateCheck{
152151
statecheck.ExpectKnownValue(resName, tfjsonpath.New(consts.FieldBackend), knownvalue.StringExact(backend)),
153152
statecheck.ExpectKnownValue(resName, tfjsonpath.New(consts.FieldRole), knownvalue.StringExact(roleName)),
154153
statecheck.ExpectKnownValue(resName, tfjsonpath.New(consts.FieldApplicationObjectID), knownvalue.StringExact(conf.AppObjectID)),
155154
statecheck.ExpectKnownValue(echoName, tfjsonpath.New("data").AtMapKey(consts.FieldSecretID), knownvalue.StringRegexp(nonEmpty)),
156155
statecheck.ExpectKnownValue(echoName, tfjsonpath.New("data").AtMapKey(consts.FieldClientID), knownvalue.StringRegexp(nonEmpty)),
157156
statecheck.ExpectKnownValue(echoName, tfjsonpath.New("data").AtMapKey(consts.FieldClientSecret), knownvalue.StringRegexp(nonEmpty)),
158-
statecheck.ExpectKnownValue(echoName, tfjsonpath.New("data").AtMapKey(consts.FieldExpiration), knownvalue.StringRegexp(nonEmpty)),
159157
},
160158
},
161159
},
162160
})
163161
}
164162

165-
func testAzureStaticRole_importConfig(backend, roleName string, conf *testutil.AzureTestConf, secretID, clientSecret, expiration string) string {
166-
exp := ""
167-
if expiration != "" {
168-
exp = fmt.Sprintf(`expiration = "%s"`, expiration)
169-
}
170-
163+
func testAzureStaticRole_importConfig(backend, roleName string, conf *testutil.AzureTestConf, secretID, clientSecret string) string {
171164
return fmt.Sprintf(`
172165
resource "vault_azure_secret_backend" "azure" {
173166
path = "%[1]s"
@@ -185,7 +178,7 @@ resource "vault_azure_secret_backend_static_role" "imported" {
185178
ttl = 63072000
186179
secret_id = "%[8]s"
187180
client_secret = "%[9]s"
188-
%[10]s
181+
%[9]s
189182
}
190183
191184
ephemeral "vault_azure_static_credentials" "imported" {
@@ -199,5 +192,5 @@ provider "echo" {
199192
}
200193
201194
resource "echo" "azure_creds_import" {}
202-
`, backend, conf.SubscriptionID, conf.TenantID, conf.ClientID, conf.ClientSecret, roleName, conf.AppObjectID, secretID, clientSecret, exp)
195+
`, backend, conf.SubscriptionID, conf.TenantID, conf.ClientID, conf.ClientSecret, roleName, conf.AppObjectID, secretID, clientSecret)
203196
}

website/docs/r/azure_secret_backend_static_role.html.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,19 @@ The following arguments are supported:
5959
* `application_object_id` - (Required) The Azure AD Application Object ID associated with the existing application whose
6060
credentials Vault will manage.
6161
* `ttl` – (Optional) Duration that defines the validity period of the managed credential. Defaults to 2 years. Must be
62-
at least 1 year.
62+
at least 1 month.
6363
Accepts an integer number of seconds (31536000). Defaults to the system/engine default TTL time.
6464
* `metadata` – (Optional) A map of string key-value pairs that are stored alongside the role and returned with generated
6565
credentials.
6666
* `secret_id` - (Optional) When importing an existing credential, specifies the Azure secret’s key ID.
6767
* `client_secret` - (Optional, Sensitive) When importing an existing credential, provides the existing client secret
6868
value.
69-
* `expiration` - (Optional) - Expiration timestamp (UTC, RFC3339 format) of the existing credential being imported. If
70-
not set, Vault uses the value provided by Azure.
69+
* `expiration` - (Optional) **Deprecated** - Expiration timestamp (UTC, RFC3339 format) of the existing credential being imported.
70+
Vault reads expiration from Azure.
7171
* `skip_import_rotation` - (Optional, Bool) - If true, Vault will import the provided credential without performing
7272
rotation. Valid only during creation. Defaults to `false`.
73+
* `defer_initial_creds` - (Optional, Bool) - If true, the initial credential generation will be deferred until the
74+
first read of credentials from this role. Defaults to `false`.
7375

7476
## Attributes Reference
7577

0 commit comments

Comments
 (0)