| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package database |
| |
| import ( |
| "context" |
| "fmt" |
| |
| log "github.com/hashicorp/go-hclog" |
| "github.com/hashicorp/go-multierror" |
| "github.com/hashicorp/vault/helper/versions" |
| v4 "github.com/hashicorp/vault/sdk/database/dbplugin" |
| v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" |
| "github.com/hashicorp/vault/sdk/helper/pluginutil" |
| "github.com/hashicorp/vault/sdk/logical" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/status" |
| ) |
| |
| type databaseVersionWrapper struct { |
| v4 v4.Database |
| v5 v5.Database |
| } |
| |
| var _ logical.PluginVersioner = databaseVersionWrapper{} |
| |
| // newDatabaseWrapper figures out which version of the database the pluginName is referring to and returns a wrapper object |
| // that can be used to make operations on the underlying database plugin. If a builtin pluginVersion is provided, it will |
| // be ignored. |
| func newDatabaseWrapper(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (dbw databaseVersionWrapper, err error) { |
| // 1.12.0 and 1.12.1 stored plugin version in the config, but that stored |
| // builtin version may disappear from the plugin catalog when Vault is |
| // upgraded, so always reference builtin plugins by an empty version. |
| if versions.IsBuiltinVersion(pluginVersion) { |
| pluginVersion = "" |
| } |
| newDB, err := v5.PluginFactoryVersion(ctx, pluginName, pluginVersion, sys, logger) |
| if err == nil { |
| dbw = databaseVersionWrapper{ |
| v5: newDB, |
| } |
| return dbw, nil |
| } |
| |
| merr := &multierror.Error{} |
| merr = multierror.Append(merr, err) |
| |
| legacyDB, err := v4.PluginFactoryVersion(ctx, pluginName, pluginVersion, sys, logger) |
| if err == nil { |
| dbw = databaseVersionWrapper{ |
| v4: legacyDB, |
| } |
| return dbw, nil |
| } |
| merr = multierror.Append(merr, err) |
| |
| return dbw, fmt.Errorf("invalid database version: %s", merr) |
| } |
| |
| // Initialize the underlying database. This is analogous to a constructor on the database plugin object. |
| // Errors if the wrapper does not contain an underlying database. |
| func (d databaseVersionWrapper) Initialize(ctx context.Context, req v5.InitializeRequest) (v5.InitializeResponse, error) { |
| if !d.isV5() && !d.isV4() { |
| return v5.InitializeResponse{}, fmt.Errorf("no underlying database specified") |
| } |
| |
| // v5 Database |
| if d.isV5() { |
| return d.v5.Initialize(ctx, req) |
| } |
| |
| // v4 Database |
| saveConfig, err := d.v4.Init(ctx, req.Config, req.VerifyConnection) |
| if err != nil { |
| return v5.InitializeResponse{}, err |
| } |
| resp := v5.InitializeResponse{ |
| Config: saveConfig, |
| } |
| return resp, nil |
| } |
| |
| // NewUser in the database. This is different from the v5 Database in that it returns a password as well. |
| // This is done because the v4 Database is expected to generate a password and return it. The NewUserResponse |
| // does not have a way of returning the password so this function signature needs to be different. |
| // The password returned here should be considered the source of truth, not the provided password. |
| // Errors if the wrapper does not contain an underlying database. |
| func (d databaseVersionWrapper) NewUser(ctx context.Context, req v5.NewUserRequest) (resp v5.NewUserResponse, password string, err error) { |
| if !d.isV5() && !d.isV4() { |
| return v5.NewUserResponse{}, "", fmt.Errorf("no underlying database specified") |
| } |
| |
| // v5 Database |
| if d.isV5() { |
| resp, err = d.v5.NewUser(ctx, req) |
| return resp, req.Password, err |
| } |
| |
| // v4 Database |
| stmts := v4.Statements{ |
| Creation: req.Statements.Commands, |
| Rollback: req.RollbackStatements.Commands, |
| } |
| usernameConfig := v4.UsernameConfig{ |
| DisplayName: req.UsernameConfig.DisplayName, |
| RoleName: req.UsernameConfig.RoleName, |
| } |
| username, password, err := d.v4.CreateUser(ctx, stmts, usernameConfig, req.Expiration) |
| if err != nil { |
| return resp, "", err |
| } |
| |
| resp = v5.NewUserResponse{ |
| Username: username, |
| } |
| return resp, password, nil |
| } |
| |
| // UpdateUser in the underlying database. This is used to update any information currently supported |
| // in the UpdateUserRequest such as password credentials or user TTL. |
| // Errors if the wrapper does not contain an underlying database. |
| func (d databaseVersionWrapper) UpdateUser(ctx context.Context, req v5.UpdateUserRequest, isRootUser bool) (saveConfig map[string]interface{}, err error) { |
| if !d.isV5() && !d.isV4() { |
| return nil, fmt.Errorf("no underlying database specified") |
| } |
| |
| // v5 Database |
| if d.isV5() { |
| _, err := d.v5.UpdateUser(ctx, req) |
| return nil, err |
| } |
| |
| // v4 Database |
| if req.Password == nil && req.Expiration == nil { |
| return nil, fmt.Errorf("missing change to be sent to the database") |
| } |
| if req.Password != nil && req.Expiration != nil { |
| // We could support this, but it would require handling partial |
| // errors which I'm punting on since we don't need it for now |
| return nil, fmt.Errorf("cannot specify both password and expiration change at the same time") |
| } |
| |
| // Change password |
| if req.Password != nil { |
| return d.changePasswordLegacy(ctx, req.Username, req.Password, isRootUser) |
| } |
| |
| // Change expiration date |
| if req.Expiration != nil { |
| stmts := v4.Statements{ |
| Renewal: req.Expiration.Statements.Commands, |
| } |
| err := d.v4.RenewUser(ctx, stmts, req.Username, req.Expiration.NewExpiration) |
| return nil, err |
| } |
| return nil, nil |
| } |
| |
| // changePasswordLegacy attempts to use SetCredentials to change the password for the user with the password provided |
| // in ChangePassword. If that user is the root user and SetCredentials is unimplemented, it will fall back to using |
| // RotateRootCredentials. If not the root user, this will not use RotateRootCredentials. |
| func (d databaseVersionWrapper) changePasswordLegacy(ctx context.Context, username string, passwordChange *v5.ChangePassword, isRootUser bool) (saveConfig map[string]interface{}, err error) { |
| err = d.changeUserPasswordLegacy(ctx, username, passwordChange) |
| |
| // If changing the root user's password but SetCredentials is unimplemented, fall back to RotateRootCredentials |
| if isRootUser && (err == v4.ErrPluginStaticUnsupported || status.Code(err) == codes.Unimplemented) { |
| saveConfig, err = d.changeRootUserPasswordLegacy(ctx, passwordChange) |
| if err != nil { |
| return nil, err |
| } |
| return saveConfig, nil |
| } |
| if err != nil { |
| return nil, err |
| } |
| return nil, nil |
| } |
| |
| func (d databaseVersionWrapper) changeUserPasswordLegacy(ctx context.Context, username string, passwordChange *v5.ChangePassword) (err error) { |
| stmts := v4.Statements{ |
| Rotation: passwordChange.Statements.Commands, |
| } |
| staticConfig := v4.StaticUserConfig{ |
| Username: username, |
| Password: passwordChange.NewPassword, |
| } |
| _, _, err = d.v4.SetCredentials(ctx, stmts, staticConfig) |
| return err |
| } |
| |
| func (d databaseVersionWrapper) changeRootUserPasswordLegacy(ctx context.Context, passwordChange *v5.ChangePassword) (saveConfig map[string]interface{}, err error) { |
| return d.v4.RotateRootCredentials(ctx, passwordChange.Statements.Commands) |
| } |
| |
| // DeleteUser in the underlying database. Errors if the wrapper does not contain an underlying database. |
| func (d databaseVersionWrapper) DeleteUser(ctx context.Context, req v5.DeleteUserRequest) (v5.DeleteUserResponse, error) { |
| if !d.isV5() && !d.isV4() { |
| return v5.DeleteUserResponse{}, fmt.Errorf("no underlying database specified") |
| } |
| |
| // v5 Database |
| if d.isV5() { |
| return d.v5.DeleteUser(ctx, req) |
| } |
| |
| // v4 Database |
| stmts := v4.Statements{ |
| Revocation: req.Statements.Commands, |
| } |
| err := d.v4.RevokeUser(ctx, stmts, req.Username) |
| return v5.DeleteUserResponse{}, err |
| } |
| |
| // Type of the underlying database. Errors if the wrapper does not contain an underlying database. |
| func (d databaseVersionWrapper) Type() (string, error) { |
| if !d.isV5() && !d.isV4() { |
| return "", fmt.Errorf("no underlying database specified") |
| } |
| |
| // v5 Database |
| if d.isV5() { |
| return d.v5.Type() |
| } |
| |
| // v4 Database |
| return d.v4.Type() |
| } |
| |
| // Close the underlying database. Errors if the wrapper does not contain an underlying database. |
| func (d databaseVersionWrapper) Close() error { |
| if !d.isV5() && !d.isV4() { |
| return fmt.Errorf("no underlying database specified") |
| } |
| // v5 Database |
| if d.isV5() { |
| return d.v5.Close() |
| } |
| |
| // v4 Database |
| return d.v4.Close() |
| } |
| |
| func (d databaseVersionWrapper) PluginVersion() logical.PluginVersion { |
| // v5 Database |
| if d.isV5() { |
| if versioner, ok := d.v5.(logical.PluginVersioner); ok { |
| return versioner.PluginVersion() |
| } |
| } |
| |
| // v4 Database |
| if versioner, ok := d.v4.(logical.PluginVersioner); ok { |
| return versioner.PluginVersion() |
| } |
| return logical.EmptyPluginVersion |
| } |
| |
| func (d databaseVersionWrapper) isV5() bool { |
| return d.v5 != nil |
| } |
| |
| func (d databaseVersionWrapper) isV4() bool { |
| return d.v4 != nil |
| } |