open-vault/builtin/logical/database/rollback.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
}