Combined Database Backend: Static Accounts (#6834)

* Add priority queue to sdk

* fix issue of storing pointers and now copy

* update to use copy structure

* Remove file, put Item struct def. into other file

* add link

* clean up docs

* refactor internal data structure to hide heap method implementations. Other cleanup after feedback

* rename PushItem and PopItem to just Push/Pop, after encapsulating the heap methods

* updates after feedback

* refactoring/renaming

* guard against pushing a nil item

* minor updates after feedback

* Add SetCredentials, GenerateCredentials gRPC methods to combined database backend gPRC

* Initial Combined database backend implementation of static accounts and automatic rotation

* vendor updates

* initial implementation of static accounts with Combined database backend, starting with PostgreSQL implementation

* add lock and setup of rotation queue

* vendor the queue

* rebase on new method signature of queue

* remove mongo tests for now

* update default role sql

* gofmt after rebase

* cleanup after rebasing to remove checks for ErrNotFound error

* rebase cdcr-priority-queue

* vendor dependencies with 'go mod vendor'

* website database docs for Static Role support

* document the rotate-role API endpoint

* postgres specific static role docs

* use constants for paths

* updates from review

* remove dead code

* combine and clarify error message for older plugins

* Update builtin/logical/database/backend.go

Co-Authored-By: Jim Kalafut <jim@kalafut.net>

* cleanups from feedback

* code and comment cleanups

* move db.RLock higher to protect db.GenerateCredentials call

* Return output with WALID if we failed to delete the WAL

* Update builtin/logical/database/path_creds_create.go

Co-Authored-By: Jim Kalafut <jim@kalafut.net>

* updates after running 'make fmt'

* update after running 'make proto'

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* update comment and remove and rearrange some dead code

* Update website/source/api/secret/databases/index.html.md

Co-Authored-By: Jim Kalafut <jim@kalafut.net>

* cleanups after review

* Update sdk/database/dbplugin/grpc_transport.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* code cleanup after feedback

* remove PasswordLastSet; it's not used

* document GenerateCredentials and SetCredentials

* Update builtin/logical/database/path_rotate_credentials.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* wrap pop and popbykey in backend methods to protect against nil cred rotation queue

* use strings.HasPrefix instead of direct equality check for path

* Forgot to commit this

* updates after feedback

* re-purpose an outdated test to now check that static and dynamic roles cannot share a name

* check for unique name across dynamic and static roles

* refactor loadStaticWALs to return a map of name/setCredentialsWAL struct to consolidate where we're calling set credentials

* remove commented out code

* refactor to have loadstaticwals filter out wals for roles that no longer exist

* return error if nil input given

* add nil check for input into setStaticAccount

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* add constant for queue tick time in seconds, used for comparrison in updates

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Jim Kalafut <jim@kalafut.net>

* code cleanup after review

* remove misplaced code comment

* remove commented out code

* create a queue in the Factory method, even if it's never used

* update path_roles to use a common set of fields, with specific overrides for dynamic/static roles by type

* document new method

* move rotation things into a specific file

* rename test file and consolidate some static account tests

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* update code comments, method names, and move more methods into rotation.go

* update comments to be capitalized

* remove the item from the queue before we try to destroy it

* findStaticWAL returns an error

* use lowercase keys when encoding WAL entries

* small cleanups

* remove vestigial static account check

* remove redundant DeleteWAL call in populate queue

* if we error on loading role, push back to queue with 10 second backoff

* poll in initqueue to make sure the backend is setup and can write/delete data

* add revoke_user_on_delete flag to allow users to opt-in to revoking the static database user on delete of the Vault role. Default false

* add code comments on read-only loop

* code comment updates

* re-push if error returned from find static wal

* add locksutil and acquire locks when pop'ing from the queue

* grab exclusive locks for updating static roles

* Add SetCredentials and GenerateCredentials stubs to mockPlugin

* add a switch in initQueue to listen for cancelation

* remove guard on zero time, it should have no affect

* create a new context in Factory to pass on and use for closing the backend queue

* restore master copy of vendor dir
This commit is contained in:
Clint
2019-06-19 14:45:39 -05:00
committed by GitHub
parent 22758680d7
commit 35667f93a7
34 changed files with 3895 additions and 327 deletions

View File

@@ -14,11 +14,17 @@ import (
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/locksutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/queue"
)
const databaseConfigPath = "database/config/"
const (
databaseConfigPath = "database/config/"
databaseRolePath = "role/"
databaseStaticRolePath = "static-role/"
)
type dbPluginInstance struct {
sync.RWMutex
@@ -46,6 +52,15 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
if err := b.Setup(ctx, conf); err != nil {
return nil, err
}
b.credRotationQueue = queue.New()
// Create a context with a cancel method for processing any WAL entries and
// populating the queue
initCtx := context.Background()
ictx, cancel := context.WithCancel(initCtx)
b.cancelQueue = cancel
// Load queue and kickoff new periodic ticker
go b.initQueue(ictx, conf)
return b, nil
}
@@ -55,31 +70,39 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
Help: strings.TrimSpace(backendHelp),
PathsSpecial: &logical.Paths{
LocalStorage: []string{
framework.WALPrefix,
},
SealWrapStorage: []string{
"config/*",
"static-role/*",
},
},
Paths: []*framework.Path{
pathListPluginConnection(&b),
pathConfigurePluginConnection(&b),
Paths: framework.PathAppend(
[]*framework.Path{
pathListPluginConnection(&b),
pathConfigurePluginConnection(&b),
pathResetConnection(&b),
},
pathListRoles(&b),
pathRoles(&b),
pathCredsCreate(&b),
pathResetConnection(&b),
pathRotateCredentials(&b),
},
),
Secrets: []*framework.Secret{
secretCreds(&b),
},
Clean: b.closeAllDBs,
Clean: b.clean,
Invalidate: b.invalidate,
BackendType: logical.TypeLogical,
}
b.logger = conf.Logger
b.connections = make(map[string]*dbPluginInstance)
b.roleLocks = locksutil.CreateLocks()
return &b
}
@@ -89,6 +112,20 @@ type databaseBackend struct {
*framework.Backend
sync.RWMutex
// CredRotationQueue is an in-memory priority queue used to track Static Roles
// that require periodic rotation. Backends will have a PriorityQueue
// initialized on setup, but only backends that are mounted by a primary
// server or mounted as a local mount will perform the rotations.
//
// cancelQueue is used to remove the priority queue and terminate the
// background ticker.
credRotationQueue *queue.PriorityQueue
cancelQueue context.CancelFunc
// roleLocks is used to lock modifications to roles in the queue, to ensure
// concurrent requests are not modifying the same role and possibly causing
// issues with the priority queue.
roleLocks []*locksutil.LockEntry
}
func (b *databaseBackend) DatabaseConfig(ctx context.Context, s logical.Storage, name string) (*DatabaseConfig, error) {
@@ -124,7 +161,15 @@ type upgradeCheck struct {
}
func (b *databaseBackend) Role(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) {
entry, err := s.Get(ctx, "role/"+roleName)
return b.roleAtPath(ctx, s, roleName, databaseRolePath)
}
func (b *databaseBackend) StaticRole(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) {
return b.roleAtPath(ctx, s, roleName, databaseStaticRolePath)
}
func (b *databaseBackend) roleAtPath(ctx context.Context, s logical.Storage, roleName string, pathPrefix string) (*roleEntry, error) {
entry, err := s.Get(ctx, pathPrefix+roleName)
if err != nil {
return nil, err
}
@@ -228,6 +273,17 @@ func (b *databaseBackend) GetConnection(ctx context.Context, s logical.Storage,
return db, nil
}
// invalidateQueue cancels any background queue loading and destroys the queue.
func (b *databaseBackend) invalidateQueue() {
b.Lock()
defer b.Unlock()
if b.cancelQueue != nil {
b.cancelQueue()
}
b.credRotationQueue = nil
}
// ClearConnection closes the database connection and
// removes it from the b.connections map.
func (b *databaseBackend) ClearConnection(name string) error {
@@ -267,8 +323,13 @@ func (b *databaseBackend) CloseIfShutdown(db *dbPluginInstance, err error) {
}
}
// closeAllDBs closes all connections from all database types
func (b *databaseBackend) closeAllDBs(ctx context.Context) {
// clean closes all connections from all database types
// and cancels any rotation queue loading operation.
func (b *databaseBackend) clean(ctx context.Context) {
// invalidateQueue acquires it's own lock on the backend, removes queue, and
// terminates the background ticker
b.invalidateQueue()
b.Lock()
defer b.Unlock()

View File

@@ -56,7 +56,7 @@ func preparePostgresTestContainer(t *testing.T, s logical.Storage, b logical.Bac
retURL = fmt.Sprintf("postgres://postgres:secret@localhost:%s/database?sslmode=disable", resource.GetPort("5432/tcp"))
// exponential backoff-retry
// Exponential backoff-retry
if err = pool.Retry(func() error {
// This will cause a validation to run
resp, err := b.HandleRequest(namespace.RootContext(nil), &logical.Request{
@@ -101,12 +101,12 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) {
os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile)
sys := vault.TestDynamicSystemView(cores[0].Core)
vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain", []string{}, "")
vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Postgres", []string{}, "")
return cluster, sys
}
func TestBackend_PluginMain(t *testing.T) {
func TestBackend_PluginMain_Postgres(t *testing.T) {
if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" {
return
}
@@ -850,17 +850,6 @@ func TestBackend_roleCrud(t *testing.T) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
exists, err := b.pathRoleExistenceCheck()(context.Background(), req, &framework.FieldData{
Raw: data,
Schema: pathRoles(b).Fields,
})
if err != nil {
t.Fatal(err)
}
if exists {
t.Fatal("expected not exists")
}
// Read the role
data = map[string]interface{}{}
req = &logical.Request{
@@ -920,17 +909,6 @@ func TestBackend_roleCrud(t *testing.T) {
t.Fatalf("err:%v resp:%#v\n", err, resp)
}
exists, err := b.pathRoleExistenceCheck()(context.Background(), req, &framework.FieldData{
Raw: data,
Schema: pathRoles(b).Fields,
})
if err != nil {
t.Fatal(err)
}
if !exists {
t.Fatal("expected exists")
}
// Read the role
data = map[string]interface{}{}
req = &logical.Request{
@@ -994,17 +972,6 @@ func TestBackend_roleCrud(t *testing.T) {
t.Fatalf("err:%v resp:%#v\n", err, resp)
}
exists, err := b.pathRoleExistenceCheck()(context.Background(), req, &framework.FieldData{
Raw: data,
Schema: pathRoles(b).Fields,
})
if err != nil {
t.Fatal(err)
}
if !exists {
t.Fatal("expected exists")
}
// Read the role
data = map[string]interface{}{}
req = &logical.Request{

View File

@@ -22,6 +22,8 @@ type mockPlugin struct {
users map[string][]string
}
var _ dbplugin.Database = &mockPlugin{}
func (m *mockPlugin) Type() (string, error) { return "mock", nil }
func (m *mockPlugin) CreateUser(_ context.Context, statements dbplugin.Statements, usernameConf dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) {
err = errors.New("err")
@@ -86,6 +88,14 @@ func (m *mockPlugin) Close() error {
return nil
}
func (m *mockPlugin) GenerateCredentials(ctx context.Context) (password string, err error) {
return password, err
}
func (m *mockPlugin) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticConfig dbplugin.StaticUserConfig) (username string, password string, err error) {
return username, password, err
}
func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) {
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,

View File

@@ -11,22 +11,40 @@ import (
"github.com/hashicorp/vault/sdk/logical"
)
func pathCredsCreate(b *databaseBackend) *framework.Path {
return &framework.Path{
Pattern: "creds/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role.",
func pathCredsCreate(b *databaseBackend) []*framework.Path {
return []*framework.Path{
&framework.Path{
Pattern: "creds/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role.",
},
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathCredsCreateRead(),
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathCredsCreateRead(),
},
HelpSynopsis: pathCredsCreateReadHelpSyn,
HelpDescription: pathCredsCreateReadHelpDesc,
HelpSynopsis: pathCredsCreateReadHelpSyn,
HelpDescription: pathCredsCreateReadHelpDesc,
},
&framework.Path{
Pattern: "static-creds/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the static role.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathStaticCredsRead(),
},
HelpSynopsis: pathStaticCredsReadHelpSyn,
HelpDescription: pathStaticCredsReadHelpDesc,
},
}
}
@@ -99,6 +117,41 @@ func (b *databaseBackend) pathCredsCreateRead() framework.OperationFunc {
}
}
func (b *databaseBackend) pathStaticCredsRead() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
role, err := b.StaticRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse("unknown role: %s", name), nil
}
dbConfig, err := b.DatabaseConfig(ctx, req.Storage, role.DBName)
if err != nil {
return nil, err
}
// If role name isn't in the database's allowed roles, send back a
// permission denied.
if !strutil.StrListContains(dbConfig.AllowedRoles, "*") && !strutil.StrListContainsGlob(dbConfig.AllowedRoles, name) {
return nil, fmt.Errorf("%q is not an allowed role", name)
}
return &logical.Response{
Data: map[string]interface{}{
"username": role.StaticAccount.Username,
"password": role.StaticAccount.Password,
"ttl": role.StaticAccount.PasswordTTL().Seconds(),
"rotation_period": role.StaticAccount.RotationPeriod.Seconds(),
"last_vault_rotation": role.StaticAccount.LastVaultRotation,
},
}, nil
}
}
const pathCredsCreateReadHelpSyn = `
Request database credentials for a certain role.
`
@@ -108,3 +161,14 @@ This path reads database credentials for a certain role. The
database credentials will be generated on demand and will be automatically
revoked when the lease is up.
`
const pathStaticCredsReadHelpSyn = `
Request database credentials for a certain static role. These credentials are
rotated periodically.
`
const pathStaticCredsReadHelpDesc = `
This path reads database credentials for a certain static role. The database
credentials are rotated periodically according to their configuration, and will
return the same password until they are rotated.
`

View File

@@ -2,241 +2,485 @@ package database
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/locksutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/queue"
)
func pathListRoles(b *databaseBackend) *framework.Path {
return &framework.Path{
Pattern: "roles/?$",
func pathListRoles(b *databaseBackend) []*framework.Path {
return []*framework.Path{
&framework.Path{
Pattern: "roles/?$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathRoleList(),
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathRoleList,
},
HelpSynopsis: pathRoleHelpSyn,
HelpDescription: pathRoleHelpDesc,
},
&framework.Path{
Pattern: "static-roles/?$",
HelpSynopsis: pathRoleHelpSyn,
HelpDescription: pathRoleHelpDesc,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathRoleList,
},
HelpSynopsis: pathStaticRoleHelpSyn,
HelpDescription: pathStaticRoleHelpDesc,
},
}
}
func pathRoles(b *databaseBackend) *framework.Path {
return &framework.Path{
Pattern: "roles/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the role.",
func pathRoles(b *databaseBackend) []*framework.Path {
return []*framework.Path{
&framework.Path{
Pattern: "roles/" + framework.GenericNameRegex("name"),
Fields: fieldsForType(databaseRolePath),
ExistenceCheck: b.pathRoleExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathRoleRead,
logical.CreateOperation: b.pathRoleCreateUpdate,
logical.UpdateOperation: b.pathRoleCreateUpdate,
logical.DeleteOperation: b.pathRoleDelete,
},
"db_name": {
Type: framework.TypeString,
Description: "Name of the database this role acts on.",
},
"creation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements executed to
create and configure a user. See the plugin's API page for more
information on support and formatting for this parameter.`,
},
"revocation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
to revoke a user. See the plugin's API page for more information
on support and formatting for this parameter.`,
},
"renew_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
to renew a user. Not every plugin type will support this
functionality. See the plugin's API page for more information on
support and formatting for this parameter. `,
},
"rollback_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
rollback a create operation in the event of an error. Not every
plugin type will support this functionality. See the plugin's
API page for more information on support and formatting for this
parameter.`,
},
"default_ttl": {
Type: framework.TypeDurationSecond,
Description: "Default ttl for role.",
},
"max_ttl": {
Type: framework.TypeDurationSecond,
Description: "Maximum time a credential is valid for",
},
HelpSynopsis: pathRoleHelpSyn,
HelpDescription: pathRoleHelpDesc,
},
ExistenceCheck: b.pathRoleExistenceCheck(),
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathRoleRead(),
logical.CreateOperation: b.pathRoleCreateUpdate(),
logical.UpdateOperation: b.pathRoleCreateUpdate(),
logical.DeleteOperation: b.pathRoleDelete(),
},
&framework.Path{
Pattern: "static-roles/" + framework.GenericNameRegex("name"),
Fields: fieldsForType(databaseStaticRolePath),
ExistenceCheck: b.pathStaticRoleExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathStaticRoleRead,
logical.CreateOperation: b.pathStaticRoleCreateUpdate,
logical.UpdateOperation: b.pathStaticRoleCreateUpdate,
logical.DeleteOperation: b.pathStaticRoleDelete,
},
HelpSynopsis: pathRoleHelpSyn,
HelpDescription: pathRoleHelpDesc,
HelpSynopsis: pathStaticRoleHelpSyn,
HelpDescription: pathStaticRoleHelpDesc,
},
}
}
func (b *databaseBackend) pathRoleExistenceCheck() framework.ExistenceFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
role, err := b.Role(ctx, req.Storage, data.Get("name").(string))
if err != nil {
return false, err
}
return role != nil, nil
// fieldsForType returns a map of string/FieldSchema items for the given role
// type. The purpose is to keep the shared fields between dynamic and static
// roles consistent, and allow for each type to override or provide their own
// specific fields
func fieldsForType(roleType string) map[string]*framework.FieldSchema {
fields := map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the role.",
},
"db_name": {
Type: framework.TypeString,
Description: "Name of the database this role acts on.",
},
"creation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements executed to
create and configure a user. See the plugin's API page for more
information on support and formatting for this parameter.`,
},
"revocation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
to revoke a user. See the plugin's API page for more information
on support and formatting for this parameter.`,
},
"renew_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
to renew a user. Not every plugin type will support this
functionality. See the plugin's API page for more information on
support and formatting for this parameter. `,
},
"rollback_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
rollback a create operation in the event of an error. Not every plugin
type will support this functionality. See the plugin's API page for
more information on support and formatting for this parameter.`,
},
}
// Get the fields that are specific to the type of role, and add them to the
// common fields
var typeFields map[string]*framework.FieldSchema
switch roleType {
case databaseStaticRolePath:
typeFields = staticFields()
default:
typeFields = dynamicFields()
}
for k, v := range typeFields {
fields[k] = v
}
return fields
}
func (b *databaseBackend) pathRoleDelete() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
err := req.Storage.Delete(ctx, "role/"+data.Get("name").(string))
if err != nil {
return nil, err
}
// dynamicFields returns a map of key and field schema items that are specific
// only to dynamic roles
func dynamicFields() map[string]*framework.FieldSchema {
fields := map[string]*framework.FieldSchema{
"default_ttl": {
Type: framework.TypeDurationSecond,
Description: "Default ttl for role.",
},
"max_ttl": {
Type: framework.TypeDurationSecond,
Description: "Maximum time a credential is valid for",
},
}
return fields
}
// staticFields returns a map of key and field schema items that are specific
// only to static roles
func staticFields() map[string]*framework.FieldSchema {
fields := map[string]*framework.FieldSchema{
"username": {
Type: framework.TypeString,
Description: `Name of the static user account for Vault to manage.
Requires "rotation_period" to be specified`,
},
"rotation_period": {
Type: framework.TypeDurationSecond,
Description: `Period for automatic
credential rotation of the given username. Not valid unless used with
"username".`,
},
"rotation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed to
rotate the accounts credentials. Not every plugin type will support
this functionality. See the plugin's API page for more information on
support and formatting for this parameter.`,
},
"revoke_user_on_delete": {
Type: framework.TypeBool,
Default: false,
Description: `Revoke the database user identified by the username when
this Role is deleted. Revocation will use the configured
revocation_statements if provided. Default false.`,
},
}
return fields
}
func (b *databaseBackend) pathRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
role, err := b.Role(ctx, req.Storage, data.Get("name").(string))
if err != nil {
return false, err
}
return role != nil, nil
}
func (b *databaseBackend) pathStaticRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string))
if err != nil {
return false, err
}
return role != nil, nil
}
func (b *databaseBackend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
err := req.Storage.Delete(ctx, databaseRolePath+data.Get("name").(string))
if err != nil {
return nil, err
}
return nil, nil
}
func (b *databaseBackend) pathStaticRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
// Grab the exclusive lock
lock := locksutil.LockForKey(b.roleLocks, name)
lock.Lock()
defer lock.Unlock()
// Remove the item from the queue
_, _ = b.popFromRotationQueueByKey(name)
// If this role is a static account, we need to revoke the user from the
// database
role, err := b.StaticRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
}
func (b *databaseBackend) pathRoleRead() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
role, err := b.Role(ctx, req.Storage, d.Get("name").(string))
// Clean up the static useraccount, if it exists
revoke := role.StaticAccount.RevokeUserOnDelete
if revoke {
db, err := b.GetConnection(ctx, req.Storage, role.DBName)
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
data := map[string]interface{}{
"db_name": role.DBName,
"creation_statements": role.Statements.Creation,
"revocation_statements": role.Statements.Revocation,
"rollback_statements": role.Statements.Rollback,
"renew_statements": role.Statements.Renewal,
"default_ttl": role.DefaultTTL.Seconds(),
"max_ttl": role.MaxTTL.Seconds(),
}
if len(role.Statements.Creation) == 0 {
data["creation_statements"] = []string{}
}
if len(role.Statements.Revocation) == 0 {
data["revocation_statements"] = []string{}
}
if len(role.Statements.Rollback) == 0 {
data["rollback_statements"] = []string{}
}
if len(role.Statements.Renewal) == 0 {
data["renew_statements"] = []string{}
}
db.RLock()
defer db.RUnlock()
return &logical.Response{
Data: data,
}, nil
if err := db.RevokeUser(ctx, role.Statements, role.StaticAccount.Username); err != nil {
b.CloseIfShutdown(db, err)
return nil, err
}
}
}
func (b *databaseBackend) pathRoleList() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
entries, err := req.Storage.List(ctx, "role/")
if err != nil {
return nil, err
}
return logical.ListResponse(entries), nil
err = req.Storage.Delete(ctx, databaseStaticRolePath+name)
if err != nil {
return nil, err
}
return nil, nil
}
func (b *databaseBackend) pathRoleCreateUpdate() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
if name == "" {
return logical.ErrorResponse("empty role name attribute given"), nil
func (b *databaseBackend) pathStaticRoleRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
role, err := b.StaticRole(ctx, req.Storage, d.Get("name").(string))
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
data := pathRoleReadCommon(role)
if role.StaticAccount != nil {
data["username"] = role.StaticAccount.Username
data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds()
if !role.StaticAccount.LastVaultRotation.IsZero() {
data["last_vault_rotation"] = role.StaticAccount.LastVaultRotation
}
data["revoke_user_on_delete"] = role.StaticAccount.RevokeUserOnDelete
}
role, err := b.Role(ctx, req.Storage, data.Get("name").(string))
return &logical.Response{
Data: data,
}, nil
}
func (b *databaseBackend) pathRoleRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
role, err := b.Role(ctx, req.Storage, d.Get("name").(string))
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
return &logical.Response{
Data: pathRoleReadCommon(role),
}, nil
}
func pathRoleReadCommon(role *roleEntry) map[string]interface{} {
data := map[string]interface{}{
"db_name": role.DBName,
"creation_statements": role.Statements.Creation,
"revocation_statements": role.Statements.Revocation,
"rollback_statements": role.Statements.Rollback,
"renew_statements": role.Statements.Renewal,
"rotation_statements": role.Statements.Rotation,
"default_ttl": role.DefaultTTL.Seconds(),
"max_ttl": role.MaxTTL.Seconds(),
}
if len(role.Statements.Creation) == 0 {
data["creation_statements"] = []string{}
}
if len(role.Statements.Revocation) == 0 {
data["revocation_statements"] = []string{}
}
if len(role.Statements.Rollback) == 0 {
data["rollback_statements"] = []string{}
}
if len(role.Statements.Renewal) == 0 {
data["renew_statements"] = []string{}
}
if len(role.Statements.Rotation) == 0 {
data["rotation_statements"] = []string{}
}
return data
}
func (b *databaseBackend) pathRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
path := databaseRolePath
if strings.HasPrefix(req.Path, "static-roles") {
path = databaseStaticRolePath
}
entries, err := req.Storage.List(ctx, path)
if err != nil {
return nil, err
}
return logical.ListResponse(entries), nil
}
func (b *databaseBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
if name == "" {
return logical.ErrorResponse("empty role name attribute given"), nil
}
exists, err := b.pathStaticRoleExistenceCheck(ctx, req, data)
if err != nil {
return nil, err
}
if exists {
return logical.ErrorResponse("Role and Static Role names must be unique"), nil
}
role, err := b.Role(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
role = &roleEntry{}
}
if err := pathRoleCreateUpdateCommon(ctx, role, req.Operation, data); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// TTLs
{
if defaultTTLRaw, ok := data.GetOk("default_ttl"); ok {
role.DefaultTTL = time.Duration(defaultTTLRaw.(int)) * time.Second
} else if req.Operation == logical.CreateOperation {
role.DefaultTTL = time.Duration(data.Get("default_ttl").(int)) * time.Second
}
if maxTTLRaw, ok := data.GetOk("max_ttl"); ok {
role.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second
} else if req.Operation == logical.CreateOperation {
role.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second
}
}
// Store it
entry, err := logical.StorageEntryJSON(databaseRolePath+name, role)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
if name == "" {
return logical.ErrorResponse("empty role name attribute given"), nil
}
// Grab the exclusive lock as well potentially pop and re-push the queue item
// for this role
lock := locksutil.LockForKey(b.roleLocks, name)
lock.Lock()
defer lock.Unlock()
exists, err := b.pathRoleExistenceCheck(ctx, req, data)
if err != nil {
return nil, err
}
if exists {
return logical.ErrorResponse("Role and Static Role names must be unique"), nil
}
role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string))
if err != nil {
return nil, err
}
// createRole is a boolean to indicate if this is a new role creation. This is
// can be used later by database plugins that distinguish between creating and
// updating roles, and may use seperate statements depending on the context.
createRole := req.Operation == logical.CreateOperation
if role == nil {
role = &roleEntry{
StaticAccount: &staticAccount{},
}
createRole = true
}
if err := pathRoleCreateUpdateCommon(ctx, role, req.Operation, data); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
username := data.Get("username").(string)
if username == "" && createRole {
return logical.ErrorResponse("username is a required field to create a static account"), nil
}
if role.StaticAccount.Username != "" && role.StaticAccount.Username != username {
return logical.ErrorResponse("cannot update static account username"), nil
}
role.StaticAccount.Username = username
// If it's a Create operation, both username and rotation_period must be included
rotationPeriodSecondsRaw, ok := data.GetOk("rotation_period")
if !ok && createRole {
return logical.ErrorResponse("rotation_period is required to create static accounts"), nil
}
if ok {
rotationPeriodSeconds := rotationPeriodSecondsRaw.(int)
if rotationPeriodSeconds < queueTickSeconds {
// If rotation frequency is specified, and this is an update, the value
// must be at least that of the constant queueTickSeconds (5 seconds at
// time of writing), otherwise we wont be able to rotate in time
return logical.ErrorResponse(fmt.Sprintf("rotation_period must be %d seconds or more", queueTickSeconds)), nil
}
role.StaticAccount.RotationPeriod = time.Duration(rotationPeriodSeconds) * time.Second
}
if rotationStmtsRaw, ok := data.GetOk("rotation_statements"); ok {
role.Statements.Rotation = rotationStmtsRaw.([]string)
} else if req.Operation == logical.CreateOperation {
role.Statements.Rotation = data.Get("rotation_statements").([]string)
}
role.StaticAccount.RevokeUserOnDelete = data.Get("revoke_user_on_delete").(bool)
// lvr represents the roles' LastVaultRotation
lvr := role.StaticAccount.LastVaultRotation
// Only call setStaticAccount if we're creating the role for the
// first time
switch req.Operation {
case logical.CreateOperation:
// setStaticAccount calls Storage.Put and saves the role to storage
resp, err := b.setStaticAccount(ctx, req.Storage, &setStaticAccountInput{
RoleName: name,
Role: role,
CreateUser: createRole,
})
if err != nil {
return nil, err
}
if role == nil {
role = &roleEntry{}
}
// DB Attributes
{
if dbNameRaw, ok := data.GetOk("db_name"); ok {
role.DBName = dbNameRaw.(string)
} else if req.Operation == logical.CreateOperation {
role.DBName = data.Get("db_name").(string)
}
if role.DBName == "" {
return logical.ErrorResponse("empty database name attribute"), nil
}
}
// TTLs
{
if defaultTTLRaw, ok := data.GetOk("default_ttl"); ok {
role.DefaultTTL = time.Duration(defaultTTLRaw.(int)) * time.Second
} else if req.Operation == logical.CreateOperation {
role.DefaultTTL = time.Duration(data.Get("default_ttl").(int)) * time.Second
}
if maxTTLRaw, ok := data.GetOk("max_ttl"); ok {
role.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second
} else if req.Operation == logical.CreateOperation {
role.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second
}
}
// Statements
{
if creationStmtsRaw, ok := data.GetOk("creation_statements"); ok {
role.Statements.Creation = creationStmtsRaw.([]string)
} else if req.Operation == logical.CreateOperation {
role.Statements.Creation = data.Get("creation_statements").([]string)
}
if revocationStmtsRaw, ok := data.GetOk("revocation_statements"); ok {
role.Statements.Revocation = revocationStmtsRaw.([]string)
} else if req.Operation == logical.CreateOperation {
role.Statements.Revocation = data.Get("revocation_statements").([]string)
}
if rollbackStmtsRaw, ok := data.GetOk("rollback_statements"); ok {
role.Statements.Rollback = rollbackStmtsRaw.([]string)
} else if req.Operation == logical.CreateOperation {
role.Statements.Rollback = data.Get("rollback_statements").([]string)
}
if renewStmtsRaw, ok := data.GetOk("renew_statements"); ok {
role.Statements.Renewal = renewStmtsRaw.([]string)
} else if req.Operation == logical.CreateOperation {
role.Statements.Renewal = data.Get("renew_statements").([]string)
}
// Do not persist deprecated statements that are populated on role read
role.Statements.CreationStatements = ""
role.Statements.RevocationStatements = ""
role.Statements.RenewStatements = ""
role.Statements.RollbackStatements = ""
}
role.Statements.Revocation = strutil.RemoveEmpty(role.Statements.Revocation)
// Store it
entry, err := logical.StorageEntryJSON("role/"+name, role)
// guard against RotationTime not being set or zero-value
lvr = resp.RotationTime
case logical.UpdateOperation:
// store updated Role
entry, err := logical.StorageEntryJSON(databaseStaticRolePath+name, role)
if err != nil {
return nil, err
}
@@ -244,21 +488,134 @@ func (b *databaseBackend) pathRoleCreateUpdate() framework.OperationFunc {
return nil, err
}
return nil, nil
// In case this is an update, remove any previous version of the item from
// the queue
b.popFromRotationQueueByKey(name)
}
// Add their rotation to the queue
if err := b.pushItem(&queue.Item{
Key: name,
Priority: lvr.Add(role.StaticAccount.RotationPeriod).Unix(),
}); err != nil {
return nil, err
}
return nil, nil
}
func pathRoleCreateUpdateCommon(ctx context.Context, role *roleEntry, operation logical.Operation, data *framework.FieldData) error {
// DB Attributes
{
if dbNameRaw, ok := data.GetOk("db_name"); ok {
role.DBName = dbNameRaw.(string)
} else if operation == logical.CreateOperation {
role.DBName = data.Get("db_name").(string)
}
if role.DBName == "" {
return errors.New("empty database name attribute")
}
}
// Statements
{
if creationStmtsRaw, ok := data.GetOk("creation_statements"); ok {
role.Statements.Creation = creationStmtsRaw.([]string)
} else if operation == logical.CreateOperation {
role.Statements.Creation = data.Get("creation_statements").([]string)
}
if revocationStmtsRaw, ok := data.GetOk("revocation_statements"); ok {
role.Statements.Revocation = revocationStmtsRaw.([]string)
} else if operation == logical.CreateOperation {
role.Statements.Revocation = data.Get("revocation_statements").([]string)
}
if rollbackStmtsRaw, ok := data.GetOk("rollback_statements"); ok {
role.Statements.Rollback = rollbackStmtsRaw.([]string)
} else if operation == logical.CreateOperation {
role.Statements.Rollback = data.Get("rollback_statements").([]string)
}
if renewStmtsRaw, ok := data.GetOk("renew_statements"); ok {
role.Statements.Renewal = renewStmtsRaw.([]string)
} else if operation == logical.CreateOperation {
role.Statements.Renewal = data.Get("renew_statements").([]string)
}
// Do not persist deprecated statements that are populated on role read
role.Statements.CreationStatements = ""
role.Statements.RevocationStatements = ""
role.Statements.RenewStatements = ""
role.Statements.RollbackStatements = ""
}
role.Statements.Revocation = strutil.RemoveEmpty(role.Statements.Revocation)
return nil
}
type roleEntry struct {
DBName string `json:"db_name"`
Statements dbplugin.Statements `json:"statements"`
DefaultTTL time.Duration `json:"default_ttl"`
MaxTTL time.Duration `json:"max_ttl"`
DBName string `json:"db_name"`
Statements dbplugin.Statements `json:"statements"`
DefaultTTL time.Duration `json:"default_ttl"`
MaxTTL time.Duration `json:"max_ttl"`
StaticAccount *staticAccount `json:"static_account" mapstructure:"static_account"`
}
type staticAccount struct {
// Username to create or assume management for static accounts
Username string `json:"username"`
// Password is the current password for static accounts. As an input, this is
// used/required when trying to assume management of an existing static
// account. Return this on credential request if it exists.
Password string `json:"password"`
// LastVaultRotation represents the last time Vault rotated the password
LastVaultRotation time.Time `json:"last_vault_rotation"`
// RotationPeriod is number in seconds between each rotation, effectively a
// "time to live". This value is compared to the LastVaultRotation to
// determine if a password needs to be rotated
RotationPeriod time.Duration `json:"rotation_period"`
// RevokeUser is a boolean flag to indicate if Vault should revoke the
// database user when the role is deleted
RevokeUserOnDelete bool `json:"revoke_user_on_delete"`
}
// NextRotationTime calculates the next rotation by adding the Rotation Period
// to the last known vault rotation
func (s *staticAccount) NextRotationTime() time.Time {
return s.LastVaultRotation.Add(s.RotationPeriod)
}
// PasswordTTL calculates the approximate time remaining until the password is
// no longer valid. This is approximate because the periodic rotation is only
// checked approximately every 5 seconds, and each rotation can take a small
// amount of time to process. This can result in a negative TTL time while the
// rotation function processes the Static Role and performs the rotation. If the
// TTL is negative, zero is returned. Users should not trust passwords with a
// Zero TTL, as they are likely in the process of being rotated and will quickly
// be invalidated.
func (s *staticAccount) PasswordTTL() time.Duration {
next := s.NextRotationTime()
ttl := next.Sub(time.Now()).Round(time.Second)
if ttl < 0 {
ttl = time.Duration(0)
}
return ttl
}
const pathRoleHelpSyn = `
Manage the roles that can be created with this backend.
`
const pathStaticRoleHelpSyn = `
Manage the static roles that can be created with this backend.
`
const pathRoleHelpDesc = `
This path lets you manage the roles that can be created with this backend.
@@ -299,3 +656,43 @@ user.
The "rollback_statements' parameter customizes the statement string used to
rollback a change if needed.
`
const pathStaticRoleHelpDesc = `
This path lets you manage the static roles that can be created with this
backend. Static Roles are associated with a single database user, and manage the
password based on a rotation period, automatically rotating the password.
The "db_name" parameter is required and configures the name of the database
connection to use.
The "creation_statements" parameter customizes the string used to create the
credentials. This can be a sequence of SQL queries, or other statement formats
for a particular database type. Some substitution will be done to the statement
strings for certain keys. The names of the variables must be surrounded by "{{"
and "}}" to be replaced.
* "name" - The random username generated for the DB user.
* "password" - The random password generated for the DB user.
Example of a decent creation_statements for a postgresql database plugin:
CREATE ROLE "{{name}}" WITH
LOGIN
PASSWORD '{{password}}'
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
The "revocation_statements" parameter customizes the statement string used to
revoke a user. Example of a decent revocation_statements for a postgresql
database plugin:
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {{name}};
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {{name}};
REVOKE USAGE ON SCHEMA public FROM {{name}};
DROP ROLE IF EXISTS {{name}};
The "renew_statements" parameter customizes the statement string used to renew a
user.
The "rollback_statements' parameter customizes the statement string used to
rollback a change if needed.
`

