| // 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 |
| } |