117 lines
3.9 KiB
Go
117 lines
3.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package database
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/hashicorp/vault/sdk/database/dbplugin"
|
|
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/mitchellh/mapstructure"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// WAL storage key used for the rollback of root database credentials
|
|
const rotateRootWALKey = "rotateRootWALKey"
|
|
|
|
// WAL entry used for the rollback of root database credentials
|
|
type rotateRootCredentialsWAL struct {
|
|
ConnectionName string
|
|
UserName string
|
|
NewPassword string
|
|
OldPassword string
|
|
}
|
|
|
|
// walRollback handles WAL entries that result from partial failures
|
|
// to rotate the root credentials of a database. It is responsible
|
|
// for rolling back root database credentials when doing so would
|
|
// reconcile the credentials with Vault storage.
|
|
func (b *databaseBackend) walRollback(ctx context.Context, req *logical.Request, kind string, data interface{}) error {
|
|
if kind != rotateRootWALKey {
|
|
return errors.New("unknown type to rollback")
|
|
}
|
|
|
|
// Decode the WAL data
|
|
var entry rotateRootCredentialsWAL
|
|
if err := mapstructure.Decode(data, &entry); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the current database configuration from storage
|
|
config, err := b.DatabaseConfig(ctx, req.Storage, entry.ConnectionName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The password in storage doesn't match the new password
|
|
// in the WAL entry. This means there was a partial failure
|
|
// to update either the database or storage.
|
|
if config.ConnectionDetails["password"] != entry.NewPassword {
|
|
// Clear any cached connection to inform the rollback decision
|
|
err := b.ClearConnection(entry.ConnectionName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Attempt to get a connection with the current configuration.
|
|
// If successful, the WAL entry can be deleted. This means
|
|
// the root credentials are the same according to the database
|
|
// and storage.
|
|
_, err = b.GetConnection(ctx, req.Storage, entry.ConnectionName)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
return b.rollbackDatabaseCredentials(ctx, config, entry)
|
|
}
|
|
|
|
// The password in storage matches the new password in
|
|
// the WAL entry, so there is nothing to roll back. This
|
|
// means the new password was successfully updated in the
|
|
// database and storage, but the WAL wasn't deleted.
|
|
return nil
|
|
}
|
|
|
|
// rollbackDatabaseCredentials rolls back root database credentials for
|
|
// the connection associated with the passed WAL entry. It will create
|
|
// a connection to the database using the WAL entry new password in
|
|
// order to alter the password to be the WAL entry old password.
|
|
func (b *databaseBackend) rollbackDatabaseCredentials(ctx context.Context, config *DatabaseConfig, entry rotateRootCredentialsWAL) error {
|
|
// Attempt to get a connection with the WAL entry new password.
|
|
config.ConnectionDetails["password"] = entry.NewPassword
|
|
dbi, err := b.GetConnectionWithConfig(ctx, entry.ConnectionName, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure the connection used to roll back the database password is not cached
|
|
defer func() {
|
|
if err := b.ClearConnection(entry.ConnectionName); err != nil {
|
|
b.Logger().Error("error closing database plugin connection", "err", err)
|
|
}
|
|
}()
|
|
|
|
updateReq := v5.UpdateUserRequest{
|
|
Username: entry.UserName,
|
|
CredentialType: v5.CredentialTypePassword,
|
|
Password: &v5.ChangePassword{
|
|
NewPassword: entry.OldPassword,
|
|
Statements: v5.Statements{
|
|
Commands: config.RootCredentialsRotateStatements,
|
|
},
|
|
},
|
|
}
|
|
|
|
// It actually is the root user here, but we only want to use SetCredentials since
|
|
// RotateRootCredentials doesn't give any control over what password is used
|
|
_, err = dbi.database.UpdateUser(ctx, updateReq, false)
|
|
if status.Code(err) == codes.Unimplemented || err == dbplugin.ErrPluginStaticUnsupported {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|