View File

@@ -0,0 +1,526 @@
package database
import (
"context"
"errors"
"testing"
"time"
"github.com/go-test/deep"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/logical"
)
var dataKeys = []string{"username", "password", "last_vault_rotation", "rotation_period"}
func TestBackend_StaticRole_Config(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Test static role creation scenarios. Uses a map, so there is no guaranteed
// ordering, so each case cleans up by deleting the role
testCases := map[string]struct {
account map[string]interface{}
expected map[string]interface{}
err error
}{
"basic": {
account: map[string]interface{}{
"username": "statictest",
"rotation_period": "5400s",
},
expected: map[string]interface{}{
"username": "statictest",
"rotation_period": float64(5400),
},
},
"missing rotation period": {
account: map[string]interface{}{
"username": "statictest",
},
err: errors.New("rotation_period is required to create static accounts"),
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
data := map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"default_ttl": "5m",
"max_ttl": "10m",
}
for k, v := range tc.account {
data[k] = v
}
req := &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
if tc.err == nil {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
if err != nil && tc.err.Error() == err.Error() {
// errors match
return
}
if err == nil && tc.err.Error() == resp.Error().Error() {
// errors match
return
}
t.Fatalf("expected err message: (%s), got (%s), response error: (%s)", tc.err, err, resp.Error())
}
if tc.err != nil {
if err == nil || (resp == nil || !resp.IsError()) {
t.Fatal("expected error, got none")
}
}
// Read the role
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
expected := tc.expected
actual := make(map[string]interface{})
for _, key := range dataKeys {
if v, ok := resp.Data[key]; ok {
actual[key] = v
}
}
if len(tc.expected) > 0 {
// verify a password is returned, but we don't care what it's value is
if actual["password"] == "" {
t.Fatalf("expected result to contain password, but none found")
}
if v, ok := actual["last_vault_rotation"].(time.Time); !ok {
t.Fatalf("expected last_vault_rotation to be set to time.Time type, got: %#v", v)
}
// delete these values before the comparison, since we can't know them in
// advance
delete(actual, "password")
delete(actual, "last_vault_rotation")
if diff := deep.Equal(expected, actual); diff != nil {
t.Fatal(diff)
}
}
if len(tc.expected) == 0 && resp.Data["static_account"] != nil {
t.Fatalf("got unexpected static_account info: %#v", actual)
}
if diff := deep.Equal(resp.Data["db_name"], "plugin-test"); diff != nil {
t.Fatal(diff)
}
// Delete role for next run
req = &logical.Request{
Operation: logical.DeleteOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
})
}
}
func TestBackend_StaticRole_Updates(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
data = map[string]interface{}{
"name": "plugin-role-test-updates",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"default_ttl": "5m",
"max_ttl": "10m",
"username": "statictest",
"rotation_period": "5400s",
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test-updates",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Read the role
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-roles/plugin-role-test-updates",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
rotation := resp.Data["rotation_period"].(float64)
// capture the password to verify it doesn't change
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test-updates",
Storage: config.StorageView,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("expected both username/password, got (%s), (%s)", username, password)
}
// update rotation_period
updateData := map[string]interface{}{
"name": "plugin-role-test-updates",
"db_name": "plugin-test",
"username": "statictest",
"rotation_period": "6400s",
}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "static-roles/plugin-role-test-updates",
Storage: config.StorageView,
Data: updateData,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// re-read the role
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-roles/plugin-role-test-updates",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
newRotation := resp.Data["rotation_period"].(float64)
if newRotation == rotation {
t.Fatalf("expected change in rotation, but got old value: %#v", newRotation)
}
// re-capture the password to ensure it did not change
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test-updates",
Storage: config.StorageView,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
if username != resp.Data["username"].(string) {
t.Fatalf("usernames dont match!: (%s) / (%s)", username, resp.Data["username"].(string))
}
if password != resp.Data["password"].(string) {
t.Fatalf("passwords dont match!: (%s) / (%s)", password, resp.Data["password"].(string))
}
// verify that rotation_period is only required when creating
updateData = map[string]interface{}{
"name": "plugin-role-test-updates",
"db_name": "plugin-test",
"username": "statictest",
"rotation_statements": testRoleStaticUpdateRotation,
}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "static-roles/plugin-role-test-updates",
Storage: config.StorageView,
Data: updateData,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// verify updating static username returns an error
updateData = map[string]interface{}{
"name": "plugin-role-test-updates",
"db_name": "plugin-test",
"username": "statictestmodified",
}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "static-roles/plugin-role-test-updates",
Storage: config.StorageView,
Data: updateData,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || !resp.IsError() {
t.Fatal("expected error on updating name")
}
err = resp.Error()
if err.Error() != "cannot update static account username" {
t.Fatalf("expected error on updating name, got: %s", err)
}
}
func TestBackend_StaticRole_Role_name_check(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// non-static role
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"default_ttl": "5m",
"max_ttl": "10m",
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// create a static role with the same name, and expect failure
// static role
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if resp == nil || !resp.IsError() {
t.Fatalf("expected error, got none")
}
// repeat, with a static role first
data = map[string]interface{}{
"name": "plugin-role-test-2",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "testusername",
"rotation_period": "1h",
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test-2",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// create a non-static role with the same name, and expect failure
data = map[string]interface{}{
"name": "plugin-role-test-2",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"revocation_statements": defaultRevocationSQL,
"default_ttl": "5m",
"max_ttl": "10m",
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/plugin-role-test-2",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if resp == nil || !resp.IsError() {
t.Fatalf("expected error, got none")
}
}
const testRoleStaticCreate = `
CREATE ROLE "{{name}}" WITH
LOGIN
PASSWORD '{{password}}';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
`
const testRoleStaticUpdate = `
ALTER USER "{{name}}" WITH PASSWORD '{{password}}';
`
const testRoleStaticUpdateRotation = `
ALTER USER "{{name}}" WITH PASSWORD '{{password}}';GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
`

View File

@@ -3,27 +3,47 @@ package database
import (
"context"
"fmt"
"time"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/queue"
)
func pathRotateCredentials(b *databaseBackend) *framework.Path {
return &framework.Path{
Pattern: "rotate-root/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of this database connection",
func pathRotateCredentials(b *databaseBackend) []*framework.Path {
return []*framework.Path{
&framework.Path{
Pattern: "rotate-root/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of this database connection",
},
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathRotateCredentialsUpdate(),
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathRotateCredentialsUpdate(),
},
HelpSynopsis: pathCredsCreateReadHelpSyn,
HelpDescription: pathCredsCreateReadHelpDesc,
HelpSynopsis: pathCredsCreateReadHelpSyn,
HelpDescription: pathCredsCreateReadHelpDesc,
},
&framework.Path{
Pattern: "rotate-role/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the static role",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathRotateRoleCredentialsUpdate(),
},
HelpSynopsis: pathCredsCreateReadHelpSyn,
HelpDescription: pathCredsCreateReadHelpDesc,
},
}
}
@@ -77,6 +97,56 @@ func (b *databaseBackend) pathRotateCredentialsUpdate() framework.OperationFunc
return nil, nil
}
}
func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
if name == "" {
return logical.ErrorResponse("empty role name attribute given"), nil
}
role, err := b.StaticRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse("no static role found for role name"), nil
}
// In create/update of static accounts, we only care if the operation
// err'd , and this call does not return credentials
item, err := b.popFromRotationQueueByKey(name)
if err != nil {
item = &queue.Item{
Key: name,
}
}
resp, err := b.setStaticAccount(ctx, req.Storage, &setStaticAccountInput{
RoleName: name,
Role: role,
})
if err != nil {
b.logger.Warn("unable to rotate credentials in rotate-role", "error", err)
// Update the priority to re-try this rotation and re-add the item to
// the queue
item.Priority = time.Now().Add(10 * time.Second).Unix()
// Preserve the WALID if it was returned
if resp.WALID != "" {
item.Value = resp.WALID
}
} else {
item.Priority = resp.RotationTime.Add(role.StaticAccount.RotationPeriod).Unix()
}
// Add their rotation to the queue
if err := b.pushItem(item); err != nil {
return nil, err
}
return nil, nil
}
}
const pathRotateCredentialsUpdateHelpSyn = `
Request to rotate the root credentials for a certain database connection.
@@ -85,3 +155,10 @@ Request to rotate the root credentials for a certain database connection.
const pathRotateCredentialsUpdateHelpDesc = `
This path attempts to rotate the root credentials for the given database.
`
const pathRotateRoleCredentialsUpdateHelpSyn = `
Request to rotate the credentials for a static user account.
`
const pathRotateRoleCredentialsUpdateHelpDesc = `
This path attempts to rotate the credentials for the given static user account.
`

View File

@@ -0,0 +1,528 @@
package database
import (
"context"
"errors"
"fmt"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/locksutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/queue"
)
const (
// Interval to check the queue for items needing rotation
queueTickSeconds = 5
queueTickInterval = queueTickSeconds * time.Second
// WAL storage key used for static account rotations
staticWALKey = "staticRotationKey"
)
// populateQueue loads the priority queue with existing static accounts. This
// occurs at initialization, after any WAL entries of failed or interrupted
// rotations have been processed. It lists the roles from storage and searches
// for any that have an associated static account, then adds them to the
// priority queue for rotations.
func (b *databaseBackend) populateQueue(ctx context.Context, s logical.Storage) {
log := b.Logger()
log.Info("populating role rotation queue")
// Build map of role name / wal entries
walMap, err := b.loadStaticWALs(ctx, s)
if err != nil {
log.Warn("unable to load rotation WALs", "error", err)
}
roles, err := s.List(ctx, databaseStaticRolePath)
if err != nil {
log.Warn("unable to list role for enqueueing", "error", err)
return
}
for _, roleName := range roles {
select {
case <-ctx.Done():
log.Info("rotation queue restore cancelled")
return
default:
}
role, err := b.StaticRole(ctx, s, roleName)
if err != nil {
log.Warn("unable to read static role", "error", err, "role", roleName)
continue
}
item := queue.Item{
Key: roleName,
Priority: role.StaticAccount.LastVaultRotation.Add(role.StaticAccount.RotationPeriod).Unix(),
}
// Check if role name is in map
walEntry := walMap[roleName]
if walEntry != nil {
// Check walEntry last vault time
if !walEntry.LastVaultRotation.IsZero() && walEntry.LastVaultRotation.Before(role.StaticAccount.LastVaultRotation) {
// WAL's last vault rotation record is older than the role's data, so
// delete and move on
if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil {
log.Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID)
}
} else {
log.Info("adjusting priority for Role")
item.Value = walEntry.walID
item.Priority = time.Now().Unix()
}
}
if err := b.pushItem(&item); err != nil {
log.Warn("unable to enqueue item", "error", err, "role", roleName)
}
}
}
// runTicker kicks off a periodic ticker that invoke the automatic credential
// rotation method at a determined interval. The default interval is 5 seconds.
func (b *databaseBackend) runTicker(ctx context.Context, s logical.Storage) {
b.logger.Info("starting periodic ticker")
tick := time.NewTicker(queueTickInterval)
defer tick.Stop()
for {
select {
case <-tick.C:
b.rotateCredentials(ctx, s)
case <-ctx.Done():
b.logger.Info("stopping periodic ticker")
return
}
}
}
// setCredentialsWAL is used to store information in a WAL that can retry a
// credential setting or rotation in the event of partial failure.
type setCredentialsWAL struct {
NewPassword string `json:"new_password"`
OldPassword string `json:"old_password"`
RoleName string `json:"role_name"`
Username string `json:"username"`
LastVaultRotation time.Time `json:"last_vault_rotation"`
walID string
}
// rotateCredentials sets a new password for a static account. This method is
// invoked in the runTicker method, which is in it's own go-routine, and invoked
// periodically (approximately every 5 seconds).
//
// This method loops through the priority queue, popping the highest priority
// item until it encounters the first item that does not yet need rotation,
// based on the current time.
func (b *databaseBackend) rotateCredentials(ctx context.Context, s logical.Storage) error {
for {
// Quit rotating credentials if shutdown has started
select {
case <-ctx.Done():
return nil
default:
}
item, err := b.popFromRotationQueue()
if err != nil {
if err == queue.ErrEmpty {
return nil
}
return err
}
// Guard against possible nil item
if item == nil {
return nil
}
// Grab the exclusive lock for this Role, to make sure we don't incur and
// writes during the rotation process
lock := locksutil.LockForKey(b.roleLocks, item.Key)
lock.Lock()
defer lock.Unlock()
// Validate the role still exists
role, err := b.StaticRole(ctx, s, item.Key)
if err != nil {
b.logger.Error("unable to load role", "role", item.Key, "error", err)
item.Priority = time.Now().Add(10 * time.Second).Unix()
if err := b.pushItem(item); err != nil {
b.logger.Error("unable to push item on to queue", "error", err)
}
continue
}
if role == nil {
b.logger.Warn("role not found", "role", item.Key, "error", err)
continue
}
// If "now" is less than the Item priority, then this item does not need to
// be rotated
if time.Now().Unix() < item.Priority {
if err := b.pushItem(item); err != nil {
b.logger.Error("unable to push item on to queue", "error", err)
}
// Break out of the for loop
break
}
input := &setStaticAccountInput{
RoleName: item.Key,
Role: role,
}
// If there is a WAL entry related to this Role, the corresponding WAL ID
// should be stored in the Item's Value field.
if walID, ok := item.Value.(string); ok {
walEntry, err := b.findStaticWAL(ctx, s, walID)
if err != nil {
b.logger.Error("error finding static WAL", "error", err)
item.Priority = time.Now().Add(10 * time.Second).Unix()
if err := b.pushItem(item); err != nil {
b.logger.Error("unable to push item on to queue", "error", err)
}
}
if walEntry != nil && walEntry.NewPassword != "" {
input.Password = walEntry.NewPassword
input.WALID = walID
}
}
resp, err := b.setStaticAccount(ctx, s, input)
if err != nil {
b.logger.Error("unable to rotate credentials in periodic function", "error", err)
// Increment the priority enough so that the next call to this method
// likely will not attempt to rotate it, as a back-off of sorts
item.Priority = time.Now().Add(10 * time.Second).Unix()
// Preserve the WALID if it was returned
if resp != nil && resp.WALID != "" {
item.Value = resp.WALID
}
if err := b.pushItem(item); err != nil {
b.logger.Error("unable to push item on to queue", "error", err)
}
// Go to next item
continue
}
lvr := resp.RotationTime
if lvr.IsZero() {
lvr = time.Now()
}
// Update priority and push updated Item to the queue
nextRotation := lvr.Add(role.StaticAccount.RotationPeriod)
item.Priority = nextRotation.Unix()
if err := b.pushItem(item); err != nil {
b.logger.Warn("unable to push item on to queue", "error", err)
}
}
return nil
}
// findStaticWAL loads a WAL entry by ID. If found, only return the WAL if it
// is of type staticWALKey, otherwise return nil
func (b *databaseBackend) findStaticWAL(ctx context.Context, s logical.Storage, id string) (*setCredentialsWAL, error) {
wal, err := framework.GetWAL(ctx, s, id)
if err != nil {
return nil, err
}
if wal == nil || wal.Kind != staticWALKey {
return nil, nil
}
data := wal.Data.(map[string]interface{})
walEntry := setCredentialsWAL{
walID: id,
NewPassword: data["new_password"].(string),
OldPassword: data["old_password"].(string),
RoleName: data["role_name"].(string),
Username: data["username"].(string),
}
lvr, err := time.Parse(time.RFC3339, data["last_vault_rotation"].(string))
if err != nil {
return nil, err
}
walEntry.LastVaultRotation = lvr
return &walEntry, nil
}
type setStaticAccountInput struct {
RoleName string
Role *roleEntry
Password string
CreateUser bool
WALID string
}
type setStaticAccountOutput struct {
RotationTime time.Time
Password string
// Optional return field, in the event WAL was created and not destroyed
// during the operation
WALID string
}
// setStaticAccount sets the password for a static account associated with a
// Role. This method does many things:
// - verifies role exists and is in the allowed roles list
// - loads an existing WAL entry if WALID input is given, otherwise creates a
// new WAL entry
// - gets a database connection
// - accepts an input password, otherwise generates a new one via gRPC to the
// database plugin
// - sets new password for the static account
// - uses WAL for ensuring passwords are not lost if storage to Vault fails
//
// This method does not perform any operations on the priority queue. Those
// tasks must be handled outside of this method.
func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storage, input *setStaticAccountInput) (*setStaticAccountOutput, error) {
var merr error
if input == nil || input.Role == nil || input.RoleName == "" {
return nil, errors.New("input was empty when attempting to set credentials for static account")
}
// Re-use WAL ID if present, otherwise PUT a new WAL
output := &setStaticAccountOutput{WALID: input.WALID}
dbConfig, err := b.DatabaseConfig(ctx, s, input.Role.DBName)
if err != nil {
return output, err
}
// If role name isn't in the database's allowed roles, send back a
// permission denied.
if !strutil.StrListContains(dbConfig.AllowedRoles, "*") && !strutil.StrListContainsGlob(dbConfig.AllowedRoles, input.RoleName) {
return output, fmt.Errorf("%q is not an allowed role", input.RoleName)
}
// Get the Database object
db, err := b.GetConnection(ctx, s, input.Role.DBName)
if err != nil {
return output, err
}
db.RLock()
defer db.RUnlock()
// Use password from input if available. This happens if we're restoring from
// a WAL item or processing the rotation queue with an item that has a WAL
// associated with it
newPassword := input.Password
if newPassword == "" {
// Generate a new password
newPassword, err = db.GenerateCredentials(ctx)
if err != nil {
return output, err
}
}
output.Password = newPassword
config := dbplugin.StaticUserConfig{
Username: input.Role.StaticAccount.Username,
Password: newPassword,
}
if output.WALID == "" {
output.WALID, err = framework.PutWAL(ctx, s, staticWALKey, &setCredentialsWAL{
RoleName: input.RoleName,
Username: config.Username,
NewPassword: config.Password,
OldPassword: input.Role.StaticAccount.Password,
LastVaultRotation: input.Role.StaticAccount.LastVaultRotation,
})
if err != nil {
return output, errwrap.Wrapf("error writing WAL entry: {{err}}", err)
}
}
_, password, err := db.SetCredentials(ctx, input.Role.Statements, config)
if err != nil {
b.CloseIfShutdown(db, err)
return output, errwrap.Wrapf("error setting credentials: {{err}}", err)
}
if newPassword != password {
return output, errors.New("mismatch passwords returned")
}
// Store updated role information
// lvr is the known LastVaultRotation
lvr := time.Now()
input.Role.StaticAccount.LastVaultRotation = lvr
input.Role.StaticAccount.Password = password
output.RotationTime = lvr
entry, err := logical.StorageEntryJSON(databaseStaticRolePath+input.RoleName, input.Role)
if err != nil {
return output, err
}
if err := s.Put(ctx, entry); err != nil {
return output, err
}
// Cleanup WAL after successfully rotating and pushing new item on to queue
if err := framework.DeleteWAL(ctx, s, output.WALID); err != nil {
merr = multierror.Append(merr, err)
return output, merr
}
// The WAL has been deleted, return new setStaticAccountOutput without it
return &setStaticAccountOutput{RotationTime: lvr}, merr
}
// initQueue preforms the necessary checks and initializations needed to preform
// automatic credential rotation for roles associated with static accounts. This
// method verifies if a queue is needed (primary server or local mount), and if
// so initializes the queue and launches a go-routine to periodically invoke a
// method to preform the rotations.
//
// initQueue is invoked by the Factory method in a go-routine. The Factory does
// not wait for success or failure of it's tasks before continuing. This is to
// avoid blocking the mount process while loading and evaluating existing roles,
// etc.
func (b *databaseBackend) initQueue(ctx context.Context, conf *logical.BackendConfig) {
// Verify this mount is on the primary server, or is a local mount. If not, do
// not create a queue or launch a ticker. Both processing the WAL list and
// populating the queue are done sequentially and before launching a
// go-routine to run the periodic ticker.
replicationState := conf.System.ReplicationState()
if (conf.System.LocalMount() || !replicationState.HasState(consts.ReplicationPerformanceSecondary)) &&
!replicationState.HasState(consts.ReplicationDRSecondary) &&
!replicationState.HasState(consts.ReplicationPerformanceStandby) {
b.Logger().Info("initializing database rotation queue")
// Poll for a PutWAL call that does not return a "read-only storage" error.
// This ensures the startup phases of loading WAL entries from any possible
// failed rotations can complete without error when deleting from storage.
READONLY_LOOP:
for {
select {
case <-ctx.Done():
b.Logger().Info("queue initialization canceled")
return
default:
}
walID, err := framework.PutWAL(ctx, conf.StorageView, staticWALKey, &setCredentialsWAL{RoleName: "vault-readonlytest"})
if walID != "" {
defer framework.DeleteWAL(ctx, conf.StorageView, walID)
}
switch {
case err == nil:
break READONLY_LOOP
case err.Error() == logical.ErrSetupReadOnly.Error():
time.Sleep(10 * time.Millisecond)
default:
b.Logger().Error("deleting nil key resulted in error", "error", err)
return
}
}
// Load roles and populate queue with static accounts
b.populateQueue(ctx, conf.StorageView)
// Launch ticker
go b.runTicker(ctx, conf.StorageView)
}
}
// loadStaticWALs reads WAL entries and returns a map of roles and their
// setCredentialsWAL, if found.
func (b *databaseBackend) loadStaticWALs(ctx context.Context, s logical.Storage) (map[string]*setCredentialsWAL, error) {
keys, err := framework.ListWAL(ctx, s)
if err != nil {
return nil, err
}
if len(keys) == 0 {
b.Logger().Debug("no WAL entries found")
return nil, nil
}
walMap := make(map[string]*setCredentialsWAL)
// Loop through WAL keys and process any rotation ones
for _, walID := range keys {
walEntry, err := b.findStaticWAL(ctx, s, walID)
if err != nil {
b.Logger().Error("error loading static WAL", "id", walID, "error", err)
continue
}
if walEntry == nil {
continue
}
// Verify the static role still exists
roleName := walEntry.RoleName
role, err := b.StaticRole(ctx, s, roleName)
if err != nil {
b.Logger().Warn("unable to read static role", "error", err, "role", roleName)
continue
}
if role == nil || role.StaticAccount == nil {
if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil {
b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID)
}
continue
}
walEntry.walID = walID
walMap[walEntry.RoleName] = walEntry
}
return walMap, nil
}
// pushItem wraps the internal queue's Push call, to make sure a queue is
// actually available. This is needed because both runTicker and initQueue
// operate in go-routines, and could be accessing the queue concurrently
func (b *databaseBackend) pushItem(item *queue.Item) error {
b.RLock()
unlockFunc := b.RUnlock
defer func() { unlockFunc() }()
if b.credRotationQueue != nil {
return b.credRotationQueue.Push(item)
}
b.Logger().Warn("no queue found during push item")
return nil
}
// popFromRotationQueue wraps the internal queue's Pop call, to make sure a queue is
// actually available. This is needed because both runTicker and initQueue
// operate in go-routines, and could be accessing the queue concurrently
func (b *databaseBackend) popFromRotationQueue() (*queue.Item, error) {
b.RLock()
defer b.RUnlock()
if b.credRotationQueue != nil {
return b.credRotationQueue.Pop()
}
return nil, queue.ErrEmpty
}
// popFromRotationQueueByKey wraps the internal queue's PopByKey call, to make sure a queue is
// actually available. This is needed because both runTicker and initQueue
// operate in go-routines, and could be accessing the queue concurrently
func (b *databaseBackend) popFromRotationQueueByKey(name string) (*queue.Item, error) {
b.RLock()
defer b.RUnlock()
if b.credRotationQueue != nil {
return b.credRotationQueue.PopByKey(name)
}
return nil, queue.ErrEmpty
}

View File

@@ -0,0 +1,814 @@
package database
import (
"context"
"strings"
"testing"
"time"
"database/sql"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
_ "github.com/lib/pq"
)
func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
"rotation_period": "5400s",
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Read the creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("empty username (%s) or password (%s)", username, password)
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
// Re-read the creds, verifying they aren't changing on read
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
if username != resp.Data["username"].(string) || password != resp.Data["password"].(string) {
t.Fatal("expected re-read username/password to match, but didn't")
}
// Trigger rotation
data = map[string]interface{}{"name": "plugin-role-test"}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate-role/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
if resp != nil {
t.Fatalf("Expected empty response from rotate-role: (%#v)", resp)
}
// Re-Read the creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
newPassword := resp.Data["password"].(string)
if password == newPassword {
t.Fatalf("expected passwords to differ, got (%s)", newPassword)
}
// Verify new username/password
if err := verifyPgConn(t, username, newPassword, connURL); err != nil {
t.Fatal(err)
}
}
// Sanity check to make sure we don't allow an attempt of rotating credentials
// for non-static accounts, which doesn't make sense anyway, but doesn't hurt to
// verify we return an error
func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Read the creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("empty username (%s) or password (%s)", username, password)
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
// Trigger rotation
data = map[string]interface{}{"name": "plugin-role-test"}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate-role/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
// expect resp to be an error
resp, _ = b.HandleRequest(namespace.RootContext(nil), req)
if !resp.IsError() {
t.Fatalf("expected error rotating non-static role")
}
if resp.Error().Error() != "no static role found for role name" {
t.Fatalf("wrong error message: %s", err)
}
}
func TestBackend_StaticRole_Revoke_user(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
testCases := map[string]struct {
revoke *bool
expectVerifyErr bool
}{
// Default case: user does not specify, Vault leaves the database user
// untouched, and the final connection check passes because the user still
// exists
"unset": {},
// Revoke on delete. The final connection check should fail because the user
// no longer exists
"revoke": {
revoke: newBoolPtr(true),
expectVerifyErr: true,
},
// Revoke false, final connection check should still pass
"persist": {
revoke: newBoolPtr(false),
},
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
"rotation_period": "5400s",
}
if tc.revoke != nil {
data["revoke_user_on_delete"] = *tc.revoke
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Read the creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("empty username (%s) or password (%s)", username, password)
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
// delete the role, expect the default where the user is not destroyed
// Read the creds
req = &logical.Request{
Operation: logical.DeleteOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Verify new username/password still work
if err := verifyPgConn(t, username, password, connURL); err != nil {
if !tc.expectVerifyErr {
t.Fatal(err)
}
}
})
}
}
func verifyPgConn(t *testing.T, username, password, connURL string) error {
cURL := strings.Replace(connURL, "postgres:secret", username+":"+password, 1)
db, err := sql.Open("postgres", cURL)
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
return db.Close()
}
// WAL testing
//
// First scenario, WAL contains a role name that does not exist.
func TestBackend_Static_QueueWAL_discard_role_not_found(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
ctx := context.Background()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
_, err := framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{
RoleName: "doesnotexist",
})
if err != nil {
t.Fatalf("error with PutWAL: %s", err)
}
assertWALCount(t, config.StorageView, 1)
b, err := Factory(ctx, config)
if err != nil {
t.Fatal(err)
}
defer b.Cleanup(ctx)
time.Sleep(5 * time.Second)
bd := b.(*databaseBackend)
if bd.credRotationQueue == nil {
t.Fatal("database backend had no credential rotation queue")
}
// Verify empty queue
if bd.credRotationQueue.Len() != 0 {
t.Fatalf("expected zero queue items, got: %d", bd.credRotationQueue.Len())
}
assertWALCount(t, config.StorageView, 0)
}
// Second scenario, WAL contains a role name that does exist, but the role's
// LastVaultRotation is greater than the WAL has
func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
ctx := context.Background()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
roleName := "test-discard-by-date"
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Save Now() to make sure rotation time is after this, as well as the WAL
// time
roleTime := time.Now()
// Create role
data = map[string]interface{}{
"name": roleName,
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
// Low value here, to make sure the backend rotates this password at least
// once before we compare it to the WAL
"rotation_period": "10s",
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/" + roleName,
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Allow the first rotation to occur, setting LastVaultRotation
time.Sleep(time.Second * 12)
// Cleanup the backend, then create a WAL for the role with a
// LastVaultRotation of 1 hour ago, so that when we recreate the backend the
// WAL will be read but discarded
b.Cleanup(ctx)
b = nil
time.Sleep(time.Second * 3)
// Make a fake WAL entry with an older time
oldRotationTime := roleTime.Add(time.Hour * -1)
walPassword := "somejunkpassword"
_, err = framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{
RoleName: roleName,
NewPassword: walPassword,
LastVaultRotation: oldRotationTime,
Username: "statictest",
})
if err != nil {
t.Fatalf("error with PutWAL: %s", err)
}
assertWALCount(t, config.StorageView, 1)
// Reload backend
lb, err = Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok = lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(ctx)
// Allow enough time for populateQueue to work after boot
time.Sleep(time.Second * 12)
// PopulateQueue should have processed the entry
assertWALCount(t, config.StorageView, 0)
// Read the role
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-roles/" + roleName,
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
lastVaultRotation := resp.Data["last_vault_rotation"].(time.Time)
if !lastVaultRotation.After(oldRotationTime) {
t.Fatal("last vault rotation time not greater than WAL time")
}
if !lastVaultRotation.After(roleTime) {
t.Fatal("last vault rotation time not greater than role creation time")
}
// Grab password to verify it didn't change
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/" + roleName,
Storage: config.StorageView,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
password := resp.Data["password"].(string)
if password == walPassword {
t.Fatalf("expected password to not be changed by WAL, but was")
}
}
// Helper to assert the number of WAL entries is what we expect
func assertWALCount(t *testing.T, s logical.Storage, expected int) {
var count int
ctx := context.Background()
keys, err := framework.ListWAL(ctx, s)
if err != nil {
t.Fatal("error listing WALs")
}
// Loop through WAL keys and process any rotation ones
for _, k := range keys {
walEntry, _ := framework.GetWAL(ctx, s, k)
if walEntry == nil {
continue
}
if walEntry.Kind != staticWALKey {
continue
}
count++
}
if expected != count {
t.Fatalf("WAL count mismatch, expected (%d), got (%d)", expected, count)
}
}
//
// End WAL testing
//
func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
defer b.Cleanup(context.Background())
bd := b.(*databaseBackend)
if bd.credRotationQueue == nil {
t.Fatal("database backend had no credential rotation queue")
}
// Configure backend, add item and confirm length
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Create three static roles with different rotation periods
testCases := []string{"65", "130", "5400"}
for _, tc := range testCases {
roleName := "plugin-static-role-" + tc
data = map[string]interface{}{
"name": roleName,
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest" + tc,
"rotation_period": tc,
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/" + roleName,
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
}
// Verify the queue has 3 items in it
if bd.credRotationQueue.Len() != 3 {
t.Fatalf("expected 3 items in the rotation queue, got: (%d)", bd.credRotationQueue.Len())
}
// List the roles
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ListOperation,
Path: "static-roles/",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
keys := resp.Data["keys"].([]string)
if len(keys) != 3 {
t.Fatalf("expected 3 roles, got: (%d)", len(keys))
}
// Capture initial passwords, before the periodic function is triggered
pws := make(map[string][]string, 0)
pws = capturePasswords(t, b, config, testCases, pws)
// Sleep to make sure the 65s role will be up for rotation by the time the
// periodic function ticks
time.Sleep(7 * time.Second)
// Sleep 75 to make sure the periodic func has time to actually run
time.Sleep(75 * time.Second)
pws = capturePasswords(t, b, config, testCases, pws)
// Sleep more, this should allow both sr65 and sr130 to rotate
time.Sleep(140 * time.Second)
pws = capturePasswords(t, b, config, testCases, pws)
// Verify all pws are as they should
pass := true
for k, v := range pws {
switch {
case k == "plugin-static-role-65":
// expect all passwords to be different
if v[0] == v[1] || v[1] == v[2] || v[0] == v[2] {
pass = false
}
case k == "plugin-static-role-130":
// expect the first two to be equal, but different from the third
if v[0] != v[1] || v[0] == v[2] {
pass = false
}
case k == "plugin-static-role-5400":
// expect all passwords to be equal
if v[0] != v[1] || v[1] != v[2] {
pass = false
}
}
}
if !pass {
t.Fatalf("password rotations did not match expected: %#v", pws)
}
}
// capturePasswords captures the current passwords at the time of calling, and
// returns a map of username / passwords building off of the input map
func capturePasswords(t *testing.T, b logical.Backend, config *logical.BackendConfig, testCases []string, pws map[string][]string) map[string][]string {
new := make(map[string][]string, 0)
for _, tc := range testCases {
// Read the role
roleName := "plugin-static-role-" + tc
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/" + roleName,
Storage: config.StorageView,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("expected both username/password for (%s), got (%s), (%s)", roleName, username, password)
}
new[roleName] = append(new[roleName], password)
}
for k, v := range new {
pws[k] = append(pws[k], v...)
}
return pws
}
func newBoolPtr(b bool) *bool {
v := b
return &v
}

View File

@@ -239,3 +239,12 @@ func (c *Cassandra) RotateRootCredentials(ctx context.Context, statements []stri
c.rawConfig["password"] = password
return c.rawConfig, nil
}
// GenerateCredentials returns a generated password
func (c *Cassandra) GenerateCredentials(ctx context.Context) (string, error) {
password, err := c.GeneratePassword()
if err != nil {
return "", err
}
return password, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/gocql/gocql"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/database/helper/connutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/helper/certutil"
@@ -278,3 +279,13 @@ func (c *cassandraConnectionProducer) secretValues() map[string]interface{} {
c.PemJSON: "[pem_json]",
}
}
// SetCredentials uses provided information to set/create a user in the
// database. Unlike CreateUser, this method requires a username be provided and
// uses the name given, instead of generating a name. This is used for creating
// and setting the password of static accounts, as well as rolling back
// passwords in the database in the event an updated database fails to save in
// Vault's storage.
func (c *cassandraConnectionProducer) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) {
return "", "", dbutil.Unimplemented()
}

View File

@@ -293,3 +293,12 @@ func (h *HANA) revokeUserDefault(ctx context.Context, username string) error {
func (h *HANA) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) {
return nil, errors.New("root credentaion rotation is not currently implemented in this database secrets engine")
}
// GenerateCredentials returns a generated password
func (h *HANA) GenerateCredentials(ctx context.Context) (string, error) {
password, err := h.GeneratePassword()
if err != nil {
return "", err
}
return password, nil
}

View File

@@ -8,7 +8,9 @@ import (
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/database/helper/connutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/parseutil"
"github.com/hashicorp/vault/sdk/helper/tlsutil"
@@ -261,3 +263,13 @@ func isUserAdmin(cli influx.Client, user string) (bool, error) {
}
return false, fmt.Errorf("the provided username is not a valid user in the influxdb")
}
// SetCredentials uses provided information to set/create a user in the
// database. Unlike CreateUser, this method requires a username be provided and
// uses the name given, instead of generating a name. This is used for creating
// and setting the password of static accounts, as well as rolling back
// passwords in the database in the event an updated database fails to save in
// Vault's storage.
func (i *influxdbConnectionProducer) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) {
return "", "", dbutil.Unimplemented()
}

View File

@@ -242,3 +242,12 @@ func (i *Influxdb) RotateRootCredentials(ctx context.Context, statements []strin
i.rawConfig["password"] = password
return i.rawConfig, nil
}
// GenerateCredentials returns a generated password
func (i *Influxdb) GenerateCredentials(ctx context.Context) (string, error) {
password, err := i.GeneratePassword()
if err != nil {
return "", err
}
return password, nil
}

View File

@@ -15,6 +15,7 @@ import (
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/database/helper/connutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/mitchellh/mapstructure"
@@ -153,6 +154,16 @@ func (c *mongoDBConnectionProducer) Close() error {
return nil
}
// SetCredentials uses provided information to set/create a user in the
// database. Unlike CreateUser, this method requires a username be provided and
// uses the name given, instead of generating a name. This is used for creating
// and setting the password of static accounts, as well as rolling back
// passwords in the database in the event an updated database fails to save in
// Vault's storage.
func (c *mongoDBConnectionProducer) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) {
return "", "", dbutil.Unimplemented()
}
func parseMongoURL(rawURL string) (*mgo.DialInfo, error) {
url, err := url.Parse(rawURL)
if err != nil {

View File

@@ -224,3 +224,12 @@ func (m *MongoDB) RevokeUser(ctx context.Context, statements dbplugin.Statements
func (m *MongoDB) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) {
return nil, errors.New("root credential rotation is not currently implemented in this database secrets engine")
}
// GenerateCredentials returns a generated password
func (m *MongoDB) GenerateCredentials(ctx context.Context) (string, error) {
password, err := m.GeneratePassword()
if err != nil {
return "", err
}
return password, nil
}

View File

@@ -381,3 +381,12 @@ END
const rotateRootCredentialsSQL = `
ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}'
`
// GenerateCredentials returns a generated password
func (m *MSSQL) GenerateCredentials(ctx context.Context) (string, error) {
password, err := m.GeneratePassword()
if err != nil {
return "", err
}
return password, nil
}

View File

@@ -315,3 +315,12 @@ func (m *MySQL) RotateRootCredentials(ctx context.Context, statements []string)
m.RawConfig["password"] = password
return m.RawConfig, nil
}
// GenerateCredentials returns a generated password
func (m *MySQL) GenerateCredentials(ctx context.Context) (string, error) {
password, err := m.GeneratePassword()
if err != nil {
return "", err
}
return password, nil
}

View File

@@ -26,6 +26,10 @@ ALTER ROLE "{{name}}" VALID UNTIL '{{expiration}}';
`
defaultPostgresRotateRootCredentialsSQL = `
ALTER ROLE "{{username}}" WITH PASSWORD '{{password}}';
`
defaultPostgresRotateCredentialsSQL = `
ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}';
`
)
@@ -88,6 +92,86 @@ func (p *PostgreSQL) getConnection(ctx context.Context) (*sql.DB, error) {
return db.(*sql.DB), nil
}
// SetCredentials uses provided information to set/create a user in the
// database. Unlike CreateUser, this method requires a username be provided and
// uses the name given, instead of generating a name. This is used for creating
// and setting the password of static accounts, as well as rolling back
// passwords in the database in the event an updated database fails to save in
// Vault's storage.
func (p *PostgreSQL) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) {
if len(statements.Creation) == 0 {
return "", "", errors.New("empty creation statements")
}
username = staticUser.Username
password = staticUser.Password
if username == "" || password == "" {
return "", "", errors.New("must provide both username and password")
}
// Grab the lock
p.Lock()
defer p.Unlock()
// Get the connection
db, err := p.getConnection(ctx)
if err != nil {
return "", "", err
}
// Check if the role exists
var exists bool
err = db.QueryRowContext(ctx, "SELECT exists (SELECT rolname FROM pg_roles WHERE rolname=$1);", username).Scan(&exists)
if err != nil && err != sql.ErrNoRows {
return "", "", err
}
// Default to using Creation statements, which are required by the Vault
// backend. If the user exists, use the rotation statements, using the default
// ones if there are none provided
stmts := statements.Creation
if exists {
stmts = statements.Rotation
if len(stmts) == 0 {
stmts = []string{defaultPostgresRotateCredentialsSQL}
}
}
// Start a transaction
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return "", "", err
}
defer func() {
_ = tx.Rollback()
}()
// Execute each query
for _, stmt := range stmts {
for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}
m := map[string]string{
"name": staticUser.Username,
"password": password,
}
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
return "", "", err
}
}
}
// Commit the transaction
if err := tx.Commit(); err != nil {
return "", "", err
}
return username, password, nil
}
func (p *PostgreSQL) CreateUser(ctx context.Context, statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) {
statements = dbutil.StatementCompatibilityHelper(statements)
@@ -129,7 +213,6 @@ func (p *PostgreSQL) CreateUser(ctx context.Context, statements dbplugin.Stateme
defer func() {
tx.Rollback()
}()
// Return the secret
// Execute each query
for _, stmt := range statements.Creation {
@@ -267,7 +350,7 @@ func (p *PostgreSQL) defaultRevokeUser(ctx context.Context, username string) err
return err
}
if exists == false {
if !exists {
return nil
}
@@ -424,3 +507,12 @@ func (p *PostgreSQL) RotateRootCredentials(ctx context.Context, statements []str
p.RawConfig["password"] = password
return p.RawConfig, nil
}
// GenerateCredentials returns a generated password
func (p *PostgreSQL) GenerateCredentials(ctx context.Context) (string, error) {
password, err := p.GeneratePassword()
if err != nil {
return "", err
}
return password, nil
}

View File

@@ -317,6 +317,84 @@ func TestPostgreSQL_RevokeUser(t *testing.T) {
}
}
func TestPostgresSQL_SetCredentials(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t)
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
db := new()
_, err := db.Init(context.Background(), connectionDetails, true)
if err != nil {
t.Fatalf("err: %s", err)
}
password, err := db.GenerateCredentials(context.Background())
if err != nil {
t.Fatal(err)
}
usernameConfig := dbplugin.StaticUserConfig{
Username: "test",
Password: password,
}
// Test with no configured Creation Statement
username, password, err := db.SetCredentials(context.Background(), dbplugin.Statements{}, usernameConfig)
if err == nil {
t.Fatalf("err: %s", err)
}
statements := dbplugin.Statements{
Creation: []string{testPostgresStaticRole},
}
// User should not exist, make sure we can create
username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
if err := testCredsExist(t, connURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
// call SetCredentials again, the user will already exist, password will
// change. Without rotation statements, this should use the defaults
newPassword, _ := db.GenerateCredentials(context.Background())
usernameConfig.Password = newPassword
username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
if password != newPassword {
t.Fatal("passwords should have changed")
}
if err := testCredsExist(t, connURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
// generate a new password and supply owr own rotation statements
newPassword2, _ := db.GenerateCredentials(context.Background())
usernameConfig.Password = newPassword2
statements.Rotation = []string{testPostgresStaticRoleRotate, testPostgresStaticRoleGrant}
username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
if password != newPassword2 {
t.Fatal("passwords should have changed")
}
if err := testCredsExist(t, connURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
}
func testCredsExist(t testing.TB, connURL, username, password string) error {
t.Helper()
// Log in with the new creds
@@ -398,3 +476,18 @@ REVOKE USAGE ON SCHEMA public FROM "{{name}}";
DROP ROLE IF EXISTS "{{name}}";
`
const testPostgresStaticRole = `
CREATE ROLE "{{name}}" WITH
LOGIN
PASSWORD '{{password}}';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
`
const testPostgresStaticRoleRotate = `
ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}';
`
const testPostgresStaticRoleGrant = `
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
`

View File

@@ -9,6 +9,8 @@ import (
proto "github.com/golang/protobuf/proto"
timestamp "github.com/golang/protobuf/ptypes/timestamp"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
@@ -327,6 +329,7 @@ type Statements struct {
Revocation []string `protobuf:"bytes,6,rep,name=revocation,proto3" json:"revocation,omitempty"`
Rollback []string `protobuf:"bytes,7,rep,name=rollback,proto3" json:"rollback,omitempty"`
Renewal []string `protobuf:"bytes,8,rep,name=renewal,proto3" json:"renewal,omitempty"`
Rotation []string `protobuf:"bytes,9,rep,name=rotation,proto3" json:"rotation,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@@ -417,6 +420,13 @@ func (m *Statements) GetRenewal() []string {
return nil
}
func (m *Statements) GetRotation() []string {
if m != nil {
return m.Rotation
}
return nil
}
type UsernameConfig struct {
DisplayName string `protobuf:"bytes,1,opt,name=DisplayName,proto3" json:"DisplayName,omitempty"`
RoleName string `protobuf:"bytes,2,opt,name=RoleName,proto3" json:"RoleName,omitempty"`
@@ -659,6 +669,194 @@ func (m *Empty) XXX_DiscardUnknown() {
var xxx_messageInfo_Empty proto.InternalMessageInfo
type GenerateCredentialsResponse struct {
Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *GenerateCredentialsResponse) Reset() { *m = GenerateCredentialsResponse{} }
func (m *GenerateCredentialsResponse) String() string { return proto.CompactTextString(m) }
func (*GenerateCredentialsResponse) ProtoMessage() {}
func (*GenerateCredentialsResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_cfa445f4444c6876, []int{13}
}
func (m *GenerateCredentialsResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_GenerateCredentialsResponse.Unmarshal(m, b)
}
func (m *GenerateCredentialsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_GenerateCredentialsResponse.Marshal(b, m, deterministic)
}
func (m *GenerateCredentialsResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_GenerateCredentialsResponse.Merge(m, src)
}
func (m *GenerateCredentialsResponse) XXX_Size() int {
return xxx_messageInfo_GenerateCredentialsResponse.Size(m)
}
func (m *GenerateCredentialsResponse) XXX_DiscardUnknown() {
xxx_messageInfo_GenerateCredentialsResponse.DiscardUnknown(m)
}
var xxx_messageInfo_GenerateCredentialsResponse proto.InternalMessageInfo
func (m *GenerateCredentialsResponse) GetPassword() string {
if m != nil {
return m.Password
}
return ""
}
type StaticUserConfig struct {
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
Create bool `protobuf:"varint,3,opt,name=create,proto3" json:"create,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *StaticUserConfig) Reset() { *m = StaticUserConfig{} }
func (m *StaticUserConfig) String() string { return proto.CompactTextString(m) }
func (*StaticUserConfig) ProtoMessage() {}
func (*StaticUserConfig) Descriptor() ([]byte, []int) {
return fileDescriptor_cfa445f4444c6876, []int{14}
}
func (m *StaticUserConfig) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_StaticUserConfig.Unmarshal(m, b)
}
func (m *StaticUserConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_StaticUserConfig.Marshal(b, m, deterministic)
}
func (m *StaticUserConfig) XXX_Merge(src proto.Message) {
xxx_messageInfo_StaticUserConfig.Merge(m, src)
}
func (m *StaticUserConfig) XXX_Size() int {
return xxx_messageInfo_StaticUserConfig.Size(m)
}
func (m *StaticUserConfig) XXX_DiscardUnknown() {
xxx_messageInfo_StaticUserConfig.DiscardUnknown(m)
}
var xxx_messageInfo_StaticUserConfig proto.InternalMessageInfo
func (m *StaticUserConfig) GetUsername() string {
if m != nil {
return m.Username
}
return ""
}
func (m *StaticUserConfig) GetPassword() string {
if m != nil {
return m.Password
}
return ""
}
func (m *StaticUserConfig) GetCreate() bool {
if m != nil {
return m.Create
}
return false
}
type SetCredentialsRequest struct {
Statements *Statements `protobuf:"bytes,1,opt,name=statements,proto3" json:"statements,omitempty"`
StaticUserConfig *StaticUserConfig `protobuf:"bytes,2,opt,name=static_user_config,json=staticUserConfig,proto3" json:"static_user_config,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SetCredentialsRequest) Reset() { *m = SetCredentialsRequest{} }
func (m *SetCredentialsRequest) String() string { return proto.CompactTextString(m) }
func (*SetCredentialsRequest) ProtoMessage() {}
func (*SetCredentialsRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_cfa445f4444c6876, []int{15}
}
func (m *SetCredentialsRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SetCredentialsRequest.Unmarshal(m, b)
}
func (m *SetCredentialsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SetCredentialsRequest.Marshal(b, m, deterministic)
}
func (m *SetCredentialsRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_SetCredentialsRequest.Merge(m, src)
}
func (m *SetCredentialsRequest) XXX_Size() int {
return xxx_messageInfo_SetCredentialsRequest.Size(m)
}
func (m *SetCredentialsRequest) XXX_DiscardUnknown() {
xxx_messageInfo_SetCredentialsRequest.DiscardUnknown(m)
}
var xxx_messageInfo_SetCredentialsRequest proto.InternalMessageInfo
func (m *SetCredentialsRequest) GetStatements() *Statements {
if m != nil {
return m.Statements
}
return nil
}
func (m *SetCredentialsRequest) GetStaticUserConfig() *StaticUserConfig {
if m != nil {
return m.StaticUserConfig
}
return nil
}
type SetCredentialsResponse struct {
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SetCredentialsResponse) Reset() { *m = SetCredentialsResponse{} }
func (m *SetCredentialsResponse) String() string { return proto.CompactTextString(m) }
func (*SetCredentialsResponse) ProtoMessage() {}
func (*SetCredentialsResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_cfa445f4444c6876, []int{16}
}
func (m *SetCredentialsResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SetCredentialsResponse.Unmarshal(m, b)
}
func (m *SetCredentialsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SetCredentialsResponse.Marshal(b, m, deterministic)
}
func (m *SetCredentialsResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_SetCredentialsResponse.Merge(m, src)
}
func (m *SetCredentialsResponse) XXX_Size() int {
return xxx_messageInfo_SetCredentialsResponse.Size(m)
}
func (m *SetCredentialsResponse) XXX_DiscardUnknown() {
xxx_messageInfo_SetCredentialsResponse.DiscardUnknown(m)
}
var xxx_messageInfo_SetCredentialsResponse proto.InternalMessageInfo
func (m *SetCredentialsResponse) GetUsername() string {
if m != nil {
return m.Username
}
return ""
}
func (m *SetCredentialsResponse) GetPassword() string {
if m != nil {
return m.Password
}
return ""
}
func init() {
proto.RegisterType((*InitializeRequest)(nil), "dbplugin.InitializeRequest")
proto.RegisterType((*InitRequest)(nil), "dbplugin.InitRequest")
@@ -673,6 +871,10 @@ func init() {
proto.RegisterType((*TypeResponse)(nil), "dbplugin.TypeResponse")
proto.RegisterType((*RotateRootCredentialsResponse)(nil), "dbplugin.RotateRootCredentialsResponse")
proto.RegisterType((*Empty)(nil), "dbplugin.Empty")
proto.RegisterType((*GenerateCredentialsResponse)(nil), "dbplugin.GenerateCredentialsResponse")
proto.RegisterType((*StaticUserConfig)(nil), "dbplugin.StaticUserConfig")
proto.RegisterType((*SetCredentialsRequest)(nil), "dbplugin.SetCredentialsRequest")
proto.RegisterType((*SetCredentialsResponse)(nil), "dbplugin.SetCredentialsResponse")
}
func init() {
@@ -680,52 +882,60 @@ func init() {
}
var fileDescriptor_cfa445f4444c6876 = []byte{
// 716 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xd1, 0x4e, 0xdb, 0x4a,
0x10, 0x95, 0x93, 0x00, 0xc9, 0x80, 0x80, 0xec, 0x05, 0x64, 0xf9, 0x72, 0xef, 0x45, 0xd6, 0x15,
0xa5, 0xaa, 0x6a, 0x57, 0xd0, 0x8a, 0x8a, 0x87, 0x56, 0x25, 0x54, 0x55, 0xa5, 0x8a, 0x87, 0x05,
0x5e, 0xaa, 0x4a, 0x68, 0xe3, 0x2c, 0x89, 0x85, 0xe3, 0x75, 0xbd, 0xeb, 0xd0, 0xf4, 0x07, 0xda,
0xcf, 0xe8, 0xe7, 0xf4, 0xb1, 0x9f, 0x54, 0x79, 0xe3, 0xf5, 0x6e, 0xe2, 0x50, 0x1e, 0x68, 0xdf,
0x3c, 0x3b, 0x73, 0x66, 0xce, 0x1c, 0xcf, 0xce, 0xc2, 0xff, 0xbc, 0x77, 0xed, 0xf7, 0x88, 0x20,
0x5d, 0xc2, 0xa9, 0xdf, 0xeb, 0x26, 0x51, 0xd6, 0x0f, 0xe3, 0xf2, 0xc4, 0x4b, 0x52, 0x26, 0x18,
0x6a, 0x2a, 0x87, 0xf3, 0x5f, 0x9f, 0xb1, 0x7e, 0x44, 0x7d, 0x79, 0xde, 0xcd, 0xae, 0x7c, 0x11,
0x0e, 0x29, 0x17, 0x64, 0x98, 0x4c, 0x42, 0xdd, 0x0f, 0xd0, 0x7e, 0x1b, 0x87, 0x22, 0x24, 0x51,
0xf8, 0x99, 0x62, 0xfa, 0x31, 0xa3, 0x5c, 0xa0, 0x2d, 0x58, 0x0c, 0x58, 0x7c, 0x15, 0xf6, 0x6d,
0x6b, 0xc7, 0xda, 0x5b, 0xc1, 0x85, 0x85, 0x1e, 0x41, 0x7b, 0x44, 0xd3, 0xf0, 0x6a, 0x7c, 0x19,
0xb0, 0x38, 0xa6, 0x81, 0x08, 0x59, 0x6c, 0xd7, 0x76, 0xac, 0xbd, 0x26, 0x5e, 0x9f, 0x38, 0x3a,
0xe5, 0xf9, 0x51, 0xcd, 0xb6, 0x5c, 0x0c, 0xcb, 0x79, 0xf6, 0xdf, 0x99, 0xd7, 0xfd, 0x6e, 0x41,
0xbb, 0x93, 0x52, 0x22, 0xe8, 0x05, 0xa7, 0xa9, 0x4a, 0xfd, 0x14, 0x80, 0x0b, 0x22, 0xe8, 0x90,
0xc6, 0x82, 0xcb, 0xf4, 0xcb, 0xfb, 0x1b, 0x9e, 0xd2, 0xc1, 0x3b, 0x2b, 0x7d, 0xd8, 0x88, 0x43,
0xaf, 0x60, 0x2d, 0xe3, 0x34, 0x8d, 0xc9, 0x90, 0x5e, 0x16, 0xcc, 0x6a, 0x12, 0x6a, 0x6b, 0xe8,
0x45, 0x11, 0xd0, 0x91, 0x7e, 0xbc, 0x9a, 0x4d, 0xd9, 0xe8, 0x08, 0x80, 0x7e, 0x4a, 0xc2, 0x94,
0x48, 0xd2, 0x75, 0x89, 0x76, 0xbc, 0x89, 0xec, 0x9e, 0x92, 0xdd, 0x3b, 0x57, 0xb2, 0x63, 0x23,
0xda, 0xfd, 0x66, 0xc1, 0x3a, 0xa6, 0x31, 0xbd, 0xb9, 0x7f, 0x27, 0x0e, 0x34, 0x15, 0x31, 0xd9,
0x42, 0x0b, 0x97, 0xf6, 0xbd, 0x28, 0x52, 0x68, 0x63, 0x3a, 0x62, 0xd7, 0xf4, 0x8f, 0x52, 0x74,
0x5f, 0xc0, 0x36, 0x66, 0x79, 0x28, 0x66, 0x4c, 0x74, 0x52, 0xda, 0xa3, 0x71, 0x3e, 0x93, 0x5c,
0x55, 0xfc, 0x77, 0xa6, 0x62, 0x7d, 0xaf, 0x65, 0xe6, 0x76, 0x7f, 0xd4, 0x00, 0x74, 0x59, 0x74,
0x00, 0x7f, 0x05, 0xf9, 0x88, 0x84, 0x2c, 0xbe, 0x9c, 0x61, 0xda, 0x3a, 0xae, 0xd9, 0x16, 0x46,
0xca, 0x6d, 0x80, 0x0e, 0x61, 0x33, 0xa5, 0x23, 0x16, 0x54, 0x60, 0xb5, 0x12, 0xb6, 0xa1, 0x03,
0xa6, 0xab, 0xa5, 0x2c, 0x8a, 0xba, 0x24, 0xb8, 0x36, 0x61, 0x75, 0x5d, 0x4d, 0xb9, 0x0d, 0xd0,
0x63, 0x58, 0x4f, 0xf3, 0x5f, 0x6f, 0x22, 0x1a, 0x25, 0x62, 0x4d, 0xfa, 0xce, 0xa6, 0xc4, 0x53,
0x94, 0xed, 0x05, 0xd9, 0x7e, 0x69, 0xe7, 0xe2, 0x68, 0x5e, 0xf6, 0xe2, 0x44, 0x1c, 0x7d, 0x92,
0x63, 0x15, 0x01, 0x7b, 0x69, 0x82, 0x55, 0x36, 0xb2, 0x61, 0x49, 0x96, 0x22, 0x91, 0xdd, 0x94,
0x2e, 0x65, 0xba, 0xa7, 0xb0, 0x3a, 0x3d, 0xfa, 0x68, 0x07, 0x96, 0x4f, 0x42, 0x9e, 0x44, 0x64,
0x7c, 0x9a, 0xff, 0x43, 0xa9, 0x26, 0x36, 0x8f, 0xf2, 0x4a, 0x98, 0x45, 0xf4, 0xd4, 0xf8, 0xc5,
0xca, 0x76, 0x77, 0x61, 0x65, 0xb2, 0x0b, 0x78, 0xc2, 0x62, 0x4e, 0x6f, 0x5b, 0x06, 0xee, 0x3b,
0x40, 0xe6, 0xf5, 0x2e, 0xa2, 0xcd, 0xe1, 0xb1, 0x66, 0xe6, 0xdb, 0x81, 0x66, 0x42, 0x38, 0xbf,
0x61, 0x69, 0x4f, 0x55, 0x55, 0xb6, 0xeb, 0xc2, 0xca, 0xf9, 0x38, 0xa1, 0x65, 0x1e, 0x04, 0x0d,
0x31, 0x4e, 0x54, 0x0e, 0xf9, 0xed, 0x1e, 0xc2, 0x3f, 0xb7, 0x0c, 0xdf, 0x1d, 0x54, 0x97, 0x60,
0xe1, 0xf5, 0x30, 0x11, 0xe3, 0xfd, 0x2f, 0x0d, 0x68, 0x9e, 0x14, 0x3b, 0x18, 0xf9, 0xd0, 0xc8,
0x4b, 0xa2, 0x35, 0x7d, 0x23, 0x64, 0x94, 0xb3, 0xa5, 0x0f, 0xa6, 0x38, 0xbd, 0x01, 0xd0, 0x1d,
0xa3, 0xbf, 0x75, 0x54, 0x65, 0xcd, 0x39, 0xdb, 0xf3, 0x9d, 0x45, 0xa2, 0xe7, 0xd0, 0x2a, 0xd7,
0x09, 0x72, 0x74, 0xe8, 0xec, 0x8e, 0x71, 0x66, 0xa9, 0xe5, 0x2b, 0x42, 0x5f, 0x73, 0x93, 0x42,
0xe5, 0xf2, 0x57, 0xb1, 0x03, 0xd8, 0x9c, 0x2b, 0x1f, 0xda, 0x35, 0xd2, 0xfc, 0xe2, 0x72, 0x3b,
0x0f, 0xee, 0x8c, 0x2b, 0xfa, 0x7b, 0x06, 0x8d, 0x7c, 0x84, 0xd0, 0xa6, 0x06, 0x18, 0xcf, 0x8b,
0xa9, 0xef, 0xd4, 0xa4, 0x3d, 0x84, 0x85, 0x4e, 0xc4, 0xf8, 0x9c, 0x3f, 0x52, 0xe9, 0xe5, 0x25,
0x80, 0x7e, 0x0e, 0x4d, 0x1d, 0x2a, 0x8f, 0x64, 0x05, 0xeb, 0xd6, 0xbf, 0xd6, 0xac, 0xe3, 0xfd,
0xf7, 0x4f, 0xfa, 0xa1, 0x18, 0x64, 0x5d, 0x2f, 0x60, 0x43, 0x7f, 0x40, 0xf8, 0x20, 0x0c, 0x58,
0x9a, 0xf8, 0x23, 0x92, 0x45, 0xc2, 0x9f, 0xfb, 0x7a, 0x77, 0x17, 0xe5, 0x0e, 0x3e, 0xf8, 0x19,
0x00, 0x00, 0xff, 0xff, 0xdb, 0x96, 0x8b, 0x5c, 0xdd, 0x07, 0x00, 0x00,
// 839 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x56, 0xdd, 0x8e, 0xdb, 0x44,
0x14, 0x96, 0xf3, 0xb3, 0x9b, 0x9c, 0x5d, 0xed, 0x26, 0xd3, 0x66, 0x65, 0xb9, 0x85, 0x46, 0x23,
0x28, 0x8b, 0x10, 0x31, 0xda, 0x82, 0x0a, 0xbd, 0x00, 0xd1, 0x14, 0x15, 0x24, 0x58, 0xa1, 0x49,
0x7b, 0x83, 0x90, 0xa2, 0x89, 0x33, 0x9b, 0x58, 0xeb, 0x78, 0x8c, 0x67, 0x92, 0x12, 0x9e, 0x80,
0x37, 0xe0, 0x96, 0x7b, 0x5e, 0x84, 0x87, 0xe1, 0x21, 0x90, 0xc7, 0x1e, 0x7b, 0xfc, 0xb3, 0xad,
0xd4, 0x85, 0x3b, 0x9f, 0x39, 0xe7, 0x3b, 0xf3, 0x9d, 0x5f, 0x0f, 0xbc, 0x27, 0x96, 0xd7, 0xee,
0x92, 0x4a, 0xba, 0xa0, 0x82, 0xb9, 0xcb, 0x45, 0x14, 0x6c, 0x57, 0x7e, 0x98, 0x9f, 0x4c, 0xa2,
0x98, 0x4b, 0x8e, 0x7a, 0x5a, 0xe1, 0x3c, 0x58, 0x71, 0xbe, 0x0a, 0x98, 0xab, 0xce, 0x17, 0xdb,
0x2b, 0x57, 0xfa, 0x1b, 0x26, 0x24, 0xdd, 0x44, 0xa9, 0x29, 0xfe, 0x19, 0x86, 0xdf, 0x85, 0xbe,
0xf4, 0x69, 0xe0, 0xff, 0xc6, 0x08, 0xfb, 0x65, 0xcb, 0x84, 0x44, 0x67, 0x70, 0xe0, 0xf1, 0xf0,
0xca, 0x5f, 0xd9, 0xd6, 0xd8, 0x3a, 0x3f, 0x26, 0x99, 0x84, 0x3e, 0x82, 0xe1, 0x8e, 0xc5, 0xfe,
0xd5, 0x7e, 0xee, 0xf1, 0x30, 0x64, 0x9e, 0xf4, 0x79, 0x68, 0xb7, 0xc6, 0xd6, 0x79, 0x8f, 0x0c,
0x52, 0xc5, 0x34, 0x3f, 0x7f, 0xd2, 0xb2, 0x2d, 0x4c, 0xe0, 0x28, 0xf1, 0xfe, 0x5f, 0xfa, 0xc5,
0x7f, 0x5b, 0x30, 0x9c, 0xc6, 0x8c, 0x4a, 0xf6, 0x52, 0xb0, 0x58, 0xbb, 0xfe, 0x14, 0x40, 0x48,
0x2a, 0xd9, 0x86, 0x85, 0x52, 0x28, 0xf7, 0x47, 0x17, 0x77, 0x27, 0x3a, 0x0f, 0x93, 0x59, 0xae,
0x23, 0x86, 0x1d, 0xfa, 0x1a, 0x4e, 0xb7, 0x82, 0xc5, 0x21, 0xdd, 0xb0, 0x79, 0xc6, 0xac, 0xa5,
0xa0, 0x76, 0x01, 0x7d, 0x99, 0x19, 0x4c, 0x95, 0x9e, 0x9c, 0x6c, 0x4b, 0x32, 0x7a, 0x02, 0xc0,
0x7e, 0x8d, 0xfc, 0x98, 0x2a, 0xd2, 0x6d, 0x85, 0x76, 0x26, 0x69, 0xda, 0x27, 0x3a, 0xed, 0x93,
0x17, 0x3a, 0xed, 0xc4, 0xb0, 0xc6, 0x7f, 0x5a, 0x30, 0x20, 0x2c, 0x64, 0xaf, 0x6e, 0x1f, 0x89,
0x03, 0x3d, 0x4d, 0x4c, 0x85, 0xd0, 0x27, 0xb9, 0x7c, 0x2b, 0x8a, 0x0c, 0x86, 0x84, 0xed, 0xf8,
0x35, 0xfb, 0x5f, 0x29, 0xe2, 0x2f, 0xe1, 0x3e, 0xe1, 0x89, 0x29, 0xe1, 0x5c, 0x4e, 0x63, 0xb6,
0x64, 0x61, 0xd2, 0x93, 0x42, 0xdf, 0xf8, 0x6e, 0xe5, 0xc6, 0xf6, 0x79, 0xdf, 0xf4, 0x8d, 0xff,
0x69, 0x01, 0x14, 0xd7, 0xa2, 0x47, 0x70, 0xc7, 0x4b, 0x5a, 0xc4, 0xe7, 0xe1, 0xbc, 0xc2, 0xb4,
0xff, 0xb4, 0x65, 0x5b, 0x04, 0x69, 0xb5, 0x01, 0x7a, 0x0c, 0xa3, 0x98, 0xed, 0xb8, 0x57, 0x83,
0xb5, 0x72, 0xd8, 0xdd, 0xc2, 0xa0, 0x7c, 0x5b, 0xcc, 0x83, 0x60, 0x41, 0xbd, 0x6b, 0x13, 0xd6,
0x2e, 0x6e, 0xd3, 0x6a, 0x03, 0xf4, 0x31, 0x0c, 0xe2, 0xa4, 0xf4, 0x26, 0xa2, 0x93, 0x23, 0x4e,
0x95, 0x6e, 0x56, 0x4a, 0x9e, 0xa6, 0x6c, 0x77, 0x55, 0xf8, 0xb9, 0x9c, 0x24, 0xa7, 0xe0, 0x65,
0x1f, 0xa4, 0xc9, 0x29, 0x4e, 0x12, 0xac, 0x26, 0x60, 0x1f, 0xa6, 0x58, 0x2d, 0x23, 0x1b, 0x0e,
0xd5, 0x55, 0x34, 0xb0, 0x7b, 0x4a, 0xa5, 0xc5, 0x14, 0x25, 0x53, 0x9f, 0x7d, 0x8d, 0x4a, 0x65,
0x7c, 0x09, 0x27, 0xe5, 0xb1, 0x40, 0x63, 0x38, 0x7a, 0xe6, 0x8b, 0x28, 0xa0, 0xfb, 0xcb, 0xa4,
0xbe, 0x2a, 0xd3, 0xc4, 0x3c, 0x4a, 0xfc, 0x11, 0x1e, 0xb0, 0x4b, 0xa3, 0xfc, 0x5a, 0xc6, 0x0f,
0xe1, 0x38, 0xdd, 0x13, 0x22, 0xe2, 0xa1, 0x60, 0x37, 0x2d, 0x0a, 0xfc, 0x3d, 0x20, 0x73, 0xf4,
0x33, 0x6b, 0xb3, 0xb1, 0xac, 0x4a, 0xef, 0x3b, 0xd0, 0x8b, 0xa8, 0x10, 0xaf, 0x78, 0xbc, 0xd4,
0xb7, 0x6a, 0x19, 0x63, 0x38, 0x7e, 0xb1, 0x8f, 0x58, 0xee, 0x07, 0x41, 0x47, 0xee, 0x23, 0xed,
0x43, 0x7d, 0xe3, 0xc7, 0xf0, 0xce, 0x0d, 0x8d, 0xf9, 0x06, 0xaa, 0x87, 0xd0, 0xfd, 0x66, 0x13,
0xc9, 0x3d, 0xfe, 0x02, 0xee, 0x3d, 0x67, 0x21, 0x8b, 0xa9, 0x64, 0x4d, 0x78, 0x93, 0xa0, 0x55,
0x21, 0xb8, 0x80, 0x41, 0xd2, 0x02, 0xbe, 0x97, 0x84, 0x9b, 0x25, 0xfa, 0x2d, 0x83, 0x55, 0x3c,
0x55, 0xea, 0x54, 0x5f, 0xf6, 0x48, 0x26, 0xe1, 0x3f, 0x2c, 0x18, 0xcd, 0x58, 0xd3, 0xcc, 0xbd,
0xdd, 0x94, 0x7f, 0x0b, 0x48, 0x28, 0xce, 0xf3, 0x84, 0x56, 0x79, 0xab, 0x3a, 0x65, 0xb4, 0x19,
0x17, 0x19, 0x88, 0xca, 0x09, 0xfe, 0x11, 0xce, 0xaa, 0xc4, 0x6e, 0x57, 0xf0, 0x8b, 0xbf, 0xba,
0xd0, 0x7b, 0x96, 0xfd, 0x2a, 0x91, 0x0b, 0x9d, 0xa4, 0xfa, 0xe8, 0xb4, 0x20, 0xa5, 0x0a, 0xe6,
0x9c, 0x15, 0x07, 0xa5, 0xf6, 0x78, 0x0e, 0x50, 0x34, 0x1f, 0xba, 0x57, 0x58, 0xd5, 0xfe, 0x46,
0xce, 0xfd, 0x66, 0x65, 0xe6, 0xe8, 0x73, 0xe8, 0xe7, 0x5b, 0x1f, 0x19, 0x39, 0xa9, 0xfe, 0x0a,
0x9c, 0x2a, 0xb5, 0x64, 0x93, 0x17, 0xdb, 0xd8, 0xa4, 0x50, 0xdb, 0xd1, 0x75, 0xec, 0x1a, 0x46,
0x8d, 0x9d, 0x8c, 0x1e, 0x1a, 0x6e, 0x5e, 0xb3, 0x83, 0x9d, 0x0f, 0xde, 0x68, 0x97, 0xc5, 0xf7,
0x19, 0x74, 0x92, 0x69, 0x46, 0xa3, 0x02, 0x60, 0xbc, 0x02, 0xcc, 0xfc, 0x96, 0x86, 0xfe, 0x43,
0xe8, 0x4e, 0x03, 0x2e, 0x1a, 0x2a, 0x52, 0x8b, 0x65, 0x06, 0x27, 0xe5, 0xd6, 0x40, 0x0f, 0x8c,
0xd6, 0x6a, 0xea, 0x66, 0x67, 0x7c, 0xb3, 0x41, 0x76, 0xff, 0x0f, 0x70, 0xa7, 0x61, 0x50, 0xeb,
0x6c, 0xde, 0x2f, 0x0e, 0x5e, 0x37, 0xd8, 0x5f, 0x01, 0x14, 0x2f, 0x2b, 0xb3, 0x56, 0xb5, 0xf7,
0x56, 0x2d, 0x3e, 0xdc, 0xfe, 0xbd, 0x65, 0x3d, 0xbd, 0xf8, 0xe9, 0x93, 0x95, 0x2f, 0xd7, 0xdb,
0xc5, 0xc4, 0xe3, 0x1b, 0x77, 0x4d, 0xc5, 0xda, 0xf7, 0x78, 0x1c, 0xb9, 0x3b, 0xba, 0x0d, 0xa4,
0xdb, 0xf8, 0x10, 0x5c, 0x1c, 0xa8, 0xdf, 0xf9, 0xa3, 0x7f, 0x03, 0x00, 0x00, 0xff, 0xff, 0xf7,
0xf5, 0x87, 0x73, 0x28, 0x0a, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@@ -747,6 +957,8 @@ type DatabaseClient interface {
RotateRootCredentials(ctx context.Context, in *RotateRootCredentialsRequest, opts ...grpc.CallOption) (*RotateRootCredentialsResponse, error)
Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error)
Close(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error)
SetCredentials(ctx context.Context, in *SetCredentialsRequest, opts ...grpc.CallOption) (*SetCredentialsResponse, error)
GenerateCredentials(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GenerateCredentialsResponse, error)
Initialize(ctx context.Context, in *InitializeRequest, opts ...grpc.CallOption) (*Empty, error)
}
@@ -821,6 +1033,24 @@ func (c *databaseClient) Close(ctx context.Context, in *Empty, opts ...grpc.Call
return out, nil
}
func (c *databaseClient) SetCredentials(ctx context.Context, in *SetCredentialsRequest, opts ...grpc.CallOption) (*SetCredentialsResponse, error) {
out := new(SetCredentialsResponse)
err := c.cc.Invoke(ctx, "/dbplugin.Database/SetCredentials", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *databaseClient) GenerateCredentials(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GenerateCredentialsResponse, error) {
out := new(GenerateCredentialsResponse)
err := c.cc.Invoke(ctx, "/dbplugin.Database/GenerateCredentials", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Deprecated: Do not use.
func (c *databaseClient) Initialize(ctx context.Context, in *InitializeRequest, opts ...grpc.CallOption) (*Empty, error) {
out := new(Empty)
@@ -840,9 +1070,46 @@ type DatabaseServer interface {
RotateRootCredentials(context.Context, *RotateRootCredentialsRequest) (*RotateRootCredentialsResponse, error)
Init(context.Context, *InitRequest) (*InitResponse, error)
Close(context.Context, *Empty) (*Empty, error)
SetCredentials(context.Context, *SetCredentialsRequest) (*SetCredentialsResponse, error)
GenerateCredentials(context.Context, *Empty) (*GenerateCredentialsResponse, error)
Initialize(context.Context, *InitializeRequest) (*Empty, error)
}
// UnimplementedDatabaseServer can be embedded to have forward compatible implementations.
type UnimplementedDatabaseServer struct {
}
func (*UnimplementedDatabaseServer) Type(ctx context.Context, req *Empty) (*TypeResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Type not implemented")
}
func (*UnimplementedDatabaseServer) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented")
}
func (*UnimplementedDatabaseServer) RenewUser(ctx context.Context, req *RenewUserRequest) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RenewUser not implemented")
}
func (*UnimplementedDatabaseServer) RevokeUser(ctx context.Context, req *RevokeUserRequest) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RevokeUser not implemented")
}
func (*UnimplementedDatabaseServer) RotateRootCredentials(ctx context.Context, req *RotateRootCredentialsRequest) (*RotateRootCredentialsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RotateRootCredentials not implemented")
}
func (*UnimplementedDatabaseServer) Init(ctx context.Context, req *InitRequest) (*InitResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Init not implemented")
}
func (*UnimplementedDatabaseServer) Close(ctx context.Context, req *Empty) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Close not implemented")
}
func (*UnimplementedDatabaseServer) SetCredentials(ctx context.Context, req *SetCredentialsRequest) (*SetCredentialsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetCredentials not implemented")
}
func (*UnimplementedDatabaseServer) GenerateCredentials(ctx context.Context, req *Empty) (*GenerateCredentialsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GenerateCredentials not implemented")
}
func (*UnimplementedDatabaseServer) Initialize(ctx context.Context, req *InitializeRequest) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Initialize not implemented")
}
func RegisterDatabaseServer(s *grpc.Server, srv DatabaseServer) {
s.RegisterService(&_Database_serviceDesc, srv)
}
@@ -973,6 +1240,42 @@ func _Database_Close_Handler(srv interface{}, ctx context.Context, dec func(inte
return interceptor(ctx, in, info, handler)
}
func _Database_SetCredentials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetCredentialsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DatabaseServer).SetCredentials(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/dbplugin.Database/SetCredentials",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DatabaseServer).SetCredentials(ctx, req.(*SetCredentialsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Database_GenerateCredentials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DatabaseServer).GenerateCredentials(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/dbplugin.Database/GenerateCredentials",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DatabaseServer).GenerateCredentials(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Database_Initialize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InitializeRequest)
if err := dec(in); err != nil {
@@ -1023,6 +1326,14 @@ var _Database_serviceDesc = grpc.ServiceDesc{
MethodName: "Close",
Handler: _Database_Close_Handler,
},
{
MethodName: "SetCredentials",
Handler: _Database_SetCredentials_Handler,
},
{
MethodName: "GenerateCredentials",
Handler: _Database_GenerateCredentials_Handler,
},
{
MethodName: "Initialize",
Handler: _Database_Initialize_Handler,

View File

@@ -18,7 +18,7 @@ message InitRequest {
}
message CreateUserRequest {
Statements statements = 1;
Statements statements = 1;
UsernameConfig username_config = 2;
google.protobuf.Timestamp expiration = 3;
}
@@ -44,14 +44,15 @@ message Statements {
// DEPRECATED, will be removed in 0.12
string revocation_statements = 2 [deprecated=true];
// DEPRECATED, will be removed in 0.12
string rollback_statements = 3 [deprecated=true];
string rollback_statements = 3 [deprecated=true];
// DEPRECATED, will be removed in 0.12
string renew_statements = 4 [deprecated=true];
repeated string creation = 5;
repeated string revocation = 6;
repeated string rollback = 7;
repeated string rollback = 7;
repeated string renewal = 8;
repeated string rotation = 9;
}
message UsernameConfig {
@@ -78,6 +79,26 @@ message RotateRootCredentialsResponse {
message Empty {}
message GenerateCredentialsResponse {
string password = 1;
}
message StaticUserConfig{
string username = 1;
string password = 2;
bool create = 3;
}
message SetCredentialsRequest {
Statements statements = 1;
StaticUserConfig static_user_config = 2;
}
message SetCredentialsResponse {
string username = 1;
string password = 2;
}
service Database {
rpc Type(Empty) returns (TypeResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
@@ -86,6 +107,8 @@ service Database {
rpc RotateRootCredentials(RotateRootCredentialsRequest) returns (RotateRootCredentialsResponse);
rpc Init(InitRequest) returns (InitResponse);
rpc Close(Empty) returns (Empty);
rpc SetCredentials(SetCredentialsRequest) returns (SetCredentialsResponse);
rpc GenerateCredentials(Empty) returns (GenerateCredentialsResponse);
rpc Initialize(InitializeRequest) returns (Empty) {
option deprecated = true;

View File

@@ -86,6 +86,24 @@ func (mw *databaseTracingMiddleware) Close() (err error) {
return mw.next.Close()
}
func (mw *databaseTracingMiddleware) GenerateCredentials(ctx context.Context) (password string, err error) {
defer func(then time.Time) {
mw.logger.Trace("generate credentials", "status", "finished", "err", err, "took", time.Since(then))
}(time.Now())
mw.logger.Trace("generate credentials", "status", "started")
return mw.next.GenerateCredentials(ctx)
}
func (mw *databaseTracingMiddleware) SetCredentials(ctx context.Context, statements Statements, staticConfig StaticUserConfig) (username, password string, err error) {
defer func(then time.Time) {
mw.logger.Trace("set credentials", "status", "finished", "err", err, "took", time.Since(then))
}(time.Now())
mw.logger.Trace("set credentials", "status", "started")
return mw.next.SetCredentials(ctx, statements, staticConfig)
}
// ---- Metrics Middleware Domain ----
// databaseMetricsMiddleware wraps an implementation of Databases and on
@@ -201,6 +219,38 @@ func (mw *databaseMetricsMiddleware) Close() (err error) {
return mw.next.Close()
}
func (mw *databaseMetricsMiddleware) GenerateCredentials(ctx context.Context) (password string, err error) {
defer func(now time.Time) {
metrics.MeasureSince([]string{"database", "GenerateCredentials"}, now)
metrics.MeasureSince([]string{"database", mw.typeStr, "GenerateCredentials"}, now)
if err != nil {
metrics.IncrCounter([]string{"database", "GenerateCredentials", "error"}, 1)
metrics.IncrCounter([]string{"database", mw.typeStr, "GenerateCredentials", "error"}, 1)
}
}(time.Now())
metrics.IncrCounter([]string{"database", "GenerateCredentials"}, 1)
metrics.IncrCounter([]string{"database", mw.typeStr, "GenerateCredentials"}, 1)
return mw.next.GenerateCredentials(ctx)
}
func (mw *databaseMetricsMiddleware) SetCredentials(ctx context.Context, statements Statements, staticConfig StaticUserConfig) (username, password string, err error) {
defer func(now time.Time) {
metrics.MeasureSince([]string{"database", "SetCredentials"}, now)
metrics.MeasureSince([]string{"database", mw.typeStr, "SetCredentials"}, now)
if err != nil {
metrics.IncrCounter([]string{"database", "SetCredentials", "error"}, 1)
metrics.IncrCounter([]string{"database", mw.typeStr, "SetCredentials", "error"}, 1)
}
}(time.Now())
metrics.IncrCounter([]string{"database", "SetCredentials"}, 1)
metrics.IncrCounter([]string{"database", mw.typeStr, "SetCredentials"}, 1)
return mw.next.SetCredentials(ctx, statements, staticConfig)
}
// ---- Error Sanitizer Middleware Domain ----
// DatabaseErrorSanitizerMiddleware wraps an implementation of Databases and
@@ -273,3 +323,13 @@ func (mw *DatabaseErrorSanitizerMiddleware) sanitize(err error) error {
}
return err
}
func (mw *DatabaseErrorSanitizerMiddleware) GenerateCredentials(ctx context.Context) (password string, err error) {
password, err = mw.next.GenerateCredentials(ctx)
return password, mw.sanitize(err)
}
func (mw *DatabaseErrorSanitizerMiddleware) SetCredentials(ctx context.Context, statements Statements, staticConfig StaticUserConfig) (username, password string, err error) {
username, password, err = mw.next.SetCredentials(ctx, statements, staticConfig)
return username, password, mw.sanitize(err)
}

View File

@@ -15,7 +15,8 @@ import (
)
var (
ErrPluginShutdown = errors.New("plugin shutdown")
ErrPluginShutdown = errors.New("plugin shutdown")
ErrPluginStaticUnsupported = errors.New("database plugin does not support Static Accounts")
)
// ---- gRPC Server domain ----
@@ -115,6 +116,30 @@ func (s *gRPCServer) Close(_ context.Context, _ *Empty) (*Empty, error) {
return &Empty{}, nil
}
func (s *gRPCServer) GenerateCredentials(ctx context.Context, _ *Empty) (*GenerateCredentialsResponse, error) {
p, err := s.impl.GenerateCredentials(ctx)
if err != nil {
return nil, err
}
return &GenerateCredentialsResponse{
Password: p,
}, nil
}
func (s *gRPCServer) SetCredentials(ctx context.Context, req *SetCredentialsRequest) (*SetCredentialsResponse, error) {
username, password, err := s.impl.SetCredentials(ctx, *req.Statements, *req.StaticUserConfig)
if err != nil {
return nil, err
}
return &SetCredentialsResponse{
Username: username,
Password: password,
}, err
}
// ---- gRPC client domain ----
type gRPCClient struct {
@@ -283,3 +308,51 @@ func (c *gRPCClient) Close() error {
_, err := c.client.Close(c.doneCtx, &Empty{})
return err
}
func (c *gRPCClient) GenerateCredentials(ctx context.Context) (string, error) {
ctx, cancel := context.WithCancel(ctx)
quitCh := pluginutil.CtxCancelIfCanceled(cancel, c.doneCtx)
defer close(quitCh)
defer cancel()
resp, err := c.client.GenerateCredentials(ctx, &Empty{})
if err != nil {
grpcStatus, ok := status.FromError(err)
if ok && grpcStatus.Code() == codes.Unimplemented {
return "", ErrPluginStaticUnsupported
}
if c.doneCtx.Err() != nil {
return "", ErrPluginShutdown
}
return "", err
}
return resp.Password, nil
}
func (c *gRPCClient) SetCredentials(ctx context.Context, statements Statements, staticUser StaticUserConfig) (username, password string, err error) {
ctx, cancel := context.WithCancel(ctx)
quitCh := pluginutil.CtxCancelIfCanceled(cancel, c.doneCtx)
defer close(quitCh)
defer cancel()
resp, err := c.client.SetCredentials(ctx, &SetCredentialsRequest{
StaticUserConfig: &staticUser,
Statements: &statements,
})
if err != nil {
// Fall back to old call if not implemented
grpcStatus, ok := status.FromError(err)
if ok && grpcStatus.Code() == codes.Unimplemented {
return "", "", ErrPluginStaticUnsupported
}
if c.doneCtx.Err() != nil {
return "", "", ErrPluginShutdown
}
return "", "", err
}
return resp.Username, resp.Password, err
}

View File

@@ -44,6 +44,19 @@ type Database interface {
// the API.
RotateRootCredentials(ctx context.Context, statements []string) (config map[string]interface{}, err error)
// GenerateCredentials returns a generated password for the plugin. This is
// used in combination with SetCredentials to set a specific password for a
// database user and preserve the password in WAL entries.
GenerateCredentials(ctx context.Context) (string, error)
// SetCredentials uses provided information to create or set the credentials
// for a database user. Unlike CreateUser, this method requires both a
// username and a password given instead of generating them. This is used for
// creating and setting the password of static accounts, as well as rolling
// back passwords in the database in the event an updated database fails to
// save in Vault's storage.
SetCredentials(ctx context.Context, statements Statements, staticConfig StaticUserConfig) (username string, password string, err error)
// Init is called on `$ vault write database/config/:db-name`, or when you
// do a creds call after Vault's been restarted. The config provided won't
// hold all the keys and values provided in the API call, some will be

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/helper/parseutil"
"github.com/mitchellh/mapstructure"
@@ -162,3 +163,13 @@ func (c *SQLConnectionProducer) Close() error {
return nil
}
// SetCredentials uses provided information to set/create a user in the
// database. Unlike CreateUser, this method requires a username be provided and
// uses the name given, instead of generating a name. This is used for creating
// and setting the password of static accounts, as well as rolling back
// passwords in the database in the event an updated database fails to save in
// Vault's storage.
func (c *SQLConnectionProducer) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) {
return "", "", dbutil.Unimplemented()
}

View File

@@ -6,10 +6,13 @@ import (
"strings"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
ErrEmptyCreationStatement = errors.New("empty creation statements")
ErrEmptyRotationStatement = errors.New("empty rotation statements")
)
// Query templates a query for us.
@@ -50,3 +53,8 @@ func StatementCompatibilityHelper(statements dbplugin.Statements) dbplugin.State
}
return statements
}
// Unimplemented returns a gRPC error with the Unimplemented code
func Unimplemented() error {
return status.Error(codes.Unimplemented, "Not yet implemented")
}

View File

@@ -10,6 +10,8 @@ import (
timestamp "github.com/golang/protobuf/ptypes/timestamp"
logical "github.com/hashicorp/vault/sdk/logical"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
@@ -3036,6 +3038,32 @@ type BackendServer interface {
Type(context.Context, *Empty) (*TypeReply, error)
}
// UnimplementedBackendServer can be embedded to have forward compatible implementations.
type UnimplementedBackendServer struct {
}
func (*UnimplementedBackendServer) HandleRequest(ctx context.Context, req *HandleRequestArgs) (*HandleRequestReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method HandleRequest not implemented")
}
func (*UnimplementedBackendServer) SpecialPaths(ctx context.Context, req *Empty) (*SpecialPathsReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SpecialPaths not implemented")
}
func (*UnimplementedBackendServer) HandleExistenceCheck(ctx context.Context, req *HandleExistenceCheckArgs) (*HandleExistenceCheckReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method HandleExistenceCheck not implemented")
}
func (*UnimplementedBackendServer) Cleanup(ctx context.Context, req *Empty) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Cleanup not implemented")
}
func (*UnimplementedBackendServer) InvalidateKey(ctx context.Context, req *InvalidateKeyArgs) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method InvalidateKey not implemented")
}
func (*UnimplementedBackendServer) Setup(ctx context.Context, req *SetupArgs) (*SetupReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Setup not implemented")
}
func (*UnimplementedBackendServer) Type(ctx context.Context, req *Empty) (*TypeReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Type not implemented")
}
func RegisterBackendServer(s *grpc.Server, srv BackendServer) {
s.RegisterService(&_Backend_serviceDesc, srv)
}
@@ -3265,6 +3293,23 @@ type StorageServer interface {
Delete(context.Context, *StorageDeleteArgs) (*StorageDeleteReply, error)
}
// UnimplementedStorageServer can be embedded to have forward compatible implementations.
type UnimplementedStorageServer struct {
}
func (*UnimplementedStorageServer) List(ctx context.Context, req *StorageListArgs) (*StorageListReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (*UnimplementedStorageServer) Get(ctx context.Context, req *StorageGetArgs) (*StorageGetReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
}
func (*UnimplementedStorageServer) Put(ctx context.Context, req *StoragePutArgs) (*StoragePutReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Put not implemented")
}
func (*UnimplementedStorageServer) Delete(ctx context.Context, req *StorageDeleteArgs) (*StorageDeleteReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
}
func RegisterStorageServer(s *grpc.Server, srv StorageServer) {
s.RegisterService(&_Storage_serviceDesc, srv)
}
@@ -3555,6 +3600,44 @@ type SystemViewServer interface {
PluginEnv(context.Context, *Empty) (*PluginEnvReply, error)
}
// UnimplementedSystemViewServer can be embedded to have forward compatible implementations.
type UnimplementedSystemViewServer struct {
}
func (*UnimplementedSystemViewServer) DefaultLeaseTTL(ctx context.Context, req *Empty) (*TTLReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method DefaultLeaseTTL not implemented")
}
func (*UnimplementedSystemViewServer) MaxLeaseTTL(ctx context.Context, req *Empty) (*TTLReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method MaxLeaseTTL not implemented")
}
func (*UnimplementedSystemViewServer) SudoPrivilege(ctx context.Context, req *SudoPrivilegeArgs) (*SudoPrivilegeReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SudoPrivilege not implemented")
}
func (*UnimplementedSystemViewServer) Tainted(ctx context.Context, req *Empty) (*TaintedReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Tainted not implemented")
}
func (*UnimplementedSystemViewServer) CachingDisabled(ctx context.Context, req *Empty) (*CachingDisabledReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method CachingDisabled not implemented")
}
func (*UnimplementedSystemViewServer) ReplicationState(ctx context.Context, req *Empty) (*ReplicationStateReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReplicationState not implemented")
}
func (*UnimplementedSystemViewServer) ResponseWrapData(ctx context.Context, req *ResponseWrapDataArgs) (*ResponseWrapDataReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method ResponseWrapData not implemented")
}
func (*UnimplementedSystemViewServer) MlockEnabled(ctx context.Context, req *Empty) (*MlockEnabledReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method MlockEnabled not implemented")
}
func (*UnimplementedSystemViewServer) LocalMount(ctx context.Context, req *Empty) (*LocalMountReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method LocalMount not implemented")
}
func (*UnimplementedSystemViewServer) EntityInfo(ctx context.Context, req *EntityInfoArgs) (*EntityInfoReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method EntityInfo not implemented")
}
func (*UnimplementedSystemViewServer) PluginEnv(ctx context.Context, req *Empty) (*PluginEnvReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method PluginEnv not implemented")
}
func RegisterSystemViewServer(s *grpc.Server, srv SystemViewServer) {
s.RegisterService(&_SystemView_serviceDesc, srv)
}

View File

@@ -113,7 +113,7 @@ func (pq *PriorityQueue) Push(i *Item) error {
if _, ok := pq.dataMap[i.Key]; ok {
return ErrDuplicateItem
}
// copy the item value(s) so that modifications to the source item does not
// Copy the item value(s) so that modifications to the source item does not
// affect the item on the queue
clone, err := copystructure.Copy(i)
if err != nil {
@@ -126,8 +126,8 @@ func (pq *PriorityQueue) Push(i *Item) error {
}
// PopByKey searches the queue for an item with the given key and removes it
// from the queue if found. Returns ErrItemNotFound(key) if not found. This
// method must fix the queue after removal.
// from the queue if found. Returns nil if not found. This method must fix the
// queue after removing any key.
func (pq *PriorityQueue) PopByKey(key string) (*Item, error) {
pq.lock.Lock()
defer pq.lock.Unlock()
@@ -137,7 +137,7 @@ func (pq *PriorityQueue) PopByKey(key string) (*Item, error) {
return nil, nil
}
// remove the item the heap and delete it from the dataMap
// Remove the item the heap and delete it from the dataMap
itemRaw := heap.Remove(&pq.data, item.index)
delete(pq.dataMap, key)

View File

@@ -9,6 +9,8 @@ import (
proto "github.com/golang/protobuf/proto"
forwarding "github.com/hashicorp/vault/helper/forwarding"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
@@ -439,6 +441,20 @@ type RequestForwardingServer interface {
PerformanceStandbyElectionRequest(*PerfStandbyElectionInput, RequestForwarding_PerformanceStandbyElectionRequestServer) error
}
// UnimplementedRequestForwardingServer can be embedded to have forward compatible implementations.
type UnimplementedRequestForwardingServer struct {
}
func (*UnimplementedRequestForwardingServer) ForwardRequest(ctx context.Context, req *forwarding.Request) (*forwarding.Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method ForwardRequest not implemented")
}
func (*UnimplementedRequestForwardingServer) Echo(ctx context.Context, req *EchoRequest) (*EchoReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented")
}
func (*UnimplementedRequestForwardingServer) PerformanceStandbyElectionRequest(req *PerfStandbyElectionInput, srv RequestForwarding_PerformanceStandbyElectionRequestServer) error {
return status.Errorf(codes.Unimplemented, "method PerformanceStandbyElectionRequest not implemented")
}
func RegisterRequestForwardingServer(s *grpc.Server, srv RequestForwardingServer) {
s.RegisterService(&_RequestForwarding_serviceDesc, srv)
}

View File

@@ -397,3 +397,233 @@ $ curl \
}
}
```
## Create Static Role
This endpoint creates or updates a static role definition. Static Roles are a
1-to-1 mapping of a Vault Role to a user in a database which are automatically
rotated based on the configured `rotation_period`. Not all databases support
Static Roles, please see the database-specific documentation.
~> This endpoint distinguishes between `create` and `update` ACL capabilities.
| Method | Path |
| :--------------------------- | :--------------------- |
| `POST` | `/database/static-roles/:name` |
### Parameters
- `name` `(string: <required>)` Specifies the name of the role to create. This
is specified as part of the URL.
- `username` `(string: <required>)` Specifies the database username that this
Vault role corresponds to.
- `rotation_period` `(string/int: <required>)` Specifies the amount of time
Vault should wait before rotating the password. The minimum is 5 seconds.
- `db_name` `(string: <required>)` - The name of the database connection to use
for this role.
- `creation_statements` `(list: <required>)` Specifies the database
statements executed to create and configure a user. See the plugin's API page
for more information on support and formatting for this parameter.
- `revocation_statements` `(list: [])` Specifies the database statements to
be executed to revoke a user. See the plugin's API page for more information
on support and formatting for this parameter.
- `rollback_statements` `(list: [])` Specifies the database statements to be
executed rollback a create operation in the event of an error. Not every
plugin type will support this functionality. See the plugin's API page for
more information on support and formatting for this parameter.
- `renew_statements` `(list: [])` Specifies the database statements to be
executed to renew a user. Not every plugin type will support this
functionality. See the plugin's API page for more information on support and
formatting for this parameter.
- `rotation_statements` `(list: [])` Specifies the database statements to be
executed to rotate the password for the configured database user. Not every
plugin type will support this functionality. See the plugin's API page for
more information on support and formatting for this parameter.
- `revoke_user_on_delete` `(boolean: false)` Specifies if Vault should attempt
to revoke the database user associated with this static role, indicated by the
`username`. If `true`, when Vault deletes this Role it will attempt to revoke
the database user using the configured `revocation_statements` if they exist.
Default `false`
### Sample Payload
```json
{
"db_name": "mysql",
"username": "static-database-user",
"creation_statements": ["CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'", "GRANT SELECT ON *.* TO '{{name}}'@'%'"],
"rotation_statements": ["ALTER USER "{{name}}" WITH PASSWORD '{{password}}';"],
"rotation_period": "1h"
}
```
### Sample Request
```
$ curl \
--header "X-Vault-Token: ..." \
--request POST \
--data @payload.json \
http://127.0.0.1:8200/v1/database/static-roles/my-static-role
```
## Read Static Role
This endpoint queries the static role definition.
| Method | Path |
| :--------------------------- | :--------------------- |
| `GET` | `/database/static-roles/:name` |
### Parameters
- `name` `(string: <required>)` Specifies the name of the static role to read.
This is specified as part of the URL.
### Sample Request
```
$ curl \
--header "X-Vault-Token: ..." \
http://127.0.0.1:8200/v1/database/static-roles/my-static-role
```
### Sample Response
```json
{
"data": {
"db_name": "mysql",
"username":"static-user",
"creation_statements": ["CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';"], "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"],
"rotation_statements": ["ALTER USER "{{name}}" WITH PASSWORD '{{password}}';"],
"rotation_period":"1h",
"renew_statements": [],
"revocation_statements": [],
"rollback_statements": []
"revoke_user_on_delete": false,
},
}
```
## List Static Roles
This endpoint returns a list of available static roles. Only the role names are
returned, not any values.
| Method | Path |
| :--------------------------- | :--------------------- |
| `LIST` | `/database/static-roles` |
### Sample Request
```
$ curl \
--header "X-Vault-Token: ..." \
--request LIST \
http://127.0.0.1:8200/v1/database/static-roles
```
### Sample Response
```json
{
"auth": null,
"data": {
"keys": ["dev-static", "prod-static"]
}
}
```
## Delete Static Role
This endpoint deletes the static role definition and revokes the database user.
| Method | Path |
| :--------------------------- | :--------------------- |
| `DELETE` | `/database/static-roles/:name` |
### Parameters
- `name` `(string: <required>)` Specifies the name of the static role to
delete. This is specified as part of the URL.
### Sample Request
```
$ curl \
--header "X-Vault-Token: ..." \
--request DELETE \
http://127.0.0.1:8200/v1/database/static-roles/my-role
```
## Get Static Credentials
This endpoint returns the current credentials based on the named static role.
| Method | Path |
| :--------------------------- | :--------------------- |
| `GET` | `/database/static-creds/:name` |
### Parameters
- `name` `(string: <required>)` Specifies the name of the static role to get
credentials for. This is specified as part of the URL.
### Sample Request
```
$ curl \
--header "X-Vault-Token: ..." \
http://127.0.0.1:8200/v1/database/static-creds/my-static-role
```
### Sample Response
```json
{
"data": {
"username": "static-user",
"password": "132ae3ef-5a64-7499-351e-bfe59f3a2a21"
"last_vault_rotation": "2019-05-06T15:26:42.525302-05:00",
"rotation_period": 30,
"ttl": 28,
}
}
```
## Rotate Static Role Credentials
This endpoint is used to rotate the Static Role credentials stored for a given
role name. While Static Roles are rotated automatically by Vault at configured
rotation periods, users can use this endpoint to manually trigger a rotation to
change the stored password and reset the TTL of the Static Role's password.
| Method | Path |
| :---------------------------- | :--------------------- |
| `POST` | `/database/rotate-role/:name` |
### Parameters
- `name` `(string: <required>)` Specifies the name of the Static Role to
trigger the password rotation for. The name is specified as part of the URL.
### Sample Request
```
$ curl \
--header "X-Vault-Token: ..." \
--request POST \
http://127.0.0.1:8200/v1/database/rotate-role/my-static-role
```

View File

@@ -105,4 +105,10 @@ list the plugin does not support that statement type.
functionality. Must be a semicolon-separated string, a base64-encoded
semicolon-separated string, a serialized JSON string array, or a
base64-encoded serialized JSON string array. The '{{name}}' and
'{{expiration}}` values will be substituted.
'{{expiration}}' values will be substituted.
- `rotation_statements` `(list: [])` Specifies the database statements to be
executed to rotate the password for a given username. Must be a
semicolon-separated string, a base64-encoded semicolon-separated string, a
serialized JSON string array, or a base64-encoded serialized JSON string
array. The '{{name}}' and '{{password}}' values will be substituted.

View File

@@ -27,6 +27,22 @@ it down to the specific instance of a service based on the SQL username.
Vault makes use of its own internal revocation system to ensure that users
become invalid within a reasonable time of the lease expiring.
### Static Roles
The database secrets engine supports the concept of "static roles", which are
a 1-to-1 mapping of Vault Roles to usernames in a database. The current password
for the database user is stored and automatically rotated by Vault on a
configurable period of time. This is in contrast to dynamic secrets, where a
unique username and password pair are generated with each credential request.
When credentials are requested for the Role, Vault returns the current
password for the configured database user, allowing anyone with the proper
Vault policies to have access to the user account in the database.
Not all database types support static roles at this time. Please consult the
specific database documentation on the left navigation to see if a given
database backend supports static roles.
## Setup
Most secrets engines must be configured in advance before they can perform their

View File

@@ -13,7 +13,8 @@ description: |-
PostgreSQL is one of the supported plugins for the database secrets engine. This
plugin generates database credentials dynamically based on configured roles for
the PostgreSQL database.
the PostgreSQL database, and also supports [Static
Roles](/docs/secrets/databases/index.html#static-roles).
See the [database secrets engine](/docs/secrets/databases/index.html) docs for
more information about setting up the database secrets engine.