@@ -12,6 +12,7 @@ import (
1212 "strings"
1313
1414 "github.com/go-viper/mapstructure/v2"
15+ "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
1516 "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1617 "github.com/hashicorp/terraform-plugin-framework/diag"
1718 "github.com/hashicorp/terraform-plugin-framework/path"
@@ -51,10 +52,12 @@ type UserpassAuthUserResource struct {
5152type UserpassAuthUserModel struct {
5253 token.TokenModel
5354
54- Mount types.String `tfsdk:"mount"`
55- Username types.String `tfsdk:"username"`
56- PasswordWO types.String `tfsdk:"password_wo"`
57- PasswordHashWO types.String `tfsdk:"password_hash_wo"`
55+ Mount types.String `tfsdk:"mount"`
56+ Username types.String `tfsdk:"username"`
57+ PasswordWO types.String `tfsdk:"password_wo"`
58+ PasswordWOVersion types.Int64 `tfsdk:"password_wo_version"`
59+ PasswordHashWO types.String `tfsdk:"password_hash_wo"`
60+ PasswordHashWOVersion types.Int64 `tfsdk:"password_hash_wo_version"`
5861}
5962
6063type UserpassAuthUserAPIModel struct {
@@ -89,21 +92,41 @@ func (r *UserpassAuthUserResource) Schema(_ context.Context, _ resource.SchemaRe
8992 Sensitive : true ,
9093 WriteOnly : true ,
9194 Validators : []validator.String {
92- stringvalidator .ConflictsWith (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordHashWO )),
9395 stringvalidator .ExactlyOneOf (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordHashWO )),
9496 },
9597 },
98+ consts .FieldPasswordWOVersion : schema.Int64Attribute {
99+ MarkdownDescription : "Version counter for the write-only `password_wo` field. " +
100+ "Since write-only values are not stored in state, Terraform cannot detect when the password changes. " +
101+ "Increment this value whenever you update `password_wo` to ensure the new password is sent to Vault." ,
102+ Optional : true ,
103+ Validators : []validator.Int64 {
104+ int64validator .AlsoRequires (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordWO )),
105+ int64validator .ConflictsWith (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordHashWO )),
106+ int64validator .ConflictsWith (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordHashWOVersion )),
107+ },
108+ },
96109 consts .FieldPasswordHashWO : schema.StringAttribute {
97110 MarkdownDescription : "Pre-hashed password for this user in bcrypt format. Available in Vault 1.17 and later. Mutually exclusive with password_wo." ,
98111 Optional : true ,
99112 Sensitive : true ,
100113 WriteOnly : true ,
101114 Validators : []validator.String {
102- stringvalidator .ConflictsWith (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordWO )),
103115 stringvalidator .ExactlyOneOf (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordWO )),
104116 stringvalidator .RegexMatches (bcryptHashRegexp , "must be a bcrypt hash" ),
105117 },
106118 },
119+ consts .FieldPasswordHashWOVersion : schema.Int64Attribute {
120+ MarkdownDescription : "Version counter for the write-only `password_hash_wo` field. " +
121+ "Since write-only values are not stored in state, Terraform cannot detect when the password hash changes. " +
122+ "Increment this value whenever you update `password_hash_wo` to ensure the new password hash is sent to Vault." ,
123+ Optional : true ,
124+ Validators : []validator.Int64 {
125+ int64validator .AlsoRequires (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordHashWO )),
126+ int64validator .ConflictsWith (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordWO )),
127+ int64validator .ConflictsWith (path .MatchRelative ().AtParent ().AtName (consts .FieldPasswordWOVersion )),
128+ },
129+ },
107130 },
108131 }
109132
@@ -117,6 +140,15 @@ func (r *UserpassAuthUserResource) Create(ctx context.Context, req resource.Crea
117140 return
118141 }
119142
143+ // Read version fields from config to ensure they're stored in state
144+ var configData UserpassAuthUserModel
145+ resp .Diagnostics .Append (req .Config .Get (ctx , & configData )... )
146+ if resp .Diagnostics .HasError () {
147+ return
148+ }
149+ data .PasswordWOVersion = configData .PasswordWOVersion
150+ data .PasswordHashWOVersion = configData .PasswordHashWOVersion
151+
120152 resp .Diagnostics .Append (r .upsertUser (ctx , & data , req .Config , errutil .VaultCreateErr )... )
121153 if resp .Diagnostics .HasError () {
122154 return
@@ -164,6 +196,15 @@ func (r *UserpassAuthUserResource) Update(ctx context.Context, req resource.Upda
164196 return
165197 }
166198
199+ // Read version fields from config to ensure they're stored in state
200+ var configData UserpassAuthUserModel
201+ resp .Diagnostics .Append (req .Config .Get (ctx , & configData )... )
202+ if resp .Diagnostics .HasError () {
203+ return
204+ }
205+ data .PasswordWOVersion = configData .PasswordWOVersion
206+ data .PasswordHashWOVersion = configData .PasswordHashWOVersion
207+
167208 resp .Diagnostics .Append (r .upsertUser (ctx , & data , req .Config , errutil .VaultUpdateErr )... )
168209 if resp .Diagnostics .HasError () {
169210 return
@@ -271,7 +312,7 @@ func (r *UserpassAuthUserResource) upsertUser(ctx context.Context, data *Userpas
271312 return diags
272313 }
273314
274- if err := r .updatePasswordAndPoliciesEndpoints (ctx , vaultClient , data , passwordWO .ValueString ()); err != nil {
315+ if err := r .updatePasswordAndPoliciesEndpoints (ctx , vaultClient , data , passwordWO .ValueString (), passwordHashWO . ValueString () ); err != nil {
275316 diags .AddError (writeErr (err ))
276317 return diags
277318 }
@@ -344,14 +385,21 @@ func (r *UserpassAuthUserResource) getAPIModel(ctx context.Context, data *Userpa
344385}
345386
346387// updatePasswordAndPoliciesEndpoints writes legacy compatibility endpoints when needed.
347- func (r * UserpassAuthUserResource ) updatePasswordAndPoliciesEndpoints (ctx context.Context , vaultClient * api.Client , data * UserpassAuthUserModel , password string ) error {
388+ func (r * UserpassAuthUserResource ) updatePasswordAndPoliciesEndpoints (ctx context.Context , vaultClient * api.Client , data * UserpassAuthUserModel , password , passwordHash string ) error {
348389 if password != "" {
349390 _ , err := vaultClient .Logical ().WriteWithContext (ctx , r .userPath (data .Mount .ValueString (), data .Username .ValueString (), "password" ), map [string ]any {"password" : password })
350391 if err != nil && ! util .Is404 (err ) {
351392 return fmt .Errorf ("failed writing user password endpoint: %w" , err )
352393 }
353394 }
354395
396+ if passwordHash != "" {
397+ _ , err := vaultClient .Logical ().WriteWithContext (ctx , r .userPath (data .Mount .ValueString (), data .Username .ValueString (), "password" ), map [string ]any {"password_hash" : passwordHash })
398+ if err != nil && ! util .Is404 (err ) {
399+ return fmt .Errorf ("failed writing user password_hash endpoint: %w" , err )
400+ }
401+ }
402+
355403 if data .TokenPolicies .IsNull () || data .TokenPolicies .IsUnknown () {
356404 return nil
357405 }
@@ -367,7 +415,7 @@ func (r *UserpassAuthUserResource) updatePasswordAndPoliciesEndpoints(ctx contex
367415
368416 sort .Strings (policies )
369417 payload := map [string ]any {
370- "policies " : strings . Join ( policies , "," ) ,
418+ "token_policies " : policies ,
371419 }
372420
373421 _ , err := vaultClient .Logical ().WriteWithContext (ctx , r .userPath (data .Mount .ValueString (), data .Username .ValueString (), "policies" ), payload )
@@ -394,7 +442,7 @@ func (r *UserpassAuthUserResource) populateDataModelFromAPI(ctx context.Context,
394442 }
395443
396444 // Save the current alias_metadata value before PopulateTokenModelFromAPI
397- // overwrites it. On Vault versions prior to 1.21, the CF auth plugin does
445+ // overwrites it. On Vault versions prior to 1.21, the Userpass auth does
398446 // not support alias_metadata: it is silently dropped on write and absent
399447 // on read. Restoring the value preserves plan/state consistency and avoids
400448 // a "provider produced inconsistent result" error.
@@ -407,8 +455,6 @@ func (r *UserpassAuthUserResource) populateDataModelFromAPI(ctx context.Context,
407455 if ! r .supportsAliasMetadata () {
408456 data .TokenModel .AliasMetadata = savedAliasMetadata
409457 }
410- //data.Mount = types.StringValue(data.Mount.ValueString())
411- //data.Username = types.StringValue(data.Username.ValueString())
412458
413459 return nil
414460}
0 commit comments