vault/builtin/credential/userpass/path_user_password.go
cui fliter 31f8c8cab7
fix: Fix problematic error returns (#29883)
Signed-off-by: cuishuang <imcusg@gmail.com>
2025-04-01 16:08:17 -04:00

143 lines
3.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package userpass
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
"golang.org/x/crypto/bcrypt"
)
const (
pathUserPasswordHelpDesc = `
This endpoint allows resetting the user's password.
`
pathUserPasswordHelpSyn = `
Reset user's password.
`
// The name of the username parameter supplied via the API.
paramUsername = "username"
// The name of the password parameter supplied via the API.
paramPassword = "password"
// The name of the password hash parameter supplied via the API.
paramPasswordHash = "password_hash"
// The expected length of any hash generated by bcrypt.
bcryptHashLength = 60
)
func pathUserPassword(b *backend) *framework.Path {
return &framework.Path{
Pattern: "users/" + framework.GenericNameRegex(paramUsername) + "/password$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixUserpass,
OperationVerb: "reset",
OperationSuffix: "password",
},
Fields: map[string]*framework.FieldSchema{
paramUsername: {
Type: framework.TypeString,
Description: "Username for this user.",
},
paramPassword: {
Type: framework.TypeString,
Description: "Password for this user.",
},
paramPasswordHash: {
Type: framework.TypeString,
Description: "Pre-hashed password in bcrypt format for this user.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathUserPasswordUpdate,
},
HelpSynopsis: pathUserPasswordHelpSyn,
HelpDescription: pathUserPasswordHelpDesc,
}
}
func (b *backend) pathUserPasswordUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := d.Get(paramUsername).(string)
userEntry, err := b.user(ctx, req.Storage, username)
if err != nil {
return nil, err
}
if userEntry == nil {
return nil, fmt.Errorf("username does not exist")
}
userErr, intErr := b.updateUserPassword(req, d, userEntry)
if intErr != nil {
return nil, intErr
}
if userErr != nil {
return logical.ErrorResponse(userErr.Error()), logical.ErrInvalidRequest
}
return nil, b.setUser(ctx, req.Storage, username, userEntry)
}
func (b *backend) updateUserPassword(_ *logical.Request, d *framework.FieldData, userEntry *UserEntry) (error, error) {
password := d.Get(paramPassword).(string)
passwordHash := d.Get(paramPasswordHash).(string)
var hash []byte
var err error
switch {
case password != "" && passwordHash != "":
return fmt.Errorf("%q and %q cannot be supplied together", paramPassword, paramPasswordHash), nil
case password == "" && passwordHash == "":
return fmt.Errorf("%q or %q must be supplied", paramPassword, paramPasswordHash), nil
case password != "":
hash, err = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
case passwordHash != "":
hash, err = parsePasswordHash(passwordHash)
}
if err != nil {
return nil, err
}
userEntry.PasswordHash = hash
return nil, nil
}
// parsePasswordHash is used to parse a password hash that follows the bcrypt standard.
// It examines the prefix of the string supplied to verify it complies with a supported
// version before returning the string in bytes.
func parsePasswordHash(passwordHash string) ([]byte, error) {
var res []byte
switch {
// All bcrypt hashes should be the same length.
case len(passwordHash) != bcryptHashLength:
return nil, fmt.Errorf("password hash has incorrect length")
// See: https://en.wikipedia.org/wiki/Bcrypt for versioning history.
case strings.HasPrefix(passwordHash, "$2a$"), // $2a% (non-ASCII character support)
strings.HasPrefix(passwordHash, "$2y$"), // $2y$ (PHP fixed)
strings.HasPrefix(passwordHash, "$2b$"): // $2b$ (truncation fix)
res = []byte(passwordHash)
default:
return nil, fmt.Errorf("password hash has incorrect prefix")
}
return res, nil
}