@@ -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
125137func (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
287339func (r * AzureSecretsStaticRoleResource ) Delete (ctx context.Context , req resource.DeleteRequest , resp * resource.DeleteResponse ) {
0 commit comments