2023-03-15 16:00:52 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2020-09-18 21:10:54 +00:00
|
|
|
package database
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
2020-09-30 23:08:37 +00:00
|
|
|
"github.com/hashicorp/go-multierror"
|
2022-11-23 18:36:25 +00:00
|
|
|
"github.com/hashicorp/vault/helper/versions"
|
2020-10-15 19:20:12 +00:00
|
|
|
v4 "github.com/hashicorp/vault/sdk/database/dbplugin"
|
|
|
|
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
2020-09-18 21:10:54 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
Add plugin version to GRPC interface (#17088)
Add plugin version to GRPC interface
Added a version interface in the sdk/logical so that it can be shared between all plugin types, and then wired it up to RunningVersion in the mounts, auth list, and database systems.
I've tested that this works with auth, database, and secrets plugin types, with the following logic to populate RunningVersion:
If a plugin has a PluginVersion() method implemented, then that is used
If not, and the plugin is built into the Vault binary, then the go.mod version is used
Otherwise, the it will be the empty string.
My apologies for the length of this PR.
* Placeholder backend should be external
We use a placeholder backend (previously a framework.Backend) before a
GRPC plugin is lazy-loaded. This makes us later think the plugin is a
builtin plugin.
So we added a `placeholderBackend` type that overrides the
`IsExternal()` method so that later we know that the plugin is external,
and don't give it a default builtin version.
2022-09-15 23:37:59 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
2020-09-18 21:10:54 +00:00
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
"google.golang.org/grpc/status"
|
|
|
|
)
|
|
|
|
|
|
|
|
type databaseVersionWrapper struct {
|
2020-10-15 19:20:12 +00:00
|
|
|
v4 v4.Database
|
|
|
|
v5 v5.Database
|
2020-09-18 21:10:54 +00:00
|
|
|
}
|
|
|
|
|
Add plugin version to GRPC interface (#17088)
Add plugin version to GRPC interface
Added a version interface in the sdk/logical so that it can be shared between all plugin types, and then wired it up to RunningVersion in the mounts, auth list, and database systems.
I've tested that this works with auth, database, and secrets plugin types, with the following logic to populate RunningVersion:
If a plugin has a PluginVersion() method implemented, then that is used
If not, and the plugin is built into the Vault binary, then the go.mod version is used
Otherwise, the it will be the empty string.
My apologies for the length of this PR.
* Placeholder backend should be external
We use a placeholder backend (previously a framework.Backend) before a
GRPC plugin is lazy-loaded. This makes us later think the plugin is a
builtin plugin.
So we added a `placeholderBackend` type that overrides the
`IsExternal()` method so that later we know that the plugin is external,
and don't give it a default builtin version.
2022-09-15 23:37:59 +00:00
|
|
|
var _ logical.PluginVersioner = databaseVersionWrapper{}
|
|
|
|
|
2020-09-18 21:10:54 +00:00
|
|
|
// newDatabaseWrapper figures out which version of the database the pluginName is referring to and returns a wrapper object
|
2022-11-23 18:36:25 +00:00
|
|
|
// that can be used to make operations on the underlying database plugin. If a builtin pluginVersion is provided, it will
|
|
|
|
// be ignored.
|
2022-09-09 16:32:28 +00:00
|
|
|
func newDatabaseWrapper(ctx context.Context, pluginName string, pluginVersion string, sys pluginutil.LookRunnerUtil, logger log.Logger) (dbw databaseVersionWrapper, err error) {
|
2022-11-23 18:36:25 +00:00
|
|
|
// 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 = ""
|
|
|
|
}
|
2022-09-09 16:32:28 +00:00
|
|
|
newDB, err := v5.PluginFactoryVersion(ctx, pluginName, pluginVersion, sys, logger)
|
2020-09-18 21:10:54 +00:00
|
|
|
if err == nil {
|
|
|
|
dbw = databaseVersionWrapper{
|
|
|
|
v5: newDB,
|
|
|
|
}
|
|
|
|
return dbw, nil
|
|
|
|
}
|
|
|
|
|
2020-09-30 23:08:37 +00:00
|
|
|
merr := &multierror.Error{}
|
|
|
|
merr = multierror.Append(merr, err)
|
|
|
|
|
2022-09-09 16:32:28 +00:00
|
|
|
legacyDB, err := v4.PluginFactoryVersion(ctx, pluginName, pluginVersion, sys, logger)
|
2020-09-18 21:10:54 +00:00
|
|
|
if err == nil {
|
|
|
|
dbw = databaseVersionWrapper{
|
|
|
|
v4: legacyDB,
|
|
|
|
}
|
|
|
|
return dbw, nil
|
|
|
|
}
|
2020-09-30 23:08:37 +00:00
|
|
|
merr = multierror.Append(merr, err)
|
2020-09-18 21:10:54 +00:00
|
|
|
|
2020-09-30 23:08:37 +00:00
|
|
|
return dbw, fmt.Errorf("invalid database version: %s", merr)
|
2020-09-18 21:10:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2020-10-15 19:20:12 +00:00
|
|
|
func (d databaseVersionWrapper) Initialize(ctx context.Context, req v5.InitializeRequest) (v5.InitializeResponse, error) {
|
2020-09-18 21:10:54 +00:00
|
|
|
if !d.isV5() && !d.isV4() {
|
2020-10-15 19:20:12 +00:00
|
|
|
return v5.InitializeResponse{}, fmt.Errorf("no underlying database specified")
|
2020-09-18 21:10:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2020-10-15 19:20:12 +00:00
|
|
|
return v5.InitializeResponse{}, err
|
2020-09-18 21:10:54 +00:00
|
|
|
}
|
2020-10-15 19:20:12 +00:00
|
|
|
resp := v5.InitializeResponse{
|
2020-09-18 21:10:54 +00:00
|
|
|
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.
|
2020-10-15 19:20:12 +00:00
|
|
|
func (d databaseVersionWrapper) NewUser(ctx context.Context, req v5.NewUserRequest) (resp v5.NewUserResponse, password string, err error) {
|
2020-09-18 21:10:54 +00:00
|
|
|
if !d.isV5() && !d.isV4() {
|
2020-10-15 19:20:12 +00:00
|
|
|
return v5.NewUserResponse{}, "", fmt.Errorf("no underlying database specified")
|
2020-09-18 21:10:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// v5 Database
|
|
|
|
if d.isV5() {
|
|
|
|
resp, err = d.v5.NewUser(ctx, req)
|
|
|
|
return resp, req.Password, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// v4 Database
|
2020-10-15 19:20:12 +00:00
|
|
|
stmts := v4.Statements{
|
2020-09-18 21:10:54 +00:00
|
|
|
Creation: req.Statements.Commands,
|
|
|
|
Rollback: req.RollbackStatements.Commands,
|
|
|
|
}
|
2020-10-15 19:20:12 +00:00
|
|
|
usernameConfig := v4.UsernameConfig{
|
2020-09-18 21:10:54 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-10-15 19:20:12 +00:00
|
|
|
resp = v5.NewUserResponse{
|
2020-09-18 21:10:54 +00:00
|
|
|
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.
|
2020-10-15 19:20:12 +00:00
|
|
|
func (d databaseVersionWrapper) UpdateUser(ctx context.Context, req v5.UpdateUserRequest, isRootUser bool) (saveConfig map[string]interface{}, err error) {
|
2020-09-18 21:10:54 +00:00
|
|
|
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 {
|
2020-10-15 19:20:12 +00:00
|
|
|
stmts := v4.Statements{
|
2020-09-18 21:10:54 +00:00
|
|
|
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.
|
2020-10-15 19:20:12 +00:00
|
|
|
func (d databaseVersionWrapper) changePasswordLegacy(ctx context.Context, username string, passwordChange *v5.ChangePassword, isRootUser bool) (saveConfig map[string]interface{}, err error) {
|
2020-09-18 21:10:54 +00:00
|
|
|
err = d.changeUserPasswordLegacy(ctx, username, passwordChange)
|
|
|
|
|
|
|
|
// If changing the root user's password but SetCredentials is unimplemented, fall back to RotateRootCredentials
|
2021-05-12 21:22:41 +00:00
|
|
|
if isRootUser && (err == v4.ErrPluginStaticUnsupported || status.Code(err) == codes.Unimplemented) {
|
2020-09-18 21:10:54 +00:00
|
|
|
saveConfig, err = d.changeRootUserPasswordLegacy(ctx, passwordChange)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return saveConfig, nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2020-10-15 19:20:12 +00:00
|
|
|
func (d databaseVersionWrapper) changeUserPasswordLegacy(ctx context.Context, username string, passwordChange *v5.ChangePassword) (err error) {
|
|
|
|
stmts := v4.Statements{
|
2020-09-18 21:10:54 +00:00
|
|
|
Rotation: passwordChange.Statements.Commands,
|
|
|
|
}
|
2020-10-15 19:20:12 +00:00
|
|
|
staticConfig := v4.StaticUserConfig{
|
2020-09-18 21:10:54 +00:00
|
|
|
Username: username,
|
|
|
|
Password: passwordChange.NewPassword,
|
|
|
|
}
|
|
|
|
_, _, err = d.v4.SetCredentials(ctx, stmts, staticConfig)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-15 19:20:12 +00:00
|
|
|
func (d databaseVersionWrapper) changeRootUserPasswordLegacy(ctx context.Context, passwordChange *v5.ChangePassword) (saveConfig map[string]interface{}, err error) {
|
2020-09-18 21:10:54 +00:00
|
|
|
return d.v4.RotateRootCredentials(ctx, passwordChange.Statements.Commands)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteUser in the underlying database. Errors if the wrapper does not contain an underlying database.
|
2020-10-15 19:20:12 +00:00
|
|
|
func (d databaseVersionWrapper) DeleteUser(ctx context.Context, req v5.DeleteUserRequest) (v5.DeleteUserResponse, error) {
|
2020-09-18 21:10:54 +00:00
|
|
|
if !d.isV5() && !d.isV4() {
|
2020-10-15 19:20:12 +00:00
|
|
|
return v5.DeleteUserResponse{}, fmt.Errorf("no underlying database specified")
|
2020-09-18 21:10:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// v5 Database
|
|
|
|
if d.isV5() {
|
|
|
|
return d.v5.DeleteUser(ctx, req)
|
|
|
|
}
|
|
|
|
|
|
|
|
// v4 Database
|
2020-10-15 19:20:12 +00:00
|
|
|
stmts := v4.Statements{
|
2020-09-18 21:10:54 +00:00
|
|
|
Revocation: req.Statements.Commands,
|
|
|
|
}
|
|
|
|
err := d.v4.RevokeUser(ctx, stmts, req.Username)
|
2020-10-15 19:20:12 +00:00
|
|
|
return v5.DeleteUserResponse{}, err
|
2020-09-18 21:10:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
|
Add plugin version to GRPC interface (#17088)
Add plugin version to GRPC interface
Added a version interface in the sdk/logical so that it can be shared between all plugin types, and then wired it up to RunningVersion in the mounts, auth list, and database systems.
I've tested that this works with auth, database, and secrets plugin types, with the following logic to populate RunningVersion:
If a plugin has a PluginVersion() method implemented, then that is used
If not, and the plugin is built into the Vault binary, then the go.mod version is used
Otherwise, the it will be the empty string.
My apologies for the length of this PR.
* Placeholder backend should be external
We use a placeholder backend (previously a framework.Backend) before a
GRPC plugin is lazy-loaded. This makes us later think the plugin is a
builtin plugin.
So we added a `placeholderBackend` type that overrides the
`IsExternal()` method so that later we know that the plugin is external,
and don't give it a default builtin version.
2022-09-15 23:37:59 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-09-18 21:10:54 +00:00
|
|
|
func (d databaseVersionWrapper) isV5() bool {
|
|
|
|
return d.v5 != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d databaseVersionWrapper) isV4() bool {
|
|
|
|
return d.v4 != nil
|
|
|
|
}
|