diff --git a/builtin/logical/database/backend.go b/builtin/logical/database/backend.go index b28f8fe6f630..804a98d35bec 100644 --- a/builtin/logical/database/backend.go +++ b/builtin/logical/database/backend.go @@ -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() diff --git a/builtin/logical/database/backend_test.go b/builtin/logical/database/backend_test.go index 07c93382b9e0..e7f186d335ae 100644 --- a/builtin/logical/database/backend_test.go +++ b/builtin/logical/database/backend_test.go @@ -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{ diff --git a/builtin/logical/database/dbplugin/plugin_test.go b/builtin/logical/database/dbplugin/plugin_test.go index e076cc4811c6..2f0667b3dd93 100644 --- a/builtin/logical/database/dbplugin/plugin_test.go +++ b/builtin/logical/database/dbplugin/plugin_test.go @@ -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, diff --git a/builtin/logical/database/path_creds_create.go b/builtin/logical/database/path_creds_create.go index 2eaf79e09321..e66d6d13e4a0 100644 --- a/builtin/logical/database/path_creds_create.go +++ b/builtin/logical/database/path_creds_create.go @@ -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, }, + &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.", + }, + }, - HelpSynopsis: pathCredsCreateReadHelpSyn, - HelpDescription: pathCredsCreateReadHelpDesc, + 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. +` diff --git a/builtin/logical/database/path_roles.go b/builtin/logical/database/path_roles.go index 1d518111550b..b6ce344c7ef9 100644 --- a/builtin/logical/database/path_roles.go +++ b/builtin/logical/database/path_roles.go @@ -2,263 +2,620 @@ 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/?$", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathRoleList, + }, - HelpSynopsis: pathRoleHelpSyn, - HelpDescription: pathRoleHelpDesc, + 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.`, - }, + HelpSynopsis: pathRoleHelpSyn, + HelpDescription: pathRoleHelpDesc, + }, - "default_ttl": { - Type: framework.TypeDurationSecond, - Description: "Default ttl for role.", + &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, }, - "max_ttl": { - Type: framework.TypeDurationSecond, - Description: "Maximum time a credential is valid for", - }, + HelpSynopsis: pathStaticRoleHelpSyn, + HelpDescription: pathStaticRoleHelpDesc, }, + } +} + +// 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 + } - 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(), + return fields +} + +// 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.", }, - HelpSynopsis: pathRoleHelpSyn, - HelpDescription: pathRoleHelpDesc, + "max_ttl": { + Type: framework.TypeDurationSecond, + Description: "Maximum time a credential is valid for", + }, } + return fields } -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 - } +// 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 +} - return role != nil, nil +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) 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 - } +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 +} - return 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) 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)) +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 + } + + // 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{} + db.RLock() + defer db.RUnlock() + + if err := db.RevokeUser(ctx, role.Statements, role.StaticAccount.Username); err != nil { + b.CloseIfShutdown(db, err) + return nil, err } - if len(role.Statements.Revocation) == 0 { - data["revocation_statements"] = []string{} + } + + err = req.Storage.Delete(ctx, databaseStaticRolePath+name) + if err != nil { + return nil, err + } + + return nil, 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 } - if len(role.Statements.Rollback) == 0 { - data["rollback_statements"] = []string{} + data["revoke_user_on_delete"] = role.StaticAccount.RevokeUserOnDelete + } + + 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 len(role.Statements.Renewal) == 0 { - data["renew_statements"] = []string{} + 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 } + } - return &logical.Response{ - Data: data, - }, nil + // 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) 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 +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 + } - return logical.ListResponse(entries), nil + 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 } -} -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 + 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, err := b.Role(ctx, req.Storage, data.Get("name").(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{} + // 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 } - - // 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 - } + if err := req.Storage.Put(ctx, entry); err != nil { + return nil, err } - // 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 - } + // 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 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 = "" + // 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) } - role.Statements.Revocation = strutil.RemoveEmpty(role.Statements.Revocation) + 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) + } - // Store it - entry, err := logical.StorageEntryJSON("role/"+name, role) - if err != nil { - return nil, err + 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 err := req.Storage.Put(ctx, entry); err != nil { - return nil, err + + 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) } - return nil, nil + // 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. +` diff --git a/builtin/logical/database/path_roles_test.go b/builtin/logical/database/path_roles_test.go new file mode 100644 index 000000000000..085c5cf34d7e --- /dev/null +++ b/builtin/logical/database/path_roles_test.go @@ -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}}"; +` diff --git a/builtin/logical/database/path_rotate_credentials.go b/builtin/logical/database/path_rotate_credentials.go index a80c17892f58..bc82b7adbf8a 100644 --- a/builtin/logical/database/path_rotate_credentials.go +++ b/builtin/logical/database/path_rotate_credentials.go @@ -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, }, + &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", + }, + }, - HelpSynopsis: pathCredsCreateReadHelpSyn, - HelpDescription: pathCredsCreateReadHelpDesc, + 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. +` diff --git a/builtin/logical/database/rotation.go b/builtin/logical/database/rotation.go new file mode 100644 index 000000000000..46759cbec681 --- /dev/null +++ b/builtin/logical/database/rotation.go @@ -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 +} diff --git a/builtin/logical/database/rotation_test.go b/builtin/logical/database/rotation_test.go new file mode 100644 index 000000000000..bec4c5a613e9 --- /dev/null +++ b/builtin/logical/database/rotation_test.go @@ -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 +} diff --git a/plugins/database/cassandra/cassandra.go b/plugins/database/cassandra/cassandra.go index 44c889709372..001570ffb297 100644 --- a/plugins/database/cassandra/cassandra.go +++ b/plugins/database/cassandra/cassandra.go @@ -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 +} diff --git a/plugins/database/cassandra/connection_producer.go b/plugins/database/cassandra/connection_producer.go index 87579de4e49d..bebff4ef059f 100644 --- a/plugins/database/cassandra/connection_producer.go +++ b/plugins/database/cassandra/connection_producer.go @@ -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() +} diff --git a/plugins/database/hana/hana.go b/plugins/database/hana/hana.go index a2af57526785..872177a03761 100644 --- a/plugins/database/hana/hana.go +++ b/plugins/database/hana/hana.go @@ -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 +} diff --git a/plugins/database/influxdb/connection_producer.go b/plugins/database/influxdb/connection_producer.go index 52bd8e9455ef..84f93bc81453 100644 --- a/plugins/database/influxdb/connection_producer.go +++ b/plugins/database/influxdb/connection_producer.go @@ -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() +} diff --git a/plugins/database/influxdb/influxdb.go b/plugins/database/influxdb/influxdb.go index e77adf5f16db..7a1520d9d4e1 100644 --- a/plugins/database/influxdb/influxdb.go +++ b/plugins/database/influxdb/influxdb.go @@ -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 +} diff --git a/plugins/database/mongodb/connection_producer.go b/plugins/database/mongodb/connection_producer.go index 847e8fa0ea05..20912531ff8e 100644 --- a/plugins/database/mongodb/connection_producer.go +++ b/plugins/database/mongodb/connection_producer.go @@ -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 { diff --git a/plugins/database/mongodb/mongodb.go b/plugins/database/mongodb/mongodb.go index 607aff71b18c..f3aa6e216a8c 100644 --- a/plugins/database/mongodb/mongodb.go +++ b/plugins/database/mongodb/mongodb.go @@ -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 +} diff --git a/plugins/database/mssql/mssql.go b/plugins/database/mssql/mssql.go index dfc34c1b43e2..b525be8cbc81 100644 --- a/plugins/database/mssql/mssql.go +++ b/plugins/database/mssql/mssql.go @@ -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 +} diff --git a/plugins/database/mysql/mysql.go b/plugins/database/mysql/mysql.go index bf349aaea432..f3967558309e 100644 --- a/plugins/database/mysql/mysql.go +++ b/plugins/database/mysql/mysql.go @@ -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 +} diff --git a/plugins/database/postgresql/postgresql.go b/plugins/database/postgresql/postgresql.go index 4be5418cb248..94d3650ac55f 100644 --- a/plugins/database/postgresql/postgresql.go +++ b/plugins/database/postgresql/postgresql.go @@ -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 +} diff --git a/plugins/database/postgresql/postgresql_test.go b/plugins/database/postgresql/postgresql_test.go index 85c632563083..cd803b84567a 100644 --- a/plugins/database/postgresql/postgresql_test.go +++ b/plugins/database/postgresql/postgresql_test.go @@ -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}}"; +` diff --git a/sdk/database/dbplugin/database.pb.go b/sdk/database/dbplugin/database.pb.go index 58688e91d42b..c820015136cc 100644 --- a/sdk/database/dbplugin/database.pb.go +++ b/sdk/database/dbplugin/database.pb.go @@ -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, diff --git a/sdk/database/dbplugin/database.proto b/sdk/database/dbplugin/database.proto index c3f4831b0c32..d8c208099b36 100644 --- a/sdk/database/dbplugin/database.proto +++ b/sdk/database/dbplugin/database.proto @@ -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; diff --git a/sdk/database/dbplugin/databasemiddleware.go b/sdk/database/dbplugin/databasemiddleware.go index ba2dd4e5c4a0..19cfa3374b62 100644 --- a/sdk/database/dbplugin/databasemiddleware.go +++ b/sdk/database/dbplugin/databasemiddleware.go @@ -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) +} diff --git a/sdk/database/dbplugin/grpc_transport.go b/sdk/database/dbplugin/grpc_transport.go index 1b3fe7f47383..bfd848021c2e 100644 --- a/sdk/database/dbplugin/grpc_transport.go +++ b/sdk/database/dbplugin/grpc_transport.go @@ -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 +} diff --git a/sdk/database/dbplugin/plugin.go b/sdk/database/dbplugin/plugin.go index 6d248d15af00..957cf3f489ca 100644 --- a/sdk/database/dbplugin/plugin.go +++ b/sdk/database/dbplugin/plugin.go @@ -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 diff --git a/sdk/database/helper/connutil/sql.go b/sdk/database/helper/connutil/sql.go index 44917e0d5667..05de1b4ffa4e 100644 --- a/sdk/database/helper/connutil/sql.go +++ b/sdk/database/helper/connutil/sql.go @@ -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() +} diff --git a/sdk/database/helper/dbutil/dbutil.go b/sdk/database/helper/dbutil/dbutil.go index 725338112d18..84b98d188967 100644 --- a/sdk/database/helper/dbutil/dbutil.go +++ b/sdk/database/helper/dbutil/dbutil.go @@ -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") +} diff --git a/sdk/plugin/pb/backend.pb.go b/sdk/plugin/pb/backend.pb.go index 28ff0c455ea7..bfa10aaf699e 100644 --- a/sdk/plugin/pb/backend.pb.go +++ b/sdk/plugin/pb/backend.pb.go @@ -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) } diff --git a/sdk/queue/priority_queue.go b/sdk/queue/priority_queue.go index 22a45e5d15b9..a0fda087f9be 100644 --- a/sdk/queue/priority_queue.go +++ b/sdk/queue/priority_queue.go @@ -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) diff --git a/vault/request_forwarding_service.pb.go b/vault/request_forwarding_service.pb.go index 7a6957d0ed2d..2c638ab083da 100644 --- a/vault/request_forwarding_service.pb.go +++ b/vault/request_forwarding_service.pb.go @@ -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) } diff --git a/website/source/api/secret/databases/index.html.md b/website/source/api/secret/databases/index.html.md index 6a33cfd4a800..55d67a160cc6 100644 --- a/website/source/api/secret/databases/index.html.md +++ b/website/source/api/secret/databases/index.html.md @@ -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: )` – Specifies the name of the role to create. This + is specified as part of the URL. + +- `username` `(string: )` – Specifies the database username that this + Vault role corresponds to. + +- `rotation_period` `(string/int: )` – Specifies the amount of time + Vault should wait before rotating the password. The minimum is 5 seconds. + +- `db_name` `(string: )` - The name of the database connection to use + for this role. + +- `creation_statements` `(list: )` – 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: )` – 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: )` – 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: )` – 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: )` – 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 +``` diff --git a/website/source/api/secret/databases/postgresql.html.md b/website/source/api/secret/databases/postgresql.html.md index 3d0b67244046..2a688ebe0c4b 100644 --- a/website/source/api/secret/databases/postgresql.html.md +++ b/website/source/api/secret/databases/postgresql.html.md @@ -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. diff --git a/website/source/docs/secrets/databases/index.html.md b/website/source/docs/secrets/databases/index.html.md index 90b394514d6e..8691f2f7cc8b 100644 --- a/website/source/docs/secrets/databases/index.html.md +++ b/website/source/docs/secrets/databases/index.html.md @@ -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 diff --git a/website/source/docs/secrets/databases/postgresql.html.md b/website/source/docs/secrets/databases/postgresql.html.md index 9d35705a0f74..45b3523d1642 100644 --- a/website/source/docs/secrets/databases/postgresql.html.md +++ b/website/source/docs/secrets/databases/postgresql.html.md @@ -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.