vault/builtin/logical/database/version_wrapper.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

266 lines
8.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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
}