mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
`
|
||||
|
||||
@@ -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.
|
||||
`
|
||||
|
||||
526
builtin/logical/database/path_roles_test.go
Normal file
526
builtin/logical/database/path_roles_test.go
Normal 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}}";
|
||||
`
|
||||
@@ -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.
|
||||
`
|
||||
|
||||
528
builtin/logical/database/rotation.go
Normal file
528
builtin/logical/database/rotation.go
Normal 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
|
||||
}
|
||||
814
builtin/logical/database/rotation_test.go
Normal file
814
builtin/logical/database/rotation_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}}";
|
||||
`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user