mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 10:37:56 +00:00 
			
		
		
		
	Combined Database Backend: Static Accounts (#6834)
* Add priority queue to sdk * fix issue of storing pointers and now copy * update to use copy structure * Remove file, put Item struct def. into other file * add link * clean up docs * refactor internal data structure to hide heap method implementations. Other cleanup after feedback * rename PushItem and PopItem to just Push/Pop, after encapsulating the heap methods * updates after feedback * refactoring/renaming * guard against pushing a nil item * minor updates after feedback * Add SetCredentials, GenerateCredentials gRPC methods to combined database backend gPRC * Initial Combined database backend implementation of static accounts and automatic rotation * vendor updates * initial implementation of static accounts with Combined database backend, starting with PostgreSQL implementation * add lock and setup of rotation queue * vendor the queue * rebase on new method signature of queue * remove mongo tests for now * update default role sql * gofmt after rebase * cleanup after rebasing to remove checks for ErrNotFound error * rebase cdcr-priority-queue * vendor dependencies with 'go mod vendor' * website database docs for Static Role support * document the rotate-role API endpoint * postgres specific static role docs * use constants for paths * updates from review * remove dead code * combine and clarify error message for older plugins * Update builtin/logical/database/backend.go Co-Authored-By: Jim Kalafut <jim@kalafut.net> * cleanups from feedback * code and comment cleanups * move db.RLock higher to protect db.GenerateCredentials call * Return output with WALID if we failed to delete the WAL * Update builtin/logical/database/path_creds_create.go Co-Authored-By: Jim Kalafut <jim@kalafut.net> * updates after running 'make fmt' * update after running 'make proto' * Update builtin/logical/database/path_roles.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * Update builtin/logical/database/path_roles.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * update comment and remove and rearrange some dead code * Update website/source/api/secret/databases/index.html.md Co-Authored-By: Jim Kalafut <jim@kalafut.net> * cleanups after review * Update sdk/database/dbplugin/grpc_transport.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * code cleanup after feedback * remove PasswordLastSet; it's not used * document GenerateCredentials and SetCredentials * Update builtin/logical/database/path_rotate_credentials.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * wrap pop and popbykey in backend methods to protect against nil cred rotation queue * use strings.HasPrefix instead of direct equality check for path * Forgot to commit this * updates after feedback * re-purpose an outdated test to now check that static and dynamic roles cannot share a name * check for unique name across dynamic and static roles * refactor loadStaticWALs to return a map of name/setCredentialsWAL struct to consolidate where we're calling set credentials * remove commented out code * refactor to have loadstaticwals filter out wals for roles that no longer exist * return error if nil input given * add nil check for input into setStaticAccount * Update builtin/logical/database/path_roles.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * add constant for queue tick time in seconds, used for comparrison in updates * Update builtin/logical/database/path_roles.go Co-Authored-By: Jim Kalafut <jim@kalafut.net> * code cleanup after review * remove misplaced code comment * remove commented out code * create a queue in the Factory method, even if it's never used * update path_roles to use a common set of fields, with specific overrides for dynamic/static roles by type * document new method * move rotation things into a specific file * rename test file and consolidate some static account tests * Update builtin/logical/database/path_roles.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * Update builtin/logical/database/rotation.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * Update builtin/logical/database/rotation.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * Update builtin/logical/database/rotation.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * Update builtin/logical/database/rotation.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * Update builtin/logical/database/rotation.go Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com> * update code comments, method names, and move more methods into rotation.go * update comments to be capitalized * remove the item from the queue before we try to destroy it * findStaticWAL returns an error * use lowercase keys when encoding WAL entries * small cleanups * remove vestigial static account check * remove redundant DeleteWAL call in populate queue * if we error on loading role, push back to queue with 10 second backoff * poll in initqueue to make sure the backend is setup and can write/delete data * add revoke_user_on_delete flag to allow users to opt-in to revoking the static database user on delete of the Vault role. Default false * add code comments on read-only loop * code comment updates * re-push if error returned from find static wal * add locksutil and acquire locks when pop'ing from the queue * grab exclusive locks for updating static roles * Add SetCredentials and GenerateCredentials stubs to mockPlugin * add a switch in initQueue to listen for cancelation * remove guard on zero time, it should have no affect * create a new context in Factory to pass on and use for closing the backend queue * restore master copy of vendor dir
This commit is contained in:
		| @@ -14,11 +14,17 @@ import ( | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/dbutil" | ||||
| 	"github.com/hashicorp/vault/sdk/framework" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/locksutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/strutil" | ||||
| 	"github.com/hashicorp/vault/sdk/logical" | ||||
| 	"github.com/hashicorp/vault/sdk/queue" | ||||
| ) | ||||
|  | ||||
| const databaseConfigPath = "database/config/" | ||||
| const ( | ||||
| 	databaseConfigPath     = "database/config/" | ||||
| 	databaseRolePath       = "role/" | ||||
| 	databaseStaticRolePath = "static-role/" | ||||
| ) | ||||
|  | ||||
| type dbPluginInstance struct { | ||||
| 	sync.RWMutex | ||||
| @@ -46,6 +52,15 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, | ||||
| 	if err := b.Setup(ctx, conf); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	b.credRotationQueue = queue.New() | ||||
| 	// Create a context with a cancel method for processing any WAL entries and | ||||
| 	// populating the queue | ||||
| 	initCtx := context.Background() | ||||
| 	ictx, cancel := context.WithCancel(initCtx) | ||||
| 	b.cancelQueue = cancel | ||||
| 	// Load queue and kickoff new periodic ticker | ||||
| 	go b.initQueue(ictx, conf) | ||||
| 	return b, nil | ||||
| } | ||||
|  | ||||
| @@ -55,31 +70,39 @@ func Backend(conf *logical.BackendConfig) *databaseBackend { | ||||
| 		Help: strings.TrimSpace(backendHelp), | ||||
|  | ||||
| 		PathsSpecial: &logical.Paths{ | ||||
| 			LocalStorage: []string{ | ||||
| 				framework.WALPrefix, | ||||
| 			}, | ||||
| 			SealWrapStorage: []string{ | ||||
| 				"config/*", | ||||
| 				"static-role/*", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Paths: []*framework.Path{ | ||||
| 		Paths: framework.PathAppend( | ||||
| 			[]*framework.Path{ | ||||
| 				pathListPluginConnection(&b), | ||||
| 				pathConfigurePluginConnection(&b), | ||||
| 				pathResetConnection(&b), | ||||
| 			}, | ||||
| 			pathListRoles(&b), | ||||
| 			pathRoles(&b), | ||||
| 			pathCredsCreate(&b), | ||||
| 			pathResetConnection(&b), | ||||
| 			pathRotateCredentials(&b), | ||||
| 		}, | ||||
| 		), | ||||
|  | ||||
| 		Secrets: []*framework.Secret{ | ||||
| 			secretCreds(&b), | ||||
| 		}, | ||||
| 		Clean:       b.closeAllDBs, | ||||
| 		Clean:       b.clean, | ||||
| 		Invalidate:  b.invalidate, | ||||
| 		BackendType: logical.TypeLogical, | ||||
| 	} | ||||
|  | ||||
| 	b.logger = conf.Logger | ||||
| 	b.connections = make(map[string]*dbPluginInstance) | ||||
|  | ||||
| 	b.roleLocks = locksutil.CreateLocks() | ||||
|  | ||||
| 	return &b | ||||
| } | ||||
|  | ||||
| @@ -89,6 +112,20 @@ type databaseBackend struct { | ||||
|  | ||||
| 	*framework.Backend | ||||
| 	sync.RWMutex | ||||
| 	// CredRotationQueue is an in-memory priority queue used to track Static Roles | ||||
| 	// that require periodic rotation. Backends will have a PriorityQueue | ||||
| 	// initialized on setup, but only backends that are mounted by a primary | ||||
| 	// server or mounted as a local mount will perform the rotations. | ||||
| 	// | ||||
| 	// cancelQueue is used to remove the priority queue and terminate the | ||||
| 	// background ticker. | ||||
| 	credRotationQueue *queue.PriorityQueue | ||||
| 	cancelQueue       context.CancelFunc | ||||
|  | ||||
| 	// roleLocks is used to lock modifications to roles in the queue, to ensure | ||||
| 	// concurrent requests are not modifying the same role and possibly causing | ||||
| 	// issues with the priority queue. | ||||
| 	roleLocks []*locksutil.LockEntry | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) DatabaseConfig(ctx context.Context, s logical.Storage, name string) (*DatabaseConfig, error) { | ||||
| @@ -124,7 +161,15 @@ type upgradeCheck struct { | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) Role(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) { | ||||
| 	entry, err := s.Get(ctx, "role/"+roleName) | ||||
| 	return b.roleAtPath(ctx, s, roleName, databaseRolePath) | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) StaticRole(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) { | ||||
| 	return b.roleAtPath(ctx, s, roleName, databaseStaticRolePath) | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) roleAtPath(ctx context.Context, s logical.Storage, roleName string, pathPrefix string) (*roleEntry, error) { | ||||
| 	entry, err := s.Get(ctx, pathPrefix+roleName) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @@ -228,6 +273,17 @@ func (b *databaseBackend) GetConnection(ctx context.Context, s logical.Storage, | ||||
| 	return db, nil | ||||
| } | ||||
|  | ||||
| // invalidateQueue cancels any background queue loading and destroys the queue. | ||||
| func (b *databaseBackend) invalidateQueue() { | ||||
| 	b.Lock() | ||||
| 	defer b.Unlock() | ||||
|  | ||||
| 	if b.cancelQueue != nil { | ||||
| 		b.cancelQueue() | ||||
| 	} | ||||
| 	b.credRotationQueue = nil | ||||
| } | ||||
|  | ||||
| // ClearConnection closes the database connection and | ||||
| // removes it from the b.connections map. | ||||
| func (b *databaseBackend) ClearConnection(name string) error { | ||||
| @@ -267,8 +323,13 @@ func (b *databaseBackend) CloseIfShutdown(db *dbPluginInstance, err error) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // closeAllDBs closes all connections from all database types | ||||
| func (b *databaseBackend) closeAllDBs(ctx context.Context) { | ||||
| // clean closes all connections from all database types | ||||
| // and cancels any rotation queue loading operation. | ||||
| func (b *databaseBackend) clean(ctx context.Context) { | ||||
| 	// invalidateQueue acquires it's own lock on the backend, removes queue, and | ||||
| 	// terminates the background ticker | ||||
| 	b.invalidateQueue() | ||||
|  | ||||
| 	b.Lock() | ||||
| 	defer b.Unlock() | ||||
|  | ||||
|   | ||||
| @@ -56,7 +56,7 @@ func preparePostgresTestContainer(t *testing.T, s logical.Storage, b logical.Bac | ||||
|  | ||||
| 	retURL = fmt.Sprintf("postgres://postgres:secret@localhost:%s/database?sslmode=disable", resource.GetPort("5432/tcp")) | ||||
|  | ||||
| 	// exponential backoff-retry | ||||
| 	// Exponential backoff-retry | ||||
| 	if err = pool.Retry(func() error { | ||||
| 		// This will cause a validation to run | ||||
| 		resp, err := b.HandleRequest(namespace.RootContext(nil), &logical.Request{ | ||||
| @@ -101,12 +101,12 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { | ||||
| 	os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) | ||||
|  | ||||
| 	sys := vault.TestDynamicSystemView(cores[0].Core) | ||||
| 	vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain", []string{}, "") | ||||
| 	vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Postgres", []string{}, "") | ||||
|  | ||||
| 	return cluster, sys | ||||
| } | ||||
|  | ||||
| func TestBackend_PluginMain(t *testing.T) { | ||||
| func TestBackend_PluginMain_Postgres(t *testing.T) { | ||||
| 	if os.Getenv(pluginutil.PluginUnwrapTokenEnv) == "" { | ||||
| 		return | ||||
| 	} | ||||
| @@ -850,17 +850,6 @@ func TestBackend_roleCrud(t *testing.T) { | ||||
| 			t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 		} | ||||
|  | ||||
| 		exists, err := b.pathRoleExistenceCheck()(context.Background(), req, &framework.FieldData{ | ||||
| 			Raw:    data, | ||||
| 			Schema: pathRoles(b).Fields, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if exists { | ||||
| 			t.Fatal("expected not exists") | ||||
| 		} | ||||
|  | ||||
| 		// Read the role | ||||
| 		data = map[string]interface{}{} | ||||
| 		req = &logical.Request{ | ||||
| @@ -920,17 +909,6 @@ func TestBackend_roleCrud(t *testing.T) { | ||||
| 			t.Fatalf("err:%v resp:%#v\n", err, resp) | ||||
| 		} | ||||
|  | ||||
| 		exists, err := b.pathRoleExistenceCheck()(context.Background(), req, &framework.FieldData{ | ||||
| 			Raw:    data, | ||||
| 			Schema: pathRoles(b).Fields, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if !exists { | ||||
| 			t.Fatal("expected exists") | ||||
| 		} | ||||
|  | ||||
| 		// Read the role | ||||
| 		data = map[string]interface{}{} | ||||
| 		req = &logical.Request{ | ||||
| @@ -994,17 +972,6 @@ func TestBackend_roleCrud(t *testing.T) { | ||||
| 			t.Fatalf("err:%v resp:%#v\n", err, resp) | ||||
| 		} | ||||
|  | ||||
| 		exists, err := b.pathRoleExistenceCheck()(context.Background(), req, &framework.FieldData{ | ||||
| 			Raw:    data, | ||||
| 			Schema: pathRoles(b).Fields, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if !exists { | ||||
| 			t.Fatal("expected exists") | ||||
| 		} | ||||
|  | ||||
| 		// Read the role | ||||
| 		data = map[string]interface{}{} | ||||
| 		req = &logical.Request{ | ||||
|   | ||||
| @@ -22,6 +22,8 @@ type mockPlugin struct { | ||||
| 	users map[string][]string | ||||
| } | ||||
|  | ||||
| var _ dbplugin.Database = &mockPlugin{} | ||||
|  | ||||
| func (m *mockPlugin) Type() (string, error) { return "mock", nil } | ||||
| func (m *mockPlugin) CreateUser(_ context.Context, statements dbplugin.Statements, usernameConf dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { | ||||
| 	err = errors.New("err") | ||||
| @@ -86,6 +88,14 @@ func (m *mockPlugin) Close() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *mockPlugin) GenerateCredentials(ctx context.Context) (password string, err error) { | ||||
| 	return password, err | ||||
| } | ||||
|  | ||||
| func (m *mockPlugin) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticConfig dbplugin.StaticUserConfig) (username string, password string, err error) { | ||||
| 	return username, password, err | ||||
| } | ||||
|  | ||||
| func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { | ||||
| 	cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ | ||||
| 		HandlerFunc: vaulthttp.Handler, | ||||
|   | ||||
| @@ -11,8 +11,9 @@ import ( | ||||
| 	"github.com/hashicorp/vault/sdk/logical" | ||||
| ) | ||||
|  | ||||
| func pathCredsCreate(b *databaseBackend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| func pathCredsCreate(b *databaseBackend) []*framework.Path { | ||||
| 	return []*framework.Path{ | ||||
| 		&framework.Path{ | ||||
| 			Pattern: "creds/" + framework.GenericNameRegex("name"), | ||||
| 			Fields: map[string]*framework.FieldSchema{ | ||||
| 				"name": &framework.FieldSchema{ | ||||
| @@ -27,6 +28,23 @@ func pathCredsCreate(b *databaseBackend) *framework.Path { | ||||
|  | ||||
| 			HelpSynopsis:    pathCredsCreateReadHelpSyn, | ||||
| 			HelpDescription: pathCredsCreateReadHelpDesc, | ||||
| 		}, | ||||
| 		&framework.Path{ | ||||
| 			Pattern: "static-creds/" + framework.GenericNameRegex("name"), | ||||
| 			Fields: map[string]*framework.FieldSchema{ | ||||
| 				"name": &framework.FieldSchema{ | ||||
| 					Type:        framework.TypeString, | ||||
| 					Description: "Name of the static role.", | ||||
| 				}, | ||||
| 			}, | ||||
|  | ||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 				logical.ReadOperation: b.pathStaticCredsRead(), | ||||
| 			}, | ||||
|  | ||||
| 			HelpSynopsis:    pathStaticCredsReadHelpSyn, | ||||
| 			HelpDescription: pathStaticCredsReadHelpDesc, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -99,6 +117,41 @@ func (b *databaseBackend) pathCredsCreateRead() framework.OperationFunc { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) pathStaticCredsRead() framework.OperationFunc { | ||||
| 	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 		name := data.Get("name").(string) | ||||
|  | ||||
| 		role, err := b.StaticRole(ctx, req.Storage, name) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if role == nil { | ||||
| 			return logical.ErrorResponse("unknown role: %s", name), nil | ||||
| 		} | ||||
|  | ||||
| 		dbConfig, err := b.DatabaseConfig(ctx, req.Storage, role.DBName) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		// If role name isn't in the database's allowed roles, send back a | ||||
| 		// permission denied. | ||||
| 		if !strutil.StrListContains(dbConfig.AllowedRoles, "*") && !strutil.StrListContainsGlob(dbConfig.AllowedRoles, name) { | ||||
| 			return nil, fmt.Errorf("%q is not an allowed role", name) | ||||
| 		} | ||||
|  | ||||
| 		return &logical.Response{ | ||||
| 			Data: map[string]interface{}{ | ||||
| 				"username":            role.StaticAccount.Username, | ||||
| 				"password":            role.StaticAccount.Password, | ||||
| 				"ttl":                 role.StaticAccount.PasswordTTL().Seconds(), | ||||
| 				"rotation_period":     role.StaticAccount.RotationPeriod.Seconds(), | ||||
| 				"last_vault_rotation": role.StaticAccount.LastVaultRotation, | ||||
| 			}, | ||||
| 		}, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const pathCredsCreateReadHelpSyn = ` | ||||
| Request database credentials for a certain role. | ||||
| ` | ||||
| @@ -108,3 +161,14 @@ This path reads database credentials for a certain role. The | ||||
| database credentials will be generated on demand and will be automatically | ||||
| revoked when the lease is up. | ||||
| ` | ||||
|  | ||||
| const pathStaticCredsReadHelpSyn = ` | ||||
| Request database credentials for a certain static role. These credentials are | ||||
| rotated periodically. | ||||
| ` | ||||
|  | ||||
| const pathStaticCredsReadHelpDesc = ` | ||||
| This path reads database credentials for a certain static role. The database | ||||
| credentials are rotated periodically according to their configuration, and will | ||||
| return the same password until they are rotated. | ||||
| ` | ||||
|   | ||||
| @@ -2,36 +2,88 @@ 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{ | ||||
| func pathListRoles(b *databaseBackend) []*framework.Path { | ||||
| 	return []*framework.Path{ | ||||
| 		&framework.Path{ | ||||
| 			Pattern: "roles/?$", | ||||
|  | ||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.ListOperation: b.pathRoleList(), | ||||
| 				logical.ListOperation: b.pathRoleList, | ||||
| 			}, | ||||
|  | ||||
| 			HelpSynopsis:    pathRoleHelpSyn, | ||||
| 			HelpDescription: pathRoleHelpDesc, | ||||
| 		}, | ||||
| 		&framework.Path{ | ||||
| 			Pattern: "static-roles/?$", | ||||
|  | ||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 				logical.ListOperation: b.pathRoleList, | ||||
| 			}, | ||||
|  | ||||
| 			HelpSynopsis:    pathStaticRoleHelpSyn, | ||||
| 			HelpDescription: pathStaticRoleHelpDesc, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func pathRoles(b *databaseBackend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| func pathRoles(b *databaseBackend) []*framework.Path { | ||||
| 	return []*framework.Path{ | ||||
| 		&framework.Path{ | ||||
| 			Pattern:        "roles/" + framework.GenericNameRegex("name"), | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			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, | ||||
| 			}, | ||||
|  | ||||
| 			HelpSynopsis:    pathRoleHelpSyn, | ||||
| 			HelpDescription: pathRoleHelpDesc, | ||||
| 		}, | ||||
|  | ||||
| 		&framework.Path{ | ||||
| 			Pattern:        "static-roles/" + framework.GenericNameRegex("name"), | ||||
| 			Fields:         fieldsForType(databaseStaticRolePath), | ||||
| 			ExistenceCheck: b.pathStaticRoleExistenceCheck, | ||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 				logical.ReadOperation:   b.pathStaticRoleRead, | ||||
| 				logical.CreateOperation: b.pathStaticRoleCreateUpdate, | ||||
| 				logical.UpdateOperation: b.pathStaticRoleCreateUpdate, | ||||
| 				logical.DeleteOperation: b.pathStaticRoleDelete, | ||||
| 			}, | ||||
|  | ||||
| 			HelpSynopsis:    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.", | ||||
| @@ -58,12 +110,33 @@ func pathRoles(b *databaseBackend) *framework.Path { | ||||
| 		"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.`, | ||||
| 	rollback a create operation in the event of an error. Not every plugin | ||||
| 	type will support this functionality. See the plugin's API page for | ||||
| 	more information on support and formatting for this parameter.`, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	// Get the fields that are specific to the type of role, and add them to the | ||||
| 	// common fields | ||||
| 	var typeFields map[string]*framework.FieldSchema | ||||
| 	switch roleType { | ||||
| 	case databaseStaticRolePath: | ||||
| 		typeFields = staticFields() | ||||
| 	default: | ||||
| 		typeFields = dynamicFields() | ||||
| 	} | ||||
|  | ||||
| 	for k, v := range typeFields { | ||||
| 		fields[k] = v | ||||
| 	} | ||||
|  | ||||
| 	return fields | ||||
| } | ||||
|  | ||||
| // 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.", | ||||
| @@ -73,45 +146,138 @@ func pathRoles(b *databaseBackend) *framework.Path { | ||||
| 			Type:        framework.TypeDurationSecond, | ||||
| 			Description: "Maximum time a credential is valid for", | ||||
| 		}, | ||||
| 		}, | ||||
|  | ||||
| 		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(), | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathRoleHelpSyn, | ||||
| 		HelpDescription: pathRoleHelpDesc, | ||||
| 	} | ||||
| 	return fields | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) pathRoleExistenceCheck() framework.ExistenceFunc { | ||||
| 	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { | ||||
| // staticFields returns a map of key and field schema items that are specific | ||||
| // only to static roles | ||||
| func staticFields() map[string]*framework.FieldSchema { | ||||
| 	fields := map[string]*framework.FieldSchema{ | ||||
| 		"username": { | ||||
| 			Type: framework.TypeString, | ||||
| 			Description: `Name of the static user account for Vault to manage. | ||||
| 	Requires "rotation_period" to be specified`, | ||||
| 		}, | ||||
| 		"rotation_period": { | ||||
| 			Type: framework.TypeDurationSecond, | ||||
| 			Description: `Period for automatic | ||||
| 	credential rotation of the given username. Not valid unless used with | ||||
| 	"username".`, | ||||
| 		}, | ||||
| 		"rotation_statements": { | ||||
| 			Type: framework.TypeStringSlice, | ||||
| 			Description: `Specifies the database statements to be executed to | ||||
| 	rotate the accounts credentials. Not every plugin type will support | ||||
| 	this functionality. See the plugin's API page for more information on | ||||
| 	support and formatting for this parameter.`, | ||||
| 		}, | ||||
| 		"revoke_user_on_delete": { | ||||
| 			Type:    framework.TypeBool, | ||||
| 			Default: false, | ||||
| 			Description: `Revoke the database user identified by the username when | ||||
| 	this Role is deleted. Revocation will use the configured | ||||
| 	revocation_statements if provided. Default false.`, | ||||
| 		}, | ||||
| 	} | ||||
| 	return fields | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) pathRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { | ||||
| 	role, err := b.Role(ctx, req.Storage, data.Get("name").(string)) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	return role != nil, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) 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)) | ||||
| func (b *databaseBackend) pathStaticRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { | ||||
| 	role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string)) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	return role != nil, nil | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	err := req.Storage.Delete(ctx, databaseRolePath+data.Get("name").(string)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) pathRoleRead() framework.OperationFunc { | ||||
| 	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { | ||||
| 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 | ||||
| 		} | ||||
|  | ||||
| 		db.RLock() | ||||
| 		defer db.RUnlock() | ||||
|  | ||||
| 		if err := db.RevokeUser(ctx, role.Statements, role.StaticAccount.Username); err != nil { | ||||
| 			b.CloseIfShutdown(db, err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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 | ||||
| 		} | ||||
| 		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 | ||||
| @@ -120,12 +286,19 @@ func (b *databaseBackend) pathRoleRead() framework.OperationFunc { | ||||
| 		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(), | ||||
| 	} | ||||
| @@ -141,32 +314,40 @@ func (b *databaseBackend) pathRoleRead() framework.OperationFunc { | ||||
| 	if len(role.Statements.Renewal) == 0 { | ||||
| 		data["renew_statements"] = []string{} | ||||
| 	} | ||||
|  | ||||
| 		return &logical.Response{ | ||||
| 			Data: data, | ||||
| 		}, nil | ||||
| 	if len(role.Statements.Rotation) == 0 { | ||||
| 		data["rotation_statements"] = []string{} | ||||
| 	} | ||||
| 	return data | ||||
| } | ||||
|  | ||||
| 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/") | ||||
| 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() framework.OperationFunc { | ||||
| 	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 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 | ||||
| 	} | ||||
|  | ||||
| 		role, err := b.Role(ctx, req.Storage, data.Get("name").(string)) | ||||
| 	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 | ||||
| 	} | ||||
| @@ -174,16 +355,8 @@ func (b *databaseBackend) pathRoleCreateUpdate() framework.OperationFunc { | ||||
| 		role = &roleEntry{} | ||||
| 	} | ||||
|  | ||||
| 		// DB Attributes | ||||
| 		{ | ||||
| 			if dbNameRaw, ok := data.GetOk("db_name"); ok { | ||||
| 				role.DBName = dbNameRaw.(string) | ||||
| 			} else if req.Operation == logical.CreateOperation { | ||||
| 				role.DBName = data.Get("db_name").(string) | ||||
| 			} | ||||
| 			if role.DBName == "" { | ||||
| 				return logical.ErrorResponse("empty database name attribute"), nil | ||||
| 			} | ||||
| 	if err := pathRoleCreateUpdateCommon(ctx, role, req.Operation, data); err != nil { | ||||
| 		return logical.ErrorResponse(err.Error()), nil | ||||
| 	} | ||||
|  | ||||
| 	// TTLs | ||||
| @@ -200,29 +373,173 @@ func (b *databaseBackend) pathRoleCreateUpdate() framework.OperationFunc { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Store it | ||||
| 	entry, err := logical.StorageEntryJSON(databaseRolePath+name, role) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := req.Storage.Put(ctx, entry); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	name := data.Get("name").(string) | ||||
| 	if name == "" { | ||||
| 		return logical.ErrorResponse("empty role name attribute given"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Grab the exclusive lock as well potentially pop and re-push the queue item | ||||
| 	// for this role | ||||
| 	lock := locksutil.LockForKey(b.roleLocks, name) | ||||
| 	lock.Lock() | ||||
| 	defer lock.Unlock() | ||||
|  | ||||
| 	exists, err := b.pathRoleExistenceCheck(ctx, req, data) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if exists { | ||||
| 		return logical.ErrorResponse("Role and Static Role names must be unique"), nil | ||||
| 	} | ||||
|  | ||||
| 	role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// createRole is a boolean to indicate if this is a new role creation. This is | ||||
| 	// can be used later by database plugins that distinguish between creating and | ||||
| 	// updating roles, and may use seperate statements depending on the context. | ||||
| 	createRole := req.Operation == logical.CreateOperation | ||||
| 	if role == nil { | ||||
| 		role = &roleEntry{ | ||||
| 			StaticAccount: &staticAccount{}, | ||||
| 		} | ||||
| 		createRole = true | ||||
| 	} | ||||
|  | ||||
| 	if err := pathRoleCreateUpdateCommon(ctx, role, req.Operation, data); err != nil { | ||||
| 		return logical.ErrorResponse(err.Error()), nil | ||||
| 	} | ||||
|  | ||||
| 	username := data.Get("username").(string) | ||||
| 	if username == "" && createRole { | ||||
| 		return logical.ErrorResponse("username is a required field to create a static account"), nil | ||||
| 	} | ||||
|  | ||||
| 	if role.StaticAccount.Username != "" && role.StaticAccount.Username != username { | ||||
| 		return logical.ErrorResponse("cannot update static account username"), nil | ||||
| 	} | ||||
| 	role.StaticAccount.Username = username | ||||
|  | ||||
| 	// If it's a Create operation, both username and rotation_period must be included | ||||
| 	rotationPeriodSecondsRaw, ok := data.GetOk("rotation_period") | ||||
| 	if !ok && createRole { | ||||
| 		return logical.ErrorResponse("rotation_period is required to create static accounts"), nil | ||||
| 	} | ||||
| 	if ok { | ||||
| 		rotationPeriodSeconds := rotationPeriodSecondsRaw.(int) | ||||
| 		if rotationPeriodSeconds < queueTickSeconds { | ||||
| 			// If rotation frequency is specified, and this is an update, the value | ||||
| 			// must be at least that of the constant queueTickSeconds (5 seconds at | ||||
| 			// time of writing), otherwise we wont be able to rotate in time | ||||
| 			return logical.ErrorResponse(fmt.Sprintf("rotation_period must be %d seconds or more", queueTickSeconds)), nil | ||||
| 		} | ||||
| 		role.StaticAccount.RotationPeriod = time.Duration(rotationPeriodSeconds) * time.Second | ||||
| 	} | ||||
|  | ||||
| 	if rotationStmtsRaw, ok := data.GetOk("rotation_statements"); ok { | ||||
| 		role.Statements.Rotation = rotationStmtsRaw.([]string) | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		role.Statements.Rotation = data.Get("rotation_statements").([]string) | ||||
| 	} | ||||
|  | ||||
| 	role.StaticAccount.RevokeUserOnDelete = data.Get("revoke_user_on_delete").(bool) | ||||
|  | ||||
| 	// lvr represents the roles' LastVaultRotation | ||||
| 	lvr := role.StaticAccount.LastVaultRotation | ||||
|  | ||||
| 	// Only call setStaticAccount if we're creating the role for the | ||||
| 	// first time | ||||
| 	switch req.Operation { | ||||
| 	case logical.CreateOperation: | ||||
| 		// setStaticAccount calls Storage.Put and saves the role to storage | ||||
| 		resp, err := b.setStaticAccount(ctx, req.Storage, &setStaticAccountInput{ | ||||
| 			RoleName:   name, | ||||
| 			Role:       role, | ||||
| 			CreateUser: createRole, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// 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 | ||||
| 		} | ||||
| 		if err := req.Storage.Put(ctx, entry); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		// 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 { | ||||
| 		} else if operation == logical.CreateOperation { | ||||
| 			role.Statements.Creation = data.Get("creation_statements").([]string) | ||||
| 		} | ||||
|  | ||||
| 		if revocationStmtsRaw, ok := data.GetOk("revocation_statements"); ok { | ||||
| 			role.Statements.Revocation = revocationStmtsRaw.([]string) | ||||
| 			} else if req.Operation == logical.CreateOperation { | ||||
| 		} else if operation == logical.CreateOperation { | ||||
| 			role.Statements.Revocation = data.Get("revocation_statements").([]string) | ||||
| 		} | ||||
|  | ||||
| 		if rollbackStmtsRaw, ok := data.GetOk("rollback_statements"); ok { | ||||
| 			role.Statements.Rollback = rollbackStmtsRaw.([]string) | ||||
| 			} else if req.Operation == logical.CreateOperation { | ||||
| 		} else if operation == logical.CreateOperation { | ||||
| 			role.Statements.Rollback = data.Get("rollback_statements").([]string) | ||||
| 		} | ||||
|  | ||||
| 		if renewStmtsRaw, ok := data.GetOk("renew_statements"); ok { | ||||
| 			role.Statements.Renewal = renewStmtsRaw.([]string) | ||||
| 			} else if req.Operation == logical.CreateOperation { | ||||
| 		} else if operation == logical.CreateOperation { | ||||
| 			role.Statements.Renewal = data.Get("renew_statements").([]string) | ||||
| 		} | ||||
|  | ||||
| @@ -235,17 +552,7 @@ func (b *databaseBackend) pathRoleCreateUpdate() framework.OperationFunc { | ||||
|  | ||||
| 	role.Statements.Revocation = strutil.RemoveEmpty(role.Statements.Revocation) | ||||
|  | ||||
| 		// Store it | ||||
| 		entry, err := logical.StorageEntryJSON("role/"+name, role) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if err := req.Storage.Put(ctx, entry); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type roleEntry struct { | ||||
| @@ -253,12 +560,62 @@ type roleEntry struct { | ||||
| 	Statements    dbplugin.Statements `json:"statements"` | ||||
| 	DefaultTTL    time.Duration       `json:"default_ttl"` | ||||
| 	MaxTTL        time.Duration       `json:"max_ttl"` | ||||
| 	StaticAccount *staticAccount      `json:"static_account" mapstructure:"static_account"` | ||||
| } | ||||
|  | ||||
| type staticAccount struct { | ||||
| 	// Username to create or assume management for static accounts | ||||
| 	Username string `json:"username"` | ||||
|  | ||||
| 	// Password is the current password for static accounts. As an input, this is | ||||
| 	// used/required when trying to assume management of an existing static | ||||
| 	// account. Return this on credential request if it exists. | ||||
| 	Password string `json:"password"` | ||||
|  | ||||
| 	// LastVaultRotation represents the last time Vault rotated the password | ||||
| 	LastVaultRotation time.Time `json:"last_vault_rotation"` | ||||
|  | ||||
| 	// RotationPeriod is number in seconds between each rotation, effectively a | ||||
| 	// "time to live". This value is compared to the LastVaultRotation to | ||||
| 	// determine if a password needs to be rotated | ||||
| 	RotationPeriod time.Duration `json:"rotation_period"` | ||||
|  | ||||
| 	// RevokeUser is a boolean flag to indicate if Vault should revoke the | ||||
| 	// database user when the role is deleted | ||||
| 	RevokeUserOnDelete bool `json:"revoke_user_on_delete"` | ||||
| } | ||||
|  | ||||
| // NextRotationTime calculates the next rotation by adding the Rotation Period | ||||
| // to the last known vault rotation | ||||
| func (s *staticAccount) NextRotationTime() time.Time { | ||||
| 	return s.LastVaultRotation.Add(s.RotationPeriod) | ||||
| } | ||||
|  | ||||
| // PasswordTTL calculates the approximate time remaining until the password is | ||||
| // no longer valid. This is approximate because the periodic rotation is only | ||||
| // checked approximately every 5 seconds, and each rotation can take a small | ||||
| // amount of time to process. This can result in a negative TTL time while the | ||||
| // rotation function processes the Static Role and performs the rotation. If the | ||||
| // TTL is negative, zero is returned. Users should not trust passwords with a | ||||
| // Zero TTL, as they are likely in the process of being rotated and will quickly | ||||
| // be invalidated. | ||||
| func (s *staticAccount) PasswordTTL() time.Duration { | ||||
| 	next := s.NextRotationTime() | ||||
| 	ttl := next.Sub(time.Now()).Round(time.Second) | ||||
| 	if ttl < 0 { | ||||
| 		ttl = time.Duration(0) | ||||
| 	} | ||||
| 	return ttl | ||||
| } | ||||
|  | ||||
| const pathRoleHelpSyn = ` | ||||
| Manage the roles that can be created with this backend. | ||||
| ` | ||||
|  | ||||
| const pathStaticRoleHelpSyn = ` | ||||
| Manage the static roles that can be created with this backend. | ||||
| ` | ||||
|  | ||||
| const pathRoleHelpDesc = ` | ||||
| This path lets you manage the roles that can be created with this backend. | ||||
|  | ||||
| @@ -299,3 +656,43 @@ user. | ||||
| The "rollback_statements' parameter customizes the statement string used to | ||||
| rollback a change if needed. | ||||
| ` | ||||
|  | ||||
| const pathStaticRoleHelpDesc = ` | ||||
| This path lets you manage the static roles that can be created with this | ||||
| backend. Static Roles are associated with a single database user, and manage the | ||||
| password based on a rotation period, automatically rotating the password. | ||||
|  | ||||
| The "db_name" parameter is required and configures the name of the database | ||||
| connection to use. | ||||
|  | ||||
| The "creation_statements" parameter customizes the string used to create the | ||||
| credentials. This can be a sequence of SQL queries, or other statement formats | ||||
| for a particular database type. Some substitution will be done to the statement | ||||
| strings for certain keys. The names of the variables must be surrounded by "{{" | ||||
| and "}}" to be replaced. | ||||
|  | ||||
|   * "name" - The random username generated for the DB user. | ||||
|  | ||||
|   * "password" - The random password generated for the DB user. | ||||
|  | ||||
| Example of a decent creation_statements for a postgresql database plugin: | ||||
|  | ||||
|         CREATE ROLE "{{name}}" WITH | ||||
|           LOGIN | ||||
|           PASSWORD '{{password}}' | ||||
|         GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; | ||||
|  | ||||
| The "revocation_statements" parameter customizes the statement string used to | ||||
| revoke a user. Example of a decent revocation_statements for a postgresql | ||||
| database plugin: | ||||
|  | ||||
|         REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {{name}}; | ||||
|         REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {{name}}; | ||||
|         REVOKE USAGE ON SCHEMA public FROM {{name}}; | ||||
|         DROP ROLE IF EXISTS {{name}}; | ||||
|  | ||||
| The "renew_statements" parameter customizes the statement string used to renew a | ||||
| user. | ||||
| The "rollback_statements' parameter customizes the statement string used to | ||||
| rollback a change if needed. | ||||
| ` | ||||
|   | ||||
							
								
								
									
										526
									
								
								builtin/logical/database/path_roles_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										526
									
								
								builtin/logical/database/path_roles_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,526 @@ | ||||
| package database | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/go-test/deep" | ||||
| 	"github.com/hashicorp/vault/helper/namespace" | ||||
| 	"github.com/hashicorp/vault/sdk/logical" | ||||
| ) | ||||
|  | ||||
| var dataKeys = []string{"username", "password", "last_vault_rotation", "rotation_period"} | ||||
|  | ||||
| func TestBackend_StaticRole_Config(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	lb, err := Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	b, ok := lb.(*databaseBackend) | ||||
| 	if !ok { | ||||
| 		t.Fatal("could not convert to db backend") | ||||
| 	} | ||||
| 	defer b.Cleanup(context.Background()) | ||||
|  | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	// Configure a connection | ||||
| 	data := map[string]interface{}{ | ||||
| 		"connection_url":    connURL, | ||||
| 		"plugin_name":       "postgresql-database-plugin", | ||||
| 		"verify_connection": false, | ||||
| 		"allowed_roles":     []string{"*"}, | ||||
| 		"name":              "plugin-test", | ||||
| 	} | ||||
| 	req := &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "config/plugin-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// Test static role creation scenarios. Uses a map, so there is no guaranteed | ||||
| 	// ordering, so each case cleans up by deleting the role | ||||
| 	testCases := map[string]struct { | ||||
| 		account  map[string]interface{} | ||||
| 		expected map[string]interface{} | ||||
| 		err      error | ||||
| 	}{ | ||||
| 		"basic": { | ||||
| 			account: map[string]interface{}{ | ||||
| 				"username":        "statictest", | ||||
| 				"rotation_period": "5400s", | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"username":        "statictest", | ||||
| 				"rotation_period": float64(5400), | ||||
| 			}, | ||||
| 		}, | ||||
| 		"missing rotation period": { | ||||
| 			account: map[string]interface{}{ | ||||
| 				"username": "statictest", | ||||
| 			}, | ||||
| 			err: errors.New("rotation_period is required to create static accounts"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, tc := range testCases { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			data := map[string]interface{}{ | ||||
| 				"name":                  "plugin-role-test", | ||||
| 				"db_name":               "plugin-test", | ||||
| 				"creation_statements":   testRoleStaticCreate, | ||||
| 				"rotation_statements":   testRoleStaticUpdate, | ||||
| 				"revocation_statements": defaultRevocationSQL, | ||||
| 				"default_ttl":           "5m", | ||||
| 				"max_ttl":               "10m", | ||||
| 			} | ||||
|  | ||||
| 			for k, v := range tc.account { | ||||
| 				data[k] = v | ||||
| 			} | ||||
|  | ||||
| 			req := &logical.Request{ | ||||
| 				Operation: logical.CreateOperation, | ||||
| 				Path:      "static-roles/plugin-role-test", | ||||
| 				Storage:   config.StorageView, | ||||
| 				Data:      data, | ||||
| 			} | ||||
|  | ||||
| 			resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 			if err != nil || (resp != nil && resp.IsError()) { | ||||
| 				if tc.err == nil { | ||||
| 					t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 				} | ||||
| 				if err != nil && tc.err.Error() == err.Error() { | ||||
| 					// errors match | ||||
| 					return | ||||
| 				} | ||||
| 				if err == nil && tc.err.Error() == resp.Error().Error() { | ||||
| 					// errors match | ||||
| 					return | ||||
| 				} | ||||
| 				t.Fatalf("expected err message: (%s), got (%s), response error: (%s)", tc.err, err, resp.Error()) | ||||
| 			} | ||||
|  | ||||
| 			if tc.err != nil { | ||||
| 				if err == nil || (resp == nil || !resp.IsError()) { | ||||
| 					t.Fatal("expected error, got none") | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Read the role | ||||
| 			data = map[string]interface{}{} | ||||
| 			req = &logical.Request{ | ||||
| 				Operation: logical.ReadOperation, | ||||
| 				Path:      "static-roles/plugin-role-test", | ||||
| 				Storage:   config.StorageView, | ||||
| 				Data:      data, | ||||
| 			} | ||||
| 			resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 			if err != nil || (resp != nil && resp.IsError()) { | ||||
| 				t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 			} | ||||
|  | ||||
| 			expected := tc.expected | ||||
| 			actual := make(map[string]interface{}) | ||||
| 			for _, key := range dataKeys { | ||||
| 				if v, ok := resp.Data[key]; ok { | ||||
| 					actual[key] = v | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if len(tc.expected) > 0 { | ||||
| 				// verify a password is returned, but we don't care what it's value is | ||||
| 				if actual["password"] == "" { | ||||
| 					t.Fatalf("expected result to contain password, but none found") | ||||
| 				} | ||||
| 				if v, ok := actual["last_vault_rotation"].(time.Time); !ok { | ||||
| 					t.Fatalf("expected last_vault_rotation to be set to time.Time type, got: %#v", v) | ||||
| 				} | ||||
|  | ||||
| 				// delete these values before the comparison, since we can't know them in | ||||
| 				// advance | ||||
| 				delete(actual, "password") | ||||
| 				delete(actual, "last_vault_rotation") | ||||
| 				if diff := deep.Equal(expected, actual); diff != nil { | ||||
| 					t.Fatal(diff) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if len(tc.expected) == 0 && resp.Data["static_account"] != nil { | ||||
| 				t.Fatalf("got unexpected static_account info: %#v", actual) | ||||
| 			} | ||||
|  | ||||
| 			if diff := deep.Equal(resp.Data["db_name"], "plugin-test"); diff != nil { | ||||
| 				t.Fatal(diff) | ||||
| 			} | ||||
|  | ||||
| 			// Delete role for next run | ||||
| 			req = &logical.Request{ | ||||
| 				Operation: logical.DeleteOperation, | ||||
| 				Path:      "static-roles/plugin-role-test", | ||||
| 				Storage:   config.StorageView, | ||||
| 			} | ||||
| 			resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 			if err != nil || (resp != nil && resp.IsError()) { | ||||
| 				t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestBackend_StaticRole_Updates(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	lb, err := Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	b, ok := lb.(*databaseBackend) | ||||
| 	if !ok { | ||||
| 		t.Fatal("could not convert to db backend") | ||||
| 	} | ||||
| 	defer b.Cleanup(context.Background()) | ||||
|  | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	// Configure a connection | ||||
| 	data := map[string]interface{}{ | ||||
| 		"connection_url":    connURL, | ||||
| 		"plugin_name":       "postgresql-database-plugin", | ||||
| 		"verify_connection": false, | ||||
| 		"allowed_roles":     []string{"*"}, | ||||
| 		"name":              "plugin-test", | ||||
| 	} | ||||
|  | ||||
| 	req := &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "config/plugin-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	data = map[string]interface{}{ | ||||
| 		"name":                  "plugin-role-test-updates", | ||||
| 		"db_name":               "plugin-test", | ||||
| 		"creation_statements":   testRoleStaticCreate, | ||||
| 		"rotation_statements":   testRoleStaticUpdate, | ||||
| 		"revocation_statements": defaultRevocationSQL, | ||||
| 		"default_ttl":           "5m", | ||||
| 		"max_ttl":               "10m", | ||||
| 		"username":              "statictest", | ||||
| 		"rotation_period":       "5400s", | ||||
| 	} | ||||
|  | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "static-roles/plugin-role-test-updates", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// Read the role | ||||
| 	data = map[string]interface{}{} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-roles/plugin-role-test-updates", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	rotation := resp.Data["rotation_period"].(float64) | ||||
|  | ||||
| 	// capture the password to verify it doesn't change | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-creds/plugin-role-test-updates", | ||||
| 		Storage:   config.StorageView, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	username := resp.Data["username"].(string) | ||||
| 	password := resp.Data["password"].(string) | ||||
| 	if username == "" || password == "" { | ||||
| 		t.Fatalf("expected both username/password, got (%s), (%s)", username, password) | ||||
| 	} | ||||
|  | ||||
| 	// update rotation_period | ||||
| 	updateData := map[string]interface{}{ | ||||
| 		"name":            "plugin-role-test-updates", | ||||
| 		"db_name":         "plugin-test", | ||||
| 		"username":        "statictest", | ||||
| 		"rotation_period": "6400s", | ||||
| 	} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "static-roles/plugin-role-test-updates", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      updateData, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// re-read the role | ||||
| 	data = map[string]interface{}{} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-roles/plugin-role-test-updates", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	newRotation := resp.Data["rotation_period"].(float64) | ||||
| 	if newRotation == rotation { | ||||
| 		t.Fatalf("expected change in rotation, but got old value:  %#v", newRotation) | ||||
| 	} | ||||
|  | ||||
| 	// re-capture the password to ensure it did not change | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-creds/plugin-role-test-updates", | ||||
| 		Storage:   config.StorageView, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	if username != resp.Data["username"].(string) { | ||||
| 		t.Fatalf("usernames dont match!: (%s) / (%s)", username, resp.Data["username"].(string)) | ||||
| 	} | ||||
| 	if password != resp.Data["password"].(string) { | ||||
| 		t.Fatalf("passwords dont match!: (%s) / (%s)", password, resp.Data["password"].(string)) | ||||
| 	} | ||||
|  | ||||
| 	// verify that rotation_period is only required when creating | ||||
| 	updateData = map[string]interface{}{ | ||||
| 		"name":                "plugin-role-test-updates", | ||||
| 		"db_name":             "plugin-test", | ||||
| 		"username":            "statictest", | ||||
| 		"rotation_statements": testRoleStaticUpdateRotation, | ||||
| 	} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "static-roles/plugin-role-test-updates", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      updateData, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// verify updating static username returns an error | ||||
| 	updateData = map[string]interface{}{ | ||||
| 		"name":     "plugin-role-test-updates", | ||||
| 		"db_name":  "plugin-test", | ||||
| 		"username": "statictestmodified", | ||||
| 	} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "static-roles/plugin-role-test-updates", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      updateData, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || !resp.IsError() { | ||||
| 		t.Fatal("expected error on updating name") | ||||
| 	} | ||||
| 	err = resp.Error() | ||||
| 	if err.Error() != "cannot update static account username" { | ||||
| 		t.Fatalf("expected error on updating name, got: %s", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestBackend_StaticRole_Role_name_check(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	lb, err := Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	b, ok := lb.(*databaseBackend) | ||||
| 	if !ok { | ||||
| 		t.Fatal("could not convert to db backend") | ||||
| 	} | ||||
| 	defer b.Cleanup(context.Background()) | ||||
|  | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	// Configure a connection | ||||
| 	data := map[string]interface{}{ | ||||
| 		"connection_url":    connURL, | ||||
| 		"plugin_name":       "postgresql-database-plugin", | ||||
| 		"verify_connection": false, | ||||
| 		"allowed_roles":     []string{"*"}, | ||||
| 		"name":              "plugin-test", | ||||
| 	} | ||||
|  | ||||
| 	req := &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "config/plugin-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// non-static role | ||||
| 	data = map[string]interface{}{ | ||||
| 		"name":                  "plugin-role-test", | ||||
| 		"db_name":               "plugin-test", | ||||
| 		"creation_statements":   testRoleStaticCreate, | ||||
| 		"rotation_statements":   testRoleStaticUpdate, | ||||
| 		"revocation_statements": defaultRevocationSQL, | ||||
| 		"default_ttl":           "5m", | ||||
| 		"max_ttl":               "10m", | ||||
| 	} | ||||
|  | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "roles/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// create a static role with the same name, and expect failure | ||||
| 	// static role | ||||
| 	data = map[string]interface{}{ | ||||
| 		"name":                  "plugin-role-test", | ||||
| 		"db_name":               "plugin-test", | ||||
| 		"creation_statements":   testRoleStaticCreate, | ||||
| 		"rotation_statements":   testRoleStaticUpdate, | ||||
| 		"revocation_statements": defaultRevocationSQL, | ||||
| 	} | ||||
|  | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "static-roles/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if resp == nil || !resp.IsError() { | ||||
| 		t.Fatalf("expected error, got none") | ||||
| 	} | ||||
|  | ||||
| 	// repeat, with a static role first | ||||
| 	data = map[string]interface{}{ | ||||
| 		"name":                  "plugin-role-test-2", | ||||
| 		"db_name":               "plugin-test", | ||||
| 		"creation_statements":   testRoleStaticCreate, | ||||
| 		"rotation_statements":   testRoleStaticUpdate, | ||||
| 		"revocation_statements": defaultRevocationSQL, | ||||
| 		"username":              "testusername", | ||||
| 		"rotation_period":       "1h", | ||||
| 	} | ||||
|  | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "static-roles/plugin-role-test-2", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// create a non-static role with the same name, and expect failure | ||||
| 	data = map[string]interface{}{ | ||||
| 		"name":                  "plugin-role-test-2", | ||||
| 		"db_name":               "plugin-test", | ||||
| 		"creation_statements":   testRoleStaticCreate, | ||||
| 		"revocation_statements": defaultRevocationSQL, | ||||
| 		"default_ttl":           "5m", | ||||
| 		"max_ttl":               "10m", | ||||
| 	} | ||||
|  | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "roles/plugin-role-test-2", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if resp == nil || !resp.IsError() { | ||||
| 		t.Fatalf("expected error, got none") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const testRoleStaticCreate = ` | ||||
| CREATE ROLE "{{name}}" WITH | ||||
|   LOGIN | ||||
|   PASSWORD '{{password}}'; | ||||
| GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; | ||||
| ` | ||||
|  | ||||
| const testRoleStaticUpdate = ` | ||||
| ALTER USER "{{name}}" WITH PASSWORD '{{password}}'; | ||||
| ` | ||||
|  | ||||
| const testRoleStaticUpdateRotation = ` | ||||
| ALTER USER "{{name}}" WITH PASSWORD '{{password}}';GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; | ||||
| ` | ||||
| @@ -3,13 +3,16 @@ 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{ | ||||
| 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{ | ||||
| @@ -24,6 +27,23 @@ func pathRotateCredentials(b *databaseBackend) *framework.Path { | ||||
|  | ||||
| 			HelpSynopsis:    pathCredsCreateReadHelpSyn, | ||||
| 			HelpDescription: pathCredsCreateReadHelpDesc, | ||||
| 		}, | ||||
| 		&framework.Path{ | ||||
| 			Pattern: "rotate-role/" + framework.GenericNameRegex("name"), | ||||
| 			Fields: map[string]*framework.FieldSchema{ | ||||
| 				"name": &framework.FieldSchema{ | ||||
| 					Type:        framework.TypeString, | ||||
| 					Description: "Name of the static role", | ||||
| 				}, | ||||
| 			}, | ||||
|  | ||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 				logical.UpdateOperation: b.pathRotateRoleCredentialsUpdate(), | ||||
| 			}, | ||||
|  | ||||
| 			HelpSynopsis:    pathCredsCreateReadHelpSyn, | ||||
| 			HelpDescription: pathCredsCreateReadHelpDesc, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -77,6 +97,56 @@ func (b *databaseBackend) pathRotateCredentialsUpdate() framework.OperationFunc | ||||
| 		return nil, nil | ||||
| 	} | ||||
| } | ||||
| func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationFunc { | ||||
| 	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 		name := data.Get("name").(string) | ||||
| 		if name == "" { | ||||
| 			return logical.ErrorResponse("empty role name attribute given"), nil | ||||
| 		} | ||||
|  | ||||
| 		role, err := b.StaticRole(ctx, req.Storage, name) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if role == nil { | ||||
| 			return logical.ErrorResponse("no static role found for role name"), nil | ||||
| 		} | ||||
|  | ||||
| 		// In create/update of static accounts, we only care if the operation | ||||
| 		// err'd , and this call does not return credentials | ||||
| 		item, err := b.popFromRotationQueueByKey(name) | ||||
| 		if err != nil { | ||||
| 			item = &queue.Item{ | ||||
| 				Key: name, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		resp, err := b.setStaticAccount(ctx, req.Storage, &setStaticAccountInput{ | ||||
| 			RoleName: name, | ||||
| 			Role:     role, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			b.logger.Warn("unable to rotate credentials in rotate-role", "error", err) | ||||
| 			// Update the priority to re-try this rotation and re-add the item to | ||||
| 			// the queue | ||||
| 			item.Priority = time.Now().Add(10 * time.Second).Unix() | ||||
|  | ||||
| 			// Preserve the WALID if it was returned | ||||
| 			if resp.WALID != "" { | ||||
| 				item.Value = resp.WALID | ||||
| 			} | ||||
| 		} else { | ||||
| 			item.Priority = resp.RotationTime.Add(role.StaticAccount.RotationPeriod).Unix() | ||||
| 		} | ||||
|  | ||||
| 		// Add their rotation to the queue | ||||
| 		if err := b.pushItem(item); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		return nil, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const pathRotateCredentialsUpdateHelpSyn = ` | ||||
| Request to rotate the root credentials for a certain database connection. | ||||
| @@ -85,3 +155,10 @@ Request to rotate the root credentials for a certain database connection. | ||||
| const pathRotateCredentialsUpdateHelpDesc = ` | ||||
| This path attempts to rotate the root credentials for the given database.  | ||||
| ` | ||||
|  | ||||
| const pathRotateRoleCredentialsUpdateHelpSyn = ` | ||||
| Request to rotate the credentials for a static user account. | ||||
| ` | ||||
| const pathRotateRoleCredentialsUpdateHelpDesc = ` | ||||
| This path attempts to rotate the credentials for the given static user account. | ||||
| ` | ||||
|   | ||||
							
								
								
									
										528
									
								
								builtin/logical/database/rotation.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										528
									
								
								builtin/logical/database/rotation.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,528 @@ | ||||
| package database | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/errwrap" | ||||
| 	"github.com/hashicorp/go-multierror" | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/framework" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/consts" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/locksutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/strutil" | ||||
| 	"github.com/hashicorp/vault/sdk/logical" | ||||
| 	"github.com/hashicorp/vault/sdk/queue" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// Interval to check the queue for items needing rotation | ||||
| 	queueTickSeconds  = 5 | ||||
| 	queueTickInterval = queueTickSeconds * time.Second | ||||
|  | ||||
| 	// WAL storage key used for static account rotations | ||||
| 	staticWALKey = "staticRotationKey" | ||||
| ) | ||||
|  | ||||
| // populateQueue loads the priority queue with existing static accounts. This | ||||
| // occurs at initialization, after any WAL entries of failed or interrupted | ||||
| // rotations have been processed. It lists the roles from storage and searches | ||||
| // for any that have an associated static account, then adds them to the | ||||
| // priority queue for rotations. | ||||
| func (b *databaseBackend) populateQueue(ctx context.Context, s logical.Storage) { | ||||
| 	log := b.Logger() | ||||
| 	log.Info("populating role rotation queue") | ||||
|  | ||||
| 	// Build map of role name / wal entries | ||||
| 	walMap, err := b.loadStaticWALs(ctx, s) | ||||
| 	if err != nil { | ||||
| 		log.Warn("unable to load rotation WALs", "error", err) | ||||
| 	} | ||||
|  | ||||
| 	roles, err := s.List(ctx, databaseStaticRolePath) | ||||
| 	if err != nil { | ||||
| 		log.Warn("unable to list role for enqueueing", "error", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, roleName := range roles { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			log.Info("rotation queue restore cancelled") | ||||
| 			return | ||||
| 		default: | ||||
| 		} | ||||
|  | ||||
| 		role, err := b.StaticRole(ctx, s, roleName) | ||||
| 		if err != nil { | ||||
| 			log.Warn("unable to read static role", "error", err, "role", roleName) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		item := queue.Item{ | ||||
| 			Key:      roleName, | ||||
| 			Priority: role.StaticAccount.LastVaultRotation.Add(role.StaticAccount.RotationPeriod).Unix(), | ||||
| 		} | ||||
|  | ||||
| 		// Check if role name is in map | ||||
| 		walEntry := walMap[roleName] | ||||
| 		if walEntry != nil { | ||||
| 			// Check walEntry last vault time | ||||
| 			if !walEntry.LastVaultRotation.IsZero() && walEntry.LastVaultRotation.Before(role.StaticAccount.LastVaultRotation) { | ||||
| 				// WAL's last vault rotation record is older than the role's data, so | ||||
| 				// delete and move on | ||||
| 				if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { | ||||
| 					log.Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID) | ||||
| 				} | ||||
| 			} else { | ||||
| 				log.Info("adjusting priority for Role") | ||||
| 				item.Value = walEntry.walID | ||||
| 				item.Priority = time.Now().Unix() | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if err := b.pushItem(&item); err != nil { | ||||
| 			log.Warn("unable to enqueue item", "error", err, "role", roleName) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // runTicker kicks off a periodic ticker that invoke the automatic credential | ||||
| // rotation method at a determined interval. The default interval is 5 seconds. | ||||
| func (b *databaseBackend) runTicker(ctx context.Context, s logical.Storage) { | ||||
| 	b.logger.Info("starting periodic ticker") | ||||
| 	tick := time.NewTicker(queueTickInterval) | ||||
| 	defer tick.Stop() | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-tick.C: | ||||
| 			b.rotateCredentials(ctx, s) | ||||
|  | ||||
| 		case <-ctx.Done(): | ||||
| 			b.logger.Info("stopping periodic ticker") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // setCredentialsWAL is used to store information in a WAL that can retry a | ||||
| // credential setting or rotation in the event of partial failure. | ||||
| type setCredentialsWAL struct { | ||||
| 	NewPassword string `json:"new_password"` | ||||
| 	OldPassword string `json:"old_password"` | ||||
| 	RoleName    string `json:"role_name"` | ||||
| 	Username    string `json:"username"` | ||||
|  | ||||
| 	LastVaultRotation time.Time `json:"last_vault_rotation"` | ||||
|  | ||||
| 	walID string | ||||
| } | ||||
|  | ||||
| // rotateCredentials sets a new password for a static account. This method is | ||||
| // invoked in the runTicker method, which is in it's own go-routine, and invoked | ||||
| // periodically (approximately every 5 seconds). | ||||
| // | ||||
| // This method loops through the priority queue, popping the highest priority | ||||
| // item until it encounters the first item that does not yet need rotation, | ||||
| // based on the current time. | ||||
| func (b *databaseBackend) rotateCredentials(ctx context.Context, s logical.Storage) error { | ||||
| 	for { | ||||
| 		// Quit rotating credentials if shutdown has started | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			return nil | ||||
| 		default: | ||||
| 		} | ||||
| 		item, err := b.popFromRotationQueue() | ||||
| 		if err != nil { | ||||
| 			if err == queue.ErrEmpty { | ||||
| 				return nil | ||||
| 			} | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// Guard against possible nil item | ||||
| 		if item == nil { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		// Grab the exclusive lock for this Role, to make sure we don't incur and | ||||
| 		// writes during the rotation process | ||||
| 		lock := locksutil.LockForKey(b.roleLocks, item.Key) | ||||
| 		lock.Lock() | ||||
| 		defer lock.Unlock() | ||||
|  | ||||
| 		// Validate the role still exists | ||||
| 		role, err := b.StaticRole(ctx, s, item.Key) | ||||
| 		if err != nil { | ||||
| 			b.logger.Error("unable to load role", "role", item.Key, "error", err) | ||||
| 			item.Priority = time.Now().Add(10 * time.Second).Unix() | ||||
| 			if err := b.pushItem(item); err != nil { | ||||
| 				b.logger.Error("unable to push item on to queue", "error", err) | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 		if role == nil { | ||||
| 			b.logger.Warn("role not found", "role", item.Key, "error", err) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// If "now" is less than the Item priority, then this item does not need to | ||||
| 		// be rotated | ||||
| 		if time.Now().Unix() < item.Priority { | ||||
| 			if err := b.pushItem(item); err != nil { | ||||
| 				b.logger.Error("unable to push item on to queue", "error", err) | ||||
| 			} | ||||
| 			// Break out of the for loop | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		input := &setStaticAccountInput{ | ||||
| 			RoleName: item.Key, | ||||
| 			Role:     role, | ||||
| 		} | ||||
|  | ||||
| 		// If there is a WAL entry related to this Role, the corresponding WAL ID | ||||
| 		// should be stored in the Item's Value field. | ||||
| 		if walID, ok := item.Value.(string); ok { | ||||
| 			walEntry, err := b.findStaticWAL(ctx, s, walID) | ||||
| 			if err != nil { | ||||
| 				b.logger.Error("error finding static WAL", "error", err) | ||||
| 				item.Priority = time.Now().Add(10 * time.Second).Unix() | ||||
| 				if err := b.pushItem(item); err != nil { | ||||
| 					b.logger.Error("unable to push item on to queue", "error", err) | ||||
| 				} | ||||
| 			} | ||||
| 			if walEntry != nil && walEntry.NewPassword != "" { | ||||
| 				input.Password = walEntry.NewPassword | ||||
| 				input.WALID = walID | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		resp, err := b.setStaticAccount(ctx, s, input) | ||||
| 		if err != nil { | ||||
| 			b.logger.Error("unable to rotate credentials in periodic function", "error", err) | ||||
| 			// Increment the priority enough so that the next call to this method | ||||
| 			// likely will not attempt to rotate it, as a back-off of sorts | ||||
| 			item.Priority = time.Now().Add(10 * time.Second).Unix() | ||||
|  | ||||
| 			// Preserve the WALID if it was returned | ||||
| 			if resp != nil && resp.WALID != "" { | ||||
| 				item.Value = resp.WALID | ||||
| 			} | ||||
|  | ||||
| 			if err := b.pushItem(item); err != nil { | ||||
| 				b.logger.Error("unable to push item on to queue", "error", err) | ||||
| 			} | ||||
| 			// Go to next item | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		lvr := resp.RotationTime | ||||
| 		if lvr.IsZero() { | ||||
| 			lvr = time.Now() | ||||
| 		} | ||||
|  | ||||
| 		// Update priority and push updated Item to the queue | ||||
| 		nextRotation := lvr.Add(role.StaticAccount.RotationPeriod) | ||||
| 		item.Priority = nextRotation.Unix() | ||||
| 		if err := b.pushItem(item); err != nil { | ||||
| 			b.logger.Warn("unable to push item on to queue", "error", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // findStaticWAL loads a WAL entry by ID. If found, only return the WAL if it | ||||
| // is of type staticWALKey, otherwise return nil | ||||
| func (b *databaseBackend) findStaticWAL(ctx context.Context, s logical.Storage, id string) (*setCredentialsWAL, error) { | ||||
| 	wal, err := framework.GetWAL(ctx, s, id) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if wal == nil || wal.Kind != staticWALKey { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	data := wal.Data.(map[string]interface{}) | ||||
| 	walEntry := setCredentialsWAL{ | ||||
| 		walID:       id, | ||||
| 		NewPassword: data["new_password"].(string), | ||||
| 		OldPassword: data["old_password"].(string), | ||||
| 		RoleName:    data["role_name"].(string), | ||||
| 		Username:    data["username"].(string), | ||||
| 	} | ||||
| 	lvr, err := time.Parse(time.RFC3339, data["last_vault_rotation"].(string)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	walEntry.LastVaultRotation = lvr | ||||
|  | ||||
| 	return &walEntry, nil | ||||
| } | ||||
|  | ||||
| type setStaticAccountInput struct { | ||||
| 	RoleName   string | ||||
| 	Role       *roleEntry | ||||
| 	Password   string | ||||
| 	CreateUser bool | ||||
| 	WALID      string | ||||
| } | ||||
|  | ||||
| type setStaticAccountOutput struct { | ||||
| 	RotationTime time.Time | ||||
| 	Password     string | ||||
| 	// Optional return field, in the event WAL was created and not destroyed | ||||
| 	// during the operation | ||||
| 	WALID string | ||||
| } | ||||
|  | ||||
| // setStaticAccount sets the password for a static account associated with a | ||||
| // Role. This method does many things: | ||||
| // - verifies role exists and is in the allowed roles list | ||||
| // - loads an existing WAL entry if WALID input is given, otherwise creates a | ||||
| // new WAL entry | ||||
| // - gets a database connection | ||||
| // - accepts an input password, otherwise generates a new one via gRPC to the | ||||
| // database plugin | ||||
| // - sets new password for the static account | ||||
| // - uses WAL for ensuring passwords are not lost if storage to Vault fails | ||||
| // | ||||
| // This method does not perform any operations on the priority queue. Those | ||||
| // tasks must be handled outside of this method. | ||||
| func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storage, input *setStaticAccountInput) (*setStaticAccountOutput, error) { | ||||
| 	var merr error | ||||
| 	if input == nil || input.Role == nil || input.RoleName == "" { | ||||
| 		return nil, errors.New("input was empty when attempting to set credentials for static account") | ||||
| 	} | ||||
| 	// Re-use WAL ID if present, otherwise PUT a new WAL | ||||
| 	output := &setStaticAccountOutput{WALID: input.WALID} | ||||
|  | ||||
| 	dbConfig, err := b.DatabaseConfig(ctx, s, input.Role.DBName) | ||||
| 	if err != nil { | ||||
| 		return output, err | ||||
| 	} | ||||
|  | ||||
| 	// If role name isn't in the database's allowed roles, send back a | ||||
| 	// permission denied. | ||||
| 	if !strutil.StrListContains(dbConfig.AllowedRoles, "*") && !strutil.StrListContainsGlob(dbConfig.AllowedRoles, input.RoleName) { | ||||
| 		return output, fmt.Errorf("%q is not an allowed role", input.RoleName) | ||||
| 	} | ||||
|  | ||||
| 	// Get the Database object | ||||
| 	db, err := b.GetConnection(ctx, s, input.Role.DBName) | ||||
| 	if err != nil { | ||||
| 		return output, err | ||||
| 	} | ||||
|  | ||||
| 	db.RLock() | ||||
| 	defer db.RUnlock() | ||||
|  | ||||
| 	// Use password from input if available. This happens if we're restoring from | ||||
| 	// a WAL item or processing the rotation queue with an item that has a WAL | ||||
| 	// associated with it | ||||
| 	newPassword := input.Password | ||||
| 	if newPassword == "" { | ||||
| 		// Generate a new password | ||||
| 		newPassword, err = db.GenerateCredentials(ctx) | ||||
| 		if err != nil { | ||||
| 			return output, err | ||||
| 		} | ||||
| 	} | ||||
| 	output.Password = newPassword | ||||
|  | ||||
| 	config := dbplugin.StaticUserConfig{ | ||||
| 		Username: input.Role.StaticAccount.Username, | ||||
| 		Password: newPassword, | ||||
| 	} | ||||
|  | ||||
| 	if output.WALID == "" { | ||||
| 		output.WALID, err = framework.PutWAL(ctx, s, staticWALKey, &setCredentialsWAL{ | ||||
| 			RoleName:          input.RoleName, | ||||
| 			Username:          config.Username, | ||||
| 			NewPassword:       config.Password, | ||||
| 			OldPassword:       input.Role.StaticAccount.Password, | ||||
| 			LastVaultRotation: input.Role.StaticAccount.LastVaultRotation, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return output, errwrap.Wrapf("error writing WAL entry: {{err}}", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	_, password, err := db.SetCredentials(ctx, input.Role.Statements, config) | ||||
| 	if err != nil { | ||||
| 		b.CloseIfShutdown(db, err) | ||||
| 		return output, errwrap.Wrapf("error setting credentials: {{err}}", err) | ||||
| 	} | ||||
|  | ||||
| 	if newPassword != password { | ||||
| 		return output, errors.New("mismatch passwords returned") | ||||
| 	} | ||||
|  | ||||
| 	// Store updated role information | ||||
| 	// lvr is the known LastVaultRotation | ||||
| 	lvr := time.Now() | ||||
| 	input.Role.StaticAccount.LastVaultRotation = lvr | ||||
| 	input.Role.StaticAccount.Password = password | ||||
| 	output.RotationTime = lvr | ||||
|  | ||||
| 	entry, err := logical.StorageEntryJSON(databaseStaticRolePath+input.RoleName, input.Role) | ||||
| 	if err != nil { | ||||
| 		return output, err | ||||
| 	} | ||||
| 	if err := s.Put(ctx, entry); err != nil { | ||||
| 		return output, err | ||||
| 	} | ||||
|  | ||||
| 	// Cleanup WAL after successfully rotating and pushing new item on to queue | ||||
| 	if err := framework.DeleteWAL(ctx, s, output.WALID); err != nil { | ||||
| 		merr = multierror.Append(merr, err) | ||||
| 		return output, merr | ||||
| 	} | ||||
|  | ||||
| 	// The WAL has been deleted, return new setStaticAccountOutput without it | ||||
| 	return &setStaticAccountOutput{RotationTime: lvr}, merr | ||||
| } | ||||
|  | ||||
| // initQueue preforms the necessary checks and initializations needed to preform | ||||
| // automatic credential rotation for roles associated with static accounts. This | ||||
| // method verifies if a queue is needed (primary server or local mount), and if | ||||
| // so initializes the queue and launches a go-routine to periodically invoke a | ||||
| // method to preform the rotations. | ||||
| // | ||||
| // initQueue is invoked by the Factory method in a go-routine. The Factory does | ||||
| // not wait for success or failure of it's tasks before continuing. This is to | ||||
| // avoid blocking the mount process while loading and evaluating existing roles, | ||||
| // etc. | ||||
| func (b *databaseBackend) initQueue(ctx context.Context, conf *logical.BackendConfig) { | ||||
| 	// Verify this mount is on the primary server, or is a local mount. If not, do | ||||
| 	// not create a queue or launch a ticker. Both processing the WAL list and | ||||
| 	// populating the queue are done sequentially and before launching a | ||||
| 	// go-routine to run the periodic ticker. | ||||
| 	replicationState := conf.System.ReplicationState() | ||||
| 	if (conf.System.LocalMount() || !replicationState.HasState(consts.ReplicationPerformanceSecondary)) && | ||||
| 		!replicationState.HasState(consts.ReplicationDRSecondary) && | ||||
| 		!replicationState.HasState(consts.ReplicationPerformanceStandby) { | ||||
| 		b.Logger().Info("initializing database rotation queue") | ||||
|  | ||||
| 		// Poll for a PutWAL call that does not return a "read-only storage" error. | ||||
| 		// This ensures the startup phases of loading WAL entries from any possible | ||||
| 		// failed rotations can complete without error when deleting from storage. | ||||
| 	READONLY_LOOP: | ||||
| 		for { | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				b.Logger().Info("queue initialization canceled") | ||||
| 				return | ||||
| 			default: | ||||
| 			} | ||||
|  | ||||
| 			walID, err := framework.PutWAL(ctx, conf.StorageView, staticWALKey, &setCredentialsWAL{RoleName: "vault-readonlytest"}) | ||||
| 			if walID != "" { | ||||
| 				defer framework.DeleteWAL(ctx, conf.StorageView, walID) | ||||
| 			} | ||||
| 			switch { | ||||
| 			case err == nil: | ||||
| 				break READONLY_LOOP | ||||
| 			case err.Error() == logical.ErrSetupReadOnly.Error(): | ||||
| 				time.Sleep(10 * time.Millisecond) | ||||
| 			default: | ||||
| 				b.Logger().Error("deleting nil key resulted in error", "error", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Load roles and populate queue with static accounts | ||||
| 		b.populateQueue(ctx, conf.StorageView) | ||||
|  | ||||
| 		// Launch ticker | ||||
| 		go b.runTicker(ctx, conf.StorageView) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // loadStaticWALs reads WAL entries and returns a map of roles and their | ||||
| // setCredentialsWAL, if found. | ||||
| func (b *databaseBackend) loadStaticWALs(ctx context.Context, s logical.Storage) (map[string]*setCredentialsWAL, error) { | ||||
| 	keys, err := framework.ListWAL(ctx, s) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(keys) == 0 { | ||||
| 		b.Logger().Debug("no WAL entries found") | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	walMap := make(map[string]*setCredentialsWAL) | ||||
| 	// Loop through WAL keys and process any rotation ones | ||||
| 	for _, walID := range keys { | ||||
| 		walEntry, err := b.findStaticWAL(ctx, s, walID) | ||||
| 		if err != nil { | ||||
| 			b.Logger().Error("error loading static WAL", "id", walID, "error", err) | ||||
| 			continue | ||||
| 		} | ||||
| 		if walEntry == nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Verify the static role still exists | ||||
| 		roleName := walEntry.RoleName | ||||
| 		role, err := b.StaticRole(ctx, s, roleName) | ||||
| 		if err != nil { | ||||
| 			b.Logger().Warn("unable to read static role", "error", err, "role", roleName) | ||||
| 			continue | ||||
| 		} | ||||
| 		if role == nil || role.StaticAccount == nil { | ||||
| 			if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { | ||||
| 				b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID) | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		walEntry.walID = walID | ||||
| 		walMap[walEntry.RoleName] = walEntry | ||||
| 	} | ||||
| 	return walMap, nil | ||||
| } | ||||
|  | ||||
| // pushItem wraps the internal queue's Push call, to make sure a queue is | ||||
| // actually available. This is needed because both runTicker and initQueue | ||||
| // operate in go-routines, and could be accessing the queue concurrently | ||||
| func (b *databaseBackend) pushItem(item *queue.Item) error { | ||||
| 	b.RLock() | ||||
| 	unlockFunc := b.RUnlock | ||||
| 	defer func() { unlockFunc() }() | ||||
|  | ||||
| 	if b.credRotationQueue != nil { | ||||
| 		return b.credRotationQueue.Push(item) | ||||
| 	} | ||||
|  | ||||
| 	b.Logger().Warn("no queue found during push item") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // popFromRotationQueue wraps the internal queue's Pop call, to make sure a queue is | ||||
| // actually available. This is needed because both runTicker and initQueue | ||||
| // operate in go-routines, and could be accessing the queue concurrently | ||||
| func (b *databaseBackend) popFromRotationQueue() (*queue.Item, error) { | ||||
| 	b.RLock() | ||||
| 	defer b.RUnlock() | ||||
| 	if b.credRotationQueue != nil { | ||||
| 		return b.credRotationQueue.Pop() | ||||
| 	} | ||||
| 	return nil, queue.ErrEmpty | ||||
| } | ||||
|  | ||||
| // popFromRotationQueueByKey wraps the internal queue's PopByKey call, to make sure a queue is | ||||
| // actually available. This is needed because both runTicker and initQueue | ||||
| // operate in go-routines, and could be accessing the queue concurrently | ||||
| func (b *databaseBackend) popFromRotationQueueByKey(name string) (*queue.Item, error) { | ||||
| 	b.RLock() | ||||
| 	defer b.RUnlock() | ||||
| 	if b.credRotationQueue != nil { | ||||
| 		return b.credRotationQueue.PopByKey(name) | ||||
| 	} | ||||
| 	return nil, queue.ErrEmpty | ||||
| } | ||||
							
								
								
									
										814
									
								
								builtin/logical/database/rotation_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										814
									
								
								builtin/logical/database/rotation_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,814 @@ | ||||
| package database | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"database/sql" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/helper/namespace" | ||||
| 	"github.com/hashicorp/vault/sdk/framework" | ||||
| 	"github.com/hashicorp/vault/sdk/logical" | ||||
|  | ||||
| 	_ "github.com/lib/pq" | ||||
| ) | ||||
|  | ||||
| func TestBackend_StaticRole_Rotate_basic(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	lb, err := Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	b, ok := lb.(*databaseBackend) | ||||
| 	if !ok { | ||||
| 		t.Fatal("could not convert to db backend") | ||||
| 	} | ||||
| 	defer b.Cleanup(context.Background()) | ||||
|  | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	// Configure a connection | ||||
| 	data := map[string]interface{}{ | ||||
| 		"connection_url":    connURL, | ||||
| 		"plugin_name":       "postgresql-database-plugin", | ||||
| 		"verify_connection": false, | ||||
| 		"allowed_roles":     []string{"*"}, | ||||
| 		"name":              "plugin-test", | ||||
| 	} | ||||
|  | ||||
| 	req := &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "config/plugin-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	data = map[string]interface{}{ | ||||
| 		"name":                  "plugin-role-test", | ||||
| 		"db_name":               "plugin-test", | ||||
| 		"creation_statements":   testRoleStaticCreate, | ||||
| 		"rotation_statements":   testRoleStaticUpdate, | ||||
| 		"revocation_statements": defaultRevocationSQL, | ||||
| 		"username":              "statictest", | ||||
| 		"rotation_period":       "5400s", | ||||
| 	} | ||||
|  | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "static-roles/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// Read the creds | ||||
| 	data = map[string]interface{}{} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-creds/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	username := resp.Data["username"].(string) | ||||
| 	password := resp.Data["password"].(string) | ||||
| 	if username == "" || password == "" { | ||||
| 		t.Fatalf("empty username (%s) or password (%s)", username, password) | ||||
| 	} | ||||
|  | ||||
| 	// Verify username/password | ||||
| 	if err := verifyPgConn(t, username, password, connURL); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// Re-read the creds, verifying they aren't changing on read | ||||
| 	data = map[string]interface{}{} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-creds/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	if username != resp.Data["username"].(string) || password != resp.Data["password"].(string) { | ||||
| 		t.Fatal("expected re-read username/password to match, but didn't") | ||||
| 	} | ||||
|  | ||||
| 	// Trigger rotation | ||||
| 	data = map[string]interface{}{"name": "plugin-role-test"} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "rotate-role/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	if resp != nil { | ||||
| 		t.Fatalf("Expected empty response from rotate-role: (%#v)", resp) | ||||
| 	} | ||||
|  | ||||
| 	// Re-Read the creds | ||||
| 	data = map[string]interface{}{} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-creds/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	newPassword := resp.Data["password"].(string) | ||||
| 	if password == newPassword { | ||||
| 		t.Fatalf("expected passwords to differ, got (%s)", newPassword) | ||||
| 	} | ||||
|  | ||||
| 	// Verify new username/password | ||||
| 	if err := verifyPgConn(t, username, newPassword, connURL); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Sanity check to make sure we don't allow an attempt of rotating credentials | ||||
| // for non-static accounts, which doesn't make sense anyway, but doesn't hurt to | ||||
| // verify we return an error | ||||
| func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	lb, err := Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	b, ok := lb.(*databaseBackend) | ||||
| 	if !ok { | ||||
| 		t.Fatal("could not convert to db backend") | ||||
| 	} | ||||
| 	defer b.Cleanup(context.Background()) | ||||
|  | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	// Configure a connection | ||||
| 	data := map[string]interface{}{ | ||||
| 		"connection_url":    connURL, | ||||
| 		"plugin_name":       "postgresql-database-plugin", | ||||
| 		"verify_connection": false, | ||||
| 		"allowed_roles":     []string{"*"}, | ||||
| 		"name":              "plugin-test", | ||||
| 	} | ||||
|  | ||||
| 	req := &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "config/plugin-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	data = map[string]interface{}{ | ||||
| 		"name":                  "plugin-role-test", | ||||
| 		"db_name":               "plugin-test", | ||||
| 		"creation_statements":   testRoleStaticCreate, | ||||
| 		"rotation_statements":   testRoleStaticUpdate, | ||||
| 		"revocation_statements": defaultRevocationSQL, | ||||
| 	} | ||||
|  | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "roles/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// Read the creds | ||||
| 	data = map[string]interface{}{} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "creds/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	username := resp.Data["username"].(string) | ||||
| 	password := resp.Data["password"].(string) | ||||
| 	if username == "" || password == "" { | ||||
| 		t.Fatalf("empty username (%s) or password (%s)", username, password) | ||||
| 	} | ||||
|  | ||||
| 	// Verify username/password | ||||
| 	if err := verifyPgConn(t, username, password, connURL); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// Trigger rotation | ||||
| 	data = map[string]interface{}{"name": "plugin-role-test"} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "rotate-role/plugin-role-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	// expect resp to be an error | ||||
| 	resp, _ = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if !resp.IsError() { | ||||
| 		t.Fatalf("expected error rotating non-static role") | ||||
| 	} | ||||
|  | ||||
| 	if resp.Error().Error() != "no static role found for role name" { | ||||
| 		t.Fatalf("wrong error message: %s", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestBackend_StaticRole_Revoke_user(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	lb, err := Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	b, ok := lb.(*databaseBackend) | ||||
| 	if !ok { | ||||
| 		t.Fatal("could not convert to db backend") | ||||
| 	} | ||||
| 	defer b.Cleanup(context.Background()) | ||||
|  | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	// Configure a connection | ||||
| 	data := map[string]interface{}{ | ||||
| 		"connection_url":    connURL, | ||||
| 		"plugin_name":       "postgresql-database-plugin", | ||||
| 		"verify_connection": false, | ||||
| 		"allowed_roles":     []string{"*"}, | ||||
| 		"name":              "plugin-test", | ||||
| 	} | ||||
|  | ||||
| 	req := &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "config/plugin-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	testCases := map[string]struct { | ||||
| 		revoke          *bool | ||||
| 		expectVerifyErr bool | ||||
| 	}{ | ||||
| 		// Default case: user does not specify, Vault leaves the database user | ||||
| 		// untouched, and the final connection check passes because the user still | ||||
| 		// exists | ||||
| 		"unset": {}, | ||||
| 		// Revoke on delete. The final connection check should fail because the user | ||||
| 		// no longer exists | ||||
| 		"revoke": { | ||||
| 			revoke:          newBoolPtr(true), | ||||
| 			expectVerifyErr: true, | ||||
| 		}, | ||||
| 		// Revoke false, final connection check should still pass | ||||
| 		"persist": { | ||||
| 			revoke: newBoolPtr(false), | ||||
| 		}, | ||||
| 	} | ||||
| 	for k, tc := range testCases { | ||||
| 		t.Run(k, func(t *testing.T) { | ||||
| 			data = map[string]interface{}{ | ||||
| 				"name":                  "plugin-role-test", | ||||
| 				"db_name":               "plugin-test", | ||||
| 				"creation_statements":   testRoleStaticCreate, | ||||
| 				"rotation_statements":   testRoleStaticUpdate, | ||||
| 				"revocation_statements": defaultRevocationSQL, | ||||
| 				"username":              "statictest", | ||||
| 				"rotation_period":       "5400s", | ||||
| 			} | ||||
| 			if tc.revoke != nil { | ||||
| 				data["revoke_user_on_delete"] = *tc.revoke | ||||
| 			} | ||||
|  | ||||
| 			req = &logical.Request{ | ||||
| 				Operation: logical.CreateOperation, | ||||
| 				Path:      "static-roles/plugin-role-test", | ||||
| 				Storage:   config.StorageView, | ||||
| 				Data:      data, | ||||
| 			} | ||||
|  | ||||
| 			resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 			if err != nil || (resp != nil && resp.IsError()) { | ||||
| 				t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 			} | ||||
|  | ||||
| 			// Read the creds | ||||
| 			data = map[string]interface{}{} | ||||
| 			req = &logical.Request{ | ||||
| 				Operation: logical.ReadOperation, | ||||
| 				Path:      "static-creds/plugin-role-test", | ||||
| 				Storage:   config.StorageView, | ||||
| 				Data:      data, | ||||
| 			} | ||||
|  | ||||
| 			resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 			if err != nil || (resp != nil && resp.IsError()) { | ||||
| 				t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 			} | ||||
|  | ||||
| 			username := resp.Data["username"].(string) | ||||
| 			password := resp.Data["password"].(string) | ||||
| 			if username == "" || password == "" { | ||||
| 				t.Fatalf("empty username (%s) or password (%s)", username, password) | ||||
| 			} | ||||
|  | ||||
| 			// Verify username/password | ||||
| 			if err := verifyPgConn(t, username, password, connURL); err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
|  | ||||
| 			// delete the role, expect the default where the user is not destroyed | ||||
| 			// Read the creds | ||||
| 			req = &logical.Request{ | ||||
| 				Operation: logical.DeleteOperation, | ||||
| 				Path:      "static-roles/plugin-role-test", | ||||
| 				Storage:   config.StorageView, | ||||
| 			} | ||||
|  | ||||
| 			resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 			if err != nil || (resp != nil && resp.IsError()) { | ||||
| 				t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 			} | ||||
|  | ||||
| 			// Verify new username/password still work | ||||
| 			if err := verifyPgConn(t, username, password, connURL); err != nil { | ||||
| 				if !tc.expectVerifyErr { | ||||
| 					t.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func verifyPgConn(t *testing.T, username, password, connURL string) error { | ||||
| 	cURL := strings.Replace(connURL, "postgres:secret", username+":"+password, 1) | ||||
| 	db, err := sql.Open("postgres", cURL) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := db.Ping(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return db.Close() | ||||
| } | ||||
|  | ||||
| // WAL testing | ||||
| // | ||||
| // First scenario, WAL contains a role name that does not exist. | ||||
| func TestBackend_Static_QueueWAL_discard_role_not_found(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	_, err := framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{ | ||||
| 		RoleName: "doesnotexist", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error with PutWAL: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	assertWALCount(t, config.StorageView, 1) | ||||
|  | ||||
| 	b, err := Factory(ctx, config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer b.Cleanup(ctx) | ||||
|  | ||||
| 	time.Sleep(5 * time.Second) | ||||
| 	bd := b.(*databaseBackend) | ||||
| 	if bd.credRotationQueue == nil { | ||||
| 		t.Fatal("database backend had no credential rotation queue") | ||||
| 	} | ||||
|  | ||||
| 	// Verify empty queue | ||||
| 	if bd.credRotationQueue.Len() != 0 { | ||||
| 		t.Fatalf("expected zero queue items, got: %d", bd.credRotationQueue.Len()) | ||||
| 	} | ||||
|  | ||||
| 	assertWALCount(t, config.StorageView, 0) | ||||
| } | ||||
|  | ||||
| // Second scenario, WAL contains a role name that does exist, but the role's | ||||
| // LastVaultRotation is greater than the WAL has | ||||
| func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	roleName := "test-discard-by-date" | ||||
| 	lb, err := Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	b, ok := lb.(*databaseBackend) | ||||
| 	if !ok { | ||||
| 		t.Fatal("could not convert to db backend") | ||||
| 	} | ||||
|  | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	// Configure a connection | ||||
| 	data := map[string]interface{}{ | ||||
| 		"connection_url":    connURL, | ||||
| 		"plugin_name":       "postgresql-database-plugin", | ||||
| 		"verify_connection": false, | ||||
| 		"allowed_roles":     []string{"*"}, | ||||
| 		"name":              "plugin-test", | ||||
| 	} | ||||
|  | ||||
| 	req := &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "config/plugin-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// Save Now() to make sure rotation time is after this, as well as the WAL | ||||
| 	// time | ||||
| 	roleTime := time.Now() | ||||
|  | ||||
| 	// Create role | ||||
| 	data = map[string]interface{}{ | ||||
| 		"name":                  roleName, | ||||
| 		"db_name":               "plugin-test", | ||||
| 		"creation_statements":   testRoleStaticCreate, | ||||
| 		"rotation_statements":   testRoleStaticUpdate, | ||||
| 		"revocation_statements": defaultRevocationSQL, | ||||
| 		"username":              "statictest", | ||||
| 		// Low value here, to make sure the backend rotates this password at least | ||||
| 		// once before we compare it to the WAL | ||||
| 		"rotation_period": "10s", | ||||
| 	} | ||||
|  | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "static-roles/" + roleName, | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// Allow the first rotation to occur, setting LastVaultRotation | ||||
| 	time.Sleep(time.Second * 12) | ||||
|  | ||||
| 	// Cleanup the backend, then create a WAL for the role with a | ||||
| 	// LastVaultRotation of 1 hour ago, so that when we recreate the backend the | ||||
| 	// WAL will be read but discarded | ||||
| 	b.Cleanup(ctx) | ||||
| 	b = nil | ||||
| 	time.Sleep(time.Second * 3) | ||||
|  | ||||
| 	// Make a fake WAL entry with an older time | ||||
| 	oldRotationTime := roleTime.Add(time.Hour * -1) | ||||
| 	walPassword := "somejunkpassword" | ||||
| 	_, err = framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{ | ||||
| 		RoleName:          roleName, | ||||
| 		NewPassword:       walPassword, | ||||
| 		LastVaultRotation: oldRotationTime, | ||||
| 		Username:          "statictest", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error with PutWAL: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	assertWALCount(t, config.StorageView, 1) | ||||
|  | ||||
| 	// Reload backend | ||||
| 	lb, err = Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	b, ok = lb.(*databaseBackend) | ||||
| 	if !ok { | ||||
| 		t.Fatal("could not convert to db backend") | ||||
| 	} | ||||
| 	defer b.Cleanup(ctx) | ||||
|  | ||||
| 	// Allow enough time for populateQueue to work after boot | ||||
| 	time.Sleep(time.Second * 12) | ||||
|  | ||||
| 	// PopulateQueue should have processed the entry | ||||
| 	assertWALCount(t, config.StorageView, 0) | ||||
|  | ||||
| 	// Read the role | ||||
| 	data = map[string]interface{}{} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-roles/" + roleName, | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	lastVaultRotation := resp.Data["last_vault_rotation"].(time.Time) | ||||
| 	if !lastVaultRotation.After(oldRotationTime) { | ||||
| 		t.Fatal("last vault rotation time not greater than WAL time") | ||||
| 	} | ||||
|  | ||||
| 	if !lastVaultRotation.After(roleTime) { | ||||
| 		t.Fatal("last vault rotation time not greater than role creation time") | ||||
| 	} | ||||
|  | ||||
| 	// Grab password to verify it didn't change | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      "static-creds/" + roleName, | ||||
| 		Storage:   config.StorageView, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	password := resp.Data["password"].(string) | ||||
| 	if password == walPassword { | ||||
| 		t.Fatalf("expected password to not be changed by WAL, but was") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Helper to assert the number of WAL entries is what we expect | ||||
| func assertWALCount(t *testing.T, s logical.Storage, expected int) { | ||||
| 	var count int | ||||
| 	ctx := context.Background() | ||||
| 	keys, err := framework.ListWAL(ctx, s) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("error listing WALs") | ||||
| 	} | ||||
|  | ||||
| 	// Loop through WAL keys and process any rotation ones | ||||
| 	for _, k := range keys { | ||||
| 		walEntry, _ := framework.GetWAL(ctx, s, k) | ||||
| 		if walEntry == nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if walEntry.Kind != staticWALKey { | ||||
| 			continue | ||||
| 		} | ||||
| 		count++ | ||||
| 	} | ||||
| 	if expected != count { | ||||
| 		t.Fatalf("WAL count mismatch, expected (%d), got (%d)", expected, count) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // | ||||
| // End WAL testing | ||||
| // | ||||
|  | ||||
| func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) { | ||||
| 	cluster, sys := getCluster(t) | ||||
| 	defer cluster.Cleanup() | ||||
|  | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	config.StorageView = &logical.InmemStorage{} | ||||
| 	config.System = sys | ||||
|  | ||||
| 	b, err := Factory(context.Background(), config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer b.Cleanup(context.Background()) | ||||
|  | ||||
| 	bd := b.(*databaseBackend) | ||||
| 	if bd.credRotationQueue == nil { | ||||
| 		t.Fatal("database backend had no credential rotation queue") | ||||
| 	} | ||||
|  | ||||
| 	// Configure backend, add item and confirm length | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	// Configure a connection | ||||
| 	data := map[string]interface{}{ | ||||
| 		"connection_url":    connURL, | ||||
| 		"plugin_name":       "postgresql-database-plugin", | ||||
| 		"verify_connection": false, | ||||
| 		"allowed_roles":     []string{"*"}, | ||||
| 		"name":              "plugin-test", | ||||
| 	} | ||||
| 	req := &logical.Request{ | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Path:      "config/plugin-test", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	// Create three static roles with different rotation periods | ||||
| 	testCases := []string{"65", "130", "5400"} | ||||
| 	for _, tc := range testCases { | ||||
| 		roleName := "plugin-static-role-" + tc | ||||
| 		data = map[string]interface{}{ | ||||
| 			"name":                  roleName, | ||||
| 			"db_name":               "plugin-test", | ||||
| 			"creation_statements":   testRoleStaticCreate, | ||||
| 			"rotation_statements":   testRoleStaticUpdate, | ||||
| 			"revocation_statements": defaultRevocationSQL, | ||||
| 			"username":              "statictest" + tc, | ||||
| 			"rotation_period":       tc, | ||||
| 		} | ||||
|  | ||||
| 		req = &logical.Request{ | ||||
| 			Operation: logical.CreateOperation, | ||||
| 			Path:      "static-roles/" + roleName, | ||||
| 			Storage:   config.StorageView, | ||||
| 			Data:      data, | ||||
| 		} | ||||
|  | ||||
| 		resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 		if err != nil || (resp != nil && resp.IsError()) { | ||||
| 			t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Verify the queue has 3 items in it | ||||
| 	if bd.credRotationQueue.Len() != 3 { | ||||
| 		t.Fatalf("expected 3 items in the rotation queue, got: (%d)", bd.credRotationQueue.Len()) | ||||
| 	} | ||||
|  | ||||
| 	// List the roles | ||||
| 	data = map[string]interface{}{} | ||||
| 	req = &logical.Request{ | ||||
| 		Operation: logical.ListOperation, | ||||
| 		Path:      "static-roles/", | ||||
| 		Storage:   config.StorageView, | ||||
| 		Data:      data, | ||||
| 	} | ||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	keys := resp.Data["keys"].([]string) | ||||
| 	if len(keys) != 3 { | ||||
| 		t.Fatalf("expected 3 roles, got: (%d)", len(keys)) | ||||
| 	} | ||||
|  | ||||
| 	// Capture initial passwords, before the periodic function is triggered | ||||
| 	pws := make(map[string][]string, 0) | ||||
| 	pws = capturePasswords(t, b, config, testCases, pws) | ||||
|  | ||||
| 	// Sleep to make sure the 65s role will be up for rotation by the time the | ||||
| 	// periodic function ticks | ||||
| 	time.Sleep(7 * time.Second) | ||||
|  | ||||
| 	// Sleep 75 to make sure the periodic func has time to actually run | ||||
| 	time.Sleep(75 * time.Second) | ||||
| 	pws = capturePasswords(t, b, config, testCases, pws) | ||||
|  | ||||
| 	// Sleep more, this should allow both sr65 and sr130 to rotate | ||||
| 	time.Sleep(140 * time.Second) | ||||
| 	pws = capturePasswords(t, b, config, testCases, pws) | ||||
|  | ||||
| 	// Verify all pws are as they should | ||||
| 	pass := true | ||||
| 	for k, v := range pws { | ||||
| 		switch { | ||||
| 		case k == "plugin-static-role-65": | ||||
| 			// expect all passwords to be different | ||||
| 			if v[0] == v[1] || v[1] == v[2] || v[0] == v[2] { | ||||
| 				pass = false | ||||
| 			} | ||||
| 		case k == "plugin-static-role-130": | ||||
| 			// expect the first two to be equal, but different from the third | ||||
| 			if v[0] != v[1] || v[0] == v[2] { | ||||
| 				pass = false | ||||
| 			} | ||||
| 		case k == "plugin-static-role-5400": | ||||
| 			// expect all passwords to be equal | ||||
| 			if v[0] != v[1] || v[1] != v[2] { | ||||
| 				pass = false | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if !pass { | ||||
| 		t.Fatalf("password rotations did not match expected: %#v", pws) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // capturePasswords captures the current passwords at the time of calling, and | ||||
| // returns a map of username / passwords building off of the input map | ||||
| func capturePasswords(t *testing.T, b logical.Backend, config *logical.BackendConfig, testCases []string, pws map[string][]string) map[string][]string { | ||||
| 	new := make(map[string][]string, 0) | ||||
| 	for _, tc := range testCases { | ||||
| 		// Read the role | ||||
| 		roleName := "plugin-static-role-" + tc | ||||
| 		req := &logical.Request{ | ||||
| 			Operation: logical.ReadOperation, | ||||
| 			Path:      "static-creds/" + roleName, | ||||
| 			Storage:   config.StorageView, | ||||
| 		} | ||||
| 		resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||
| 		if err != nil || (resp != nil && resp.IsError()) { | ||||
| 			t.Fatalf("err:%s resp:%#v\n", err, resp) | ||||
| 		} | ||||
|  | ||||
| 		username := resp.Data["username"].(string) | ||||
| 		password := resp.Data["password"].(string) | ||||
| 		if username == "" || password == "" { | ||||
| 			t.Fatalf("expected both username/password for (%s), got (%s), (%s)", roleName, username, password) | ||||
| 		} | ||||
| 		new[roleName] = append(new[roleName], password) | ||||
| 	} | ||||
|  | ||||
| 	for k, v := range new { | ||||
| 		pws[k] = append(pws[k], v...) | ||||
| 	} | ||||
|  | ||||
| 	return pws | ||||
| } | ||||
|  | ||||
| func newBoolPtr(b bool) *bool { | ||||
| 	v := b | ||||
| 	return &v | ||||
| } | ||||
| @@ -239,3 +239,12 @@ func (c *Cassandra) RotateRootCredentials(ctx context.Context, statements []stri | ||||
| 	c.rawConfig["password"] = password | ||||
| 	return c.rawConfig, nil | ||||
| } | ||||
|  | ||||
| // GenerateCredentials returns a generated password | ||||
| func (c *Cassandra) GenerateCredentials(ctx context.Context) (string, error) { | ||||
| 	password, err := c.GeneratePassword() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return password, nil | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
|  | ||||
| 	"github.com/gocql/gocql" | ||||
| 	"github.com/hashicorp/errwrap" | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/connutil" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/dbutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/certutil" | ||||
| @@ -278,3 +279,13 @@ func (c *cassandraConnectionProducer) secretValues() map[string]interface{} { | ||||
| 		c.PemJSON:   "[pem_json]", | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SetCredentials uses provided information to set/create a user in the | ||||
| // database. Unlike CreateUser, this method requires a username be provided and | ||||
| // uses the name given, instead of generating a name. This is used for creating | ||||
| // and setting the password of static accounts, as well as rolling back | ||||
| // passwords in the database in the event an updated database fails to save in | ||||
| // Vault's storage. | ||||
| func (c *cassandraConnectionProducer) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { | ||||
| 	return "", "", dbutil.Unimplemented() | ||||
| } | ||||
|   | ||||
| @@ -293,3 +293,12 @@ func (h *HANA) revokeUserDefault(ctx context.Context, username string) error { | ||||
| func (h *HANA) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) { | ||||
| 	return nil, errors.New("root credentaion rotation is not currently implemented in this database secrets engine") | ||||
| } | ||||
|  | ||||
| // GenerateCredentials returns a generated password | ||||
| func (h *HANA) GenerateCredentials(ctx context.Context) (string, error) { | ||||
| 	password, err := h.GeneratePassword() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return password, nil | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,9 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/errwrap" | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/connutil" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/dbutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/certutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/parseutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/tlsutil" | ||||
| @@ -261,3 +263,13 @@ func isUserAdmin(cli influx.Client, user string) (bool, error) { | ||||
| 	} | ||||
| 	return false, fmt.Errorf("the provided username is not a valid user in the influxdb") | ||||
| } | ||||
|  | ||||
| // SetCredentials uses provided information to set/create a user in the | ||||
| // database. Unlike CreateUser, this method requires a username be provided and | ||||
| // uses the name given, instead of generating a name. This is used for creating | ||||
| // and setting the password of static accounts, as well as rolling back | ||||
| // passwords in the database in the event an updated database fails to save in | ||||
| // Vault's storage. | ||||
| func (i *influxdbConnectionProducer) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { | ||||
| 	return "", "", dbutil.Unimplemented() | ||||
| } | ||||
|   | ||||
| @@ -242,3 +242,12 @@ func (i *Influxdb) RotateRootCredentials(ctx context.Context, statements []strin | ||||
| 	i.rawConfig["password"] = password | ||||
| 	return i.rawConfig, nil | ||||
| } | ||||
|  | ||||
| // GenerateCredentials returns a generated password | ||||
| func (i *Influxdb) GenerateCredentials(ctx context.Context) (string, error) { | ||||
| 	password, err := i.GeneratePassword() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return password, nil | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/errwrap" | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/connutil" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/dbutil" | ||||
| 	"github.com/mitchellh/mapstructure" | ||||
| @@ -153,6 +154,16 @@ func (c *mongoDBConnectionProducer) Close() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // SetCredentials uses provided information to set/create a user in the | ||||
| // database. Unlike CreateUser, this method requires a username be provided and | ||||
| // uses the name given, instead of generating a name. This is used for creating | ||||
| // and setting the password of static accounts, as well as rolling back | ||||
| // passwords in the database in the event an updated database fails to save in | ||||
| // Vault's storage. | ||||
| func (c *mongoDBConnectionProducer) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { | ||||
| 	return "", "", dbutil.Unimplemented() | ||||
| } | ||||
|  | ||||
| func parseMongoURL(rawURL string) (*mgo.DialInfo, error) { | ||||
| 	url, err := url.Parse(rawURL) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -224,3 +224,12 @@ func (m *MongoDB) RevokeUser(ctx context.Context, statements dbplugin.Statements | ||||
| func (m *MongoDB) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) { | ||||
| 	return nil, errors.New("root credential rotation is not currently implemented in this database secrets engine") | ||||
| } | ||||
|  | ||||
| // GenerateCredentials returns a generated password | ||||
| func (m *MongoDB) GenerateCredentials(ctx context.Context) (string, error) { | ||||
| 	password, err := m.GeneratePassword() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return password, nil | ||||
| } | ||||
|   | ||||
| @@ -381,3 +381,12 @@ END | ||||
| const rotateRootCredentialsSQL = ` | ||||
| ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}'  | ||||
| ` | ||||
|  | ||||
| // GenerateCredentials returns a generated password | ||||
| func (m *MSSQL) GenerateCredentials(ctx context.Context) (string, error) { | ||||
| 	password, err := m.GeneratePassword() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return password, nil | ||||
| } | ||||
|   | ||||
| @@ -315,3 +315,12 @@ func (m *MySQL) RotateRootCredentials(ctx context.Context, statements []string) | ||||
| 	m.RawConfig["password"] = password | ||||
| 	return m.RawConfig, nil | ||||
| } | ||||
|  | ||||
| // GenerateCredentials returns a generated password | ||||
| func (m *MySQL) GenerateCredentials(ctx context.Context) (string, error) { | ||||
| 	password, err := m.GeneratePassword() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return password, nil | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,10 @@ ALTER ROLE "{{name}}" VALID UNTIL '{{expiration}}'; | ||||
| ` | ||||
| 	defaultPostgresRotateRootCredentialsSQL = ` | ||||
| ALTER ROLE "{{username}}" WITH PASSWORD '{{password}}'; | ||||
| ` | ||||
|  | ||||
| 	defaultPostgresRotateCredentialsSQL = ` | ||||
| ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}'; | ||||
| ` | ||||
| ) | ||||
|  | ||||
| @@ -88,6 +92,86 @@ func (p *PostgreSQL) getConnection(ctx context.Context) (*sql.DB, error) { | ||||
| 	return db.(*sql.DB), nil | ||||
| } | ||||
|  | ||||
| // SetCredentials uses provided information to set/create a user in the | ||||
| // database. Unlike CreateUser, this method requires a username be provided and | ||||
| // uses the name given, instead of generating a name. This is used for creating | ||||
| // and setting the password of static accounts, as well as rolling back | ||||
| // passwords in the database in the event an updated database fails to save in | ||||
| // Vault's storage. | ||||
| func (p *PostgreSQL) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { | ||||
| 	if len(statements.Creation) == 0 { | ||||
| 		return "", "", errors.New("empty creation statements") | ||||
| 	} | ||||
|  | ||||
| 	username = staticUser.Username | ||||
| 	password = staticUser.Password | ||||
| 	if username == "" || password == "" { | ||||
| 		return "", "", errors.New("must provide both username and password") | ||||
| 	} | ||||
|  | ||||
| 	// Grab the lock | ||||
| 	p.Lock() | ||||
| 	defer p.Unlock() | ||||
|  | ||||
| 	// Get the connection | ||||
| 	db, err := p.getConnection(ctx) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
|  | ||||
| 	// Check if the role exists | ||||
| 	var exists bool | ||||
| 	err = db.QueryRowContext(ctx, "SELECT exists (SELECT rolname FROM pg_roles WHERE rolname=$1);", username).Scan(&exists) | ||||
| 	if err != nil && err != sql.ErrNoRows { | ||||
| 		return "", "", err | ||||
| 	} | ||||
|  | ||||
| 	// Default to using Creation statements, which are required by the Vault | ||||
| 	// backend. If the user exists, use the rotation statements, using the default | ||||
| 	// ones if there are none provided | ||||
| 	stmts := statements.Creation | ||||
| 	if exists { | ||||
| 		stmts = statements.Rotation | ||||
| 		if len(stmts) == 0 { | ||||
| 			stmts = []string{defaultPostgresRotateCredentialsSQL} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Start a transaction | ||||
| 	tx, err := db.BeginTx(ctx, nil) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		_ = tx.Rollback() | ||||
| 	}() | ||||
|  | ||||
| 	// Execute each query | ||||
| 	for _, stmt := range stmts { | ||||
| 		for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") { | ||||
| 			query = strings.TrimSpace(query) | ||||
| 			if len(query) == 0 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			m := map[string]string{ | ||||
| 				"name":     staticUser.Username, | ||||
| 				"password": password, | ||||
| 			} | ||||
| 			if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil { | ||||
| 				return "", "", err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Commit the transaction | ||||
| 	if err := tx.Commit(); err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
|  | ||||
| 	return username, password, nil | ||||
| } | ||||
|  | ||||
| func (p *PostgreSQL) CreateUser(ctx context.Context, statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { | ||||
| 	statements = dbutil.StatementCompatibilityHelper(statements) | ||||
|  | ||||
| @@ -129,7 +213,6 @@ func (p *PostgreSQL) CreateUser(ctx context.Context, statements dbplugin.Stateme | ||||
| 	defer func() { | ||||
| 		tx.Rollback() | ||||
| 	}() | ||||
| 	// Return the secret | ||||
|  | ||||
| 	// Execute each query | ||||
| 	for _, stmt := range statements.Creation { | ||||
| @@ -267,7 +350,7 @@ func (p *PostgreSQL) defaultRevokeUser(ctx context.Context, username string) err | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if exists == false { | ||||
| 	if !exists { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| @@ -424,3 +507,12 @@ func (p *PostgreSQL) RotateRootCredentials(ctx context.Context, statements []str | ||||
| 	p.RawConfig["password"] = password | ||||
| 	return p.RawConfig, nil | ||||
| } | ||||
|  | ||||
| // GenerateCredentials returns a generated password | ||||
| func (p *PostgreSQL) GenerateCredentials(ctx context.Context) (string, error) { | ||||
| 	password, err := p.GeneratePassword() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return password, nil | ||||
| } | ||||
|   | ||||
| @@ -317,6 +317,84 @@ func TestPostgreSQL_RevokeUser(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPostgresSQL_SetCredentials(t *testing.T) { | ||||
| 	cleanup, connURL := preparePostgresTestContainer(t) | ||||
| 	defer cleanup() | ||||
|  | ||||
| 	connectionDetails := map[string]interface{}{ | ||||
| 		"connection_url": connURL, | ||||
| 	} | ||||
|  | ||||
| 	db := new() | ||||
| 	_, err := db.Init(context.Background(), connectionDetails, true) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	password, err := db.GenerateCredentials(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	usernameConfig := dbplugin.StaticUserConfig{ | ||||
| 		Username: "test", | ||||
| 		Password: password, | ||||
| 	} | ||||
|  | ||||
| 	// Test with no configured Creation Statement | ||||
| 	username, password, err := db.SetCredentials(context.Background(), dbplugin.Statements{}, usernameConfig) | ||||
| 	if err == nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	statements := dbplugin.Statements{ | ||||
| 		Creation: []string{testPostgresStaticRole}, | ||||
| 	} | ||||
| 	// User should not exist, make sure we can create | ||||
| 	username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	if err := testCredsExist(t, connURL, username, password); err != nil { | ||||
| 		t.Fatalf("Could not connect with new credentials: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	// call SetCredentials again, the user will already exist, password will | ||||
| 	// change. Without rotation statements, this should use the defaults | ||||
| 	newPassword, _ := db.GenerateCredentials(context.Background()) | ||||
| 	usernameConfig.Password = newPassword | ||||
| 	username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	if password != newPassword { | ||||
| 		t.Fatal("passwords should have changed") | ||||
| 	} | ||||
|  | ||||
| 	if err := testCredsExist(t, connURL, username, password); err != nil { | ||||
| 		t.Fatalf("Could not connect with new credentials: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	// generate a new password and supply owr own rotation statements | ||||
| 	newPassword2, _ := db.GenerateCredentials(context.Background()) | ||||
| 	usernameConfig.Password = newPassword2 | ||||
| 	statements.Rotation = []string{testPostgresStaticRoleRotate, testPostgresStaticRoleGrant} | ||||
| 	username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	if password != newPassword2 { | ||||
| 		t.Fatal("passwords should have changed") | ||||
| 	} | ||||
|  | ||||
| 	if err := testCredsExist(t, connURL, username, password); err != nil { | ||||
| 		t.Fatalf("Could not connect with new credentials: %s", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testCredsExist(t testing.TB, connURL, username, password string) error { | ||||
| 	t.Helper() | ||||
| 	// Log in with the new creds | ||||
| @@ -398,3 +476,18 @@ REVOKE USAGE ON SCHEMA public FROM "{{name}}"; | ||||
|  | ||||
| DROP ROLE IF EXISTS "{{name}}"; | ||||
| ` | ||||
|  | ||||
| const testPostgresStaticRole = ` | ||||
| CREATE ROLE "{{name}}" WITH | ||||
|   LOGIN | ||||
|   PASSWORD '{{password}}'; | ||||
| GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; | ||||
| ` | ||||
|  | ||||
| const testPostgresStaticRoleRotate = ` | ||||
| ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}'; | ||||
| ` | ||||
|  | ||||
| const testPostgresStaticRoleGrant = ` | ||||
| GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; | ||||
| ` | ||||
|   | ||||
| @@ -9,6 +9,8 @@ import ( | ||||
| 	proto "github.com/golang/protobuf/proto" | ||||
| 	timestamp "github.com/golang/protobuf/ptypes/timestamp" | ||||
| 	grpc "google.golang.org/grpc" | ||||
| 	codes "google.golang.org/grpc/codes" | ||||
| 	status "google.golang.org/grpc/status" | ||||
| 	math "math" | ||||
| ) | ||||
|  | ||||
| @@ -327,6 +329,7 @@ type Statements struct { | ||||
| 	Revocation           []string `protobuf:"bytes,6,rep,name=revocation,proto3" json:"revocation,omitempty"` | ||||
| 	Rollback             []string `protobuf:"bytes,7,rep,name=rollback,proto3" json:"rollback,omitempty"` | ||||
| 	Renewal              []string `protobuf:"bytes,8,rep,name=renewal,proto3" json:"renewal,omitempty"` | ||||
| 	Rotation             []string `protobuf:"bytes,9,rep,name=rotation,proto3" json:"rotation,omitempty"` | ||||
| 	XXX_NoUnkeyedLiteral struct{} `json:"-"` | ||||
| 	XXX_unrecognized     []byte   `json:"-"` | ||||
| 	XXX_sizecache        int32    `json:"-"` | ||||
| @@ -417,6 +420,13 @@ func (m *Statements) GetRenewal() []string { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *Statements) GetRotation() []string { | ||||
| 	if m != nil { | ||||
| 		return m.Rotation | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type UsernameConfig struct { | ||||
| 	DisplayName          string   `protobuf:"bytes,1,opt,name=DisplayName,proto3" json:"DisplayName,omitempty"` | ||||
| 	RoleName             string   `protobuf:"bytes,2,opt,name=RoleName,proto3" json:"RoleName,omitempty"` | ||||
| @@ -659,6 +669,194 @@ func (m *Empty) XXX_DiscardUnknown() { | ||||
|  | ||||
| var xxx_messageInfo_Empty proto.InternalMessageInfo | ||||
|  | ||||
| type GenerateCredentialsResponse struct { | ||||
| 	Password             string   `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` | ||||
| 	XXX_NoUnkeyedLiteral struct{} `json:"-"` | ||||
| 	XXX_unrecognized     []byte   `json:"-"` | ||||
| 	XXX_sizecache        int32    `json:"-"` | ||||
| } | ||||
|  | ||||
| func (m *GenerateCredentialsResponse) Reset()         { *m = GenerateCredentialsResponse{} } | ||||
| func (m *GenerateCredentialsResponse) String() string { return proto.CompactTextString(m) } | ||||
| func (*GenerateCredentialsResponse) ProtoMessage()    {} | ||||
| func (*GenerateCredentialsResponse) Descriptor() ([]byte, []int) { | ||||
| 	return fileDescriptor_cfa445f4444c6876, []int{13} | ||||
| } | ||||
|  | ||||
| func (m *GenerateCredentialsResponse) XXX_Unmarshal(b []byte) error { | ||||
| 	return xxx_messageInfo_GenerateCredentialsResponse.Unmarshal(m, b) | ||||
| } | ||||
| func (m *GenerateCredentialsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | ||||
| 	return xxx_messageInfo_GenerateCredentialsResponse.Marshal(b, m, deterministic) | ||||
| } | ||||
| func (m *GenerateCredentialsResponse) XXX_Merge(src proto.Message) { | ||||
| 	xxx_messageInfo_GenerateCredentialsResponse.Merge(m, src) | ||||
| } | ||||
| func (m *GenerateCredentialsResponse) XXX_Size() int { | ||||
| 	return xxx_messageInfo_GenerateCredentialsResponse.Size(m) | ||||
| } | ||||
| func (m *GenerateCredentialsResponse) XXX_DiscardUnknown() { | ||||
| 	xxx_messageInfo_GenerateCredentialsResponse.DiscardUnknown(m) | ||||
| } | ||||
|  | ||||
| var xxx_messageInfo_GenerateCredentialsResponse proto.InternalMessageInfo | ||||
|  | ||||
| func (m *GenerateCredentialsResponse) GetPassword() string { | ||||
| 	if m != nil { | ||||
| 		return m.Password | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| type StaticUserConfig struct { | ||||
| 	Username             string   `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` | ||||
| 	Password             string   `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` | ||||
| 	Create               bool     `protobuf:"varint,3,opt,name=create,proto3" json:"create,omitempty"` | ||||
| 	XXX_NoUnkeyedLiteral struct{} `json:"-"` | ||||
| 	XXX_unrecognized     []byte   `json:"-"` | ||||
| 	XXX_sizecache        int32    `json:"-"` | ||||
| } | ||||
|  | ||||
| func (m *StaticUserConfig) Reset()         { *m = StaticUserConfig{} } | ||||
| func (m *StaticUserConfig) String() string { return proto.CompactTextString(m) } | ||||
| func (*StaticUserConfig) ProtoMessage()    {} | ||||
| func (*StaticUserConfig) Descriptor() ([]byte, []int) { | ||||
| 	return fileDescriptor_cfa445f4444c6876, []int{14} | ||||
| } | ||||
|  | ||||
| func (m *StaticUserConfig) XXX_Unmarshal(b []byte) error { | ||||
| 	return xxx_messageInfo_StaticUserConfig.Unmarshal(m, b) | ||||
| } | ||||
| func (m *StaticUserConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | ||||
| 	return xxx_messageInfo_StaticUserConfig.Marshal(b, m, deterministic) | ||||
| } | ||||
| func (m *StaticUserConfig) XXX_Merge(src proto.Message) { | ||||
| 	xxx_messageInfo_StaticUserConfig.Merge(m, src) | ||||
| } | ||||
| func (m *StaticUserConfig) XXX_Size() int { | ||||
| 	return xxx_messageInfo_StaticUserConfig.Size(m) | ||||
| } | ||||
| func (m *StaticUserConfig) XXX_DiscardUnknown() { | ||||
| 	xxx_messageInfo_StaticUserConfig.DiscardUnknown(m) | ||||
| } | ||||
|  | ||||
| var xxx_messageInfo_StaticUserConfig proto.InternalMessageInfo | ||||
|  | ||||
| func (m *StaticUserConfig) GetUsername() string { | ||||
| 	if m != nil { | ||||
| 		return m.Username | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *StaticUserConfig) GetPassword() string { | ||||
| 	if m != nil { | ||||
| 		return m.Password | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *StaticUserConfig) GetCreate() bool { | ||||
| 	if m != nil { | ||||
| 		return m.Create | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| type SetCredentialsRequest struct { | ||||
| 	Statements           *Statements       `protobuf:"bytes,1,opt,name=statements,proto3" json:"statements,omitempty"` | ||||
| 	StaticUserConfig     *StaticUserConfig `protobuf:"bytes,2,opt,name=static_user_config,json=staticUserConfig,proto3" json:"static_user_config,omitempty"` | ||||
| 	XXX_NoUnkeyedLiteral struct{}          `json:"-"` | ||||
| 	XXX_unrecognized     []byte            `json:"-"` | ||||
| 	XXX_sizecache        int32             `json:"-"` | ||||
| } | ||||
|  | ||||
| func (m *SetCredentialsRequest) Reset()         { *m = SetCredentialsRequest{} } | ||||
| func (m *SetCredentialsRequest) String() string { return proto.CompactTextString(m) } | ||||
| func (*SetCredentialsRequest) ProtoMessage()    {} | ||||
| func (*SetCredentialsRequest) Descriptor() ([]byte, []int) { | ||||
| 	return fileDescriptor_cfa445f4444c6876, []int{15} | ||||
| } | ||||
|  | ||||
| func (m *SetCredentialsRequest) XXX_Unmarshal(b []byte) error { | ||||
| 	return xxx_messageInfo_SetCredentialsRequest.Unmarshal(m, b) | ||||
| } | ||||
| func (m *SetCredentialsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | ||||
| 	return xxx_messageInfo_SetCredentialsRequest.Marshal(b, m, deterministic) | ||||
| } | ||||
| func (m *SetCredentialsRequest) XXX_Merge(src proto.Message) { | ||||
| 	xxx_messageInfo_SetCredentialsRequest.Merge(m, src) | ||||
| } | ||||
| func (m *SetCredentialsRequest) XXX_Size() int { | ||||
| 	return xxx_messageInfo_SetCredentialsRequest.Size(m) | ||||
| } | ||||
| func (m *SetCredentialsRequest) XXX_DiscardUnknown() { | ||||
| 	xxx_messageInfo_SetCredentialsRequest.DiscardUnknown(m) | ||||
| } | ||||
|  | ||||
| var xxx_messageInfo_SetCredentialsRequest proto.InternalMessageInfo | ||||
|  | ||||
| func (m *SetCredentialsRequest) GetStatements() *Statements { | ||||
| 	if m != nil { | ||||
| 		return m.Statements | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *SetCredentialsRequest) GetStaticUserConfig() *StaticUserConfig { | ||||
| 	if m != nil { | ||||
| 		return m.StaticUserConfig | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type SetCredentialsResponse struct { | ||||
| 	Username             string   `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` | ||||
| 	Password             string   `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` | ||||
| 	XXX_NoUnkeyedLiteral struct{} `json:"-"` | ||||
| 	XXX_unrecognized     []byte   `json:"-"` | ||||
| 	XXX_sizecache        int32    `json:"-"` | ||||
| } | ||||
|  | ||||
| func (m *SetCredentialsResponse) Reset()         { *m = SetCredentialsResponse{} } | ||||
| func (m *SetCredentialsResponse) String() string { return proto.CompactTextString(m) } | ||||
| func (*SetCredentialsResponse) ProtoMessage()    {} | ||||
| func (*SetCredentialsResponse) Descriptor() ([]byte, []int) { | ||||
| 	return fileDescriptor_cfa445f4444c6876, []int{16} | ||||
| } | ||||
|  | ||||
| func (m *SetCredentialsResponse) XXX_Unmarshal(b []byte) error { | ||||
| 	return xxx_messageInfo_SetCredentialsResponse.Unmarshal(m, b) | ||||
| } | ||||
| func (m *SetCredentialsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | ||||
| 	return xxx_messageInfo_SetCredentialsResponse.Marshal(b, m, deterministic) | ||||
| } | ||||
| func (m *SetCredentialsResponse) XXX_Merge(src proto.Message) { | ||||
| 	xxx_messageInfo_SetCredentialsResponse.Merge(m, src) | ||||
| } | ||||
| func (m *SetCredentialsResponse) XXX_Size() int { | ||||
| 	return xxx_messageInfo_SetCredentialsResponse.Size(m) | ||||
| } | ||||
| func (m *SetCredentialsResponse) XXX_DiscardUnknown() { | ||||
| 	xxx_messageInfo_SetCredentialsResponse.DiscardUnknown(m) | ||||
| } | ||||
|  | ||||
| var xxx_messageInfo_SetCredentialsResponse proto.InternalMessageInfo | ||||
|  | ||||
| func (m *SetCredentialsResponse) GetUsername() string { | ||||
| 	if m != nil { | ||||
| 		return m.Username | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *SetCredentialsResponse) GetPassword() string { | ||||
| 	if m != nil { | ||||
| 		return m.Password | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	proto.RegisterType((*InitializeRequest)(nil), "dbplugin.InitializeRequest") | ||||
| 	proto.RegisterType((*InitRequest)(nil), "dbplugin.InitRequest") | ||||
| @@ -673,6 +871,10 @@ func init() { | ||||
| 	proto.RegisterType((*TypeResponse)(nil), "dbplugin.TypeResponse") | ||||
| 	proto.RegisterType((*RotateRootCredentialsResponse)(nil), "dbplugin.RotateRootCredentialsResponse") | ||||
| 	proto.RegisterType((*Empty)(nil), "dbplugin.Empty") | ||||
| 	proto.RegisterType((*GenerateCredentialsResponse)(nil), "dbplugin.GenerateCredentialsResponse") | ||||
| 	proto.RegisterType((*StaticUserConfig)(nil), "dbplugin.StaticUserConfig") | ||||
| 	proto.RegisterType((*SetCredentialsRequest)(nil), "dbplugin.SetCredentialsRequest") | ||||
| 	proto.RegisterType((*SetCredentialsResponse)(nil), "dbplugin.SetCredentialsResponse") | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| @@ -680,52 +882,60 @@ func init() { | ||||
| } | ||||
|  | ||||
| var fileDescriptor_cfa445f4444c6876 = []byte{ | ||||
| 	// 716 bytes of a gzipped FileDescriptorProto | ||||
| 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xd1, 0x4e, 0xdb, 0x4a, | ||||
| 	0x10, 0x95, 0x93, 0x00, 0xc9, 0x80, 0x80, 0xec, 0x05, 0x64, 0xf9, 0x72, 0xef, 0x45, 0xd6, 0x15, | ||||
| 	0xa5, 0xaa, 0x6a, 0x57, 0xd0, 0x8a, 0x8a, 0x87, 0x56, 0x25, 0x54, 0x55, 0xa5, 0x8a, 0x87, 0x05, | ||||
| 	0x5e, 0xaa, 0x4a, 0x68, 0xe3, 0x2c, 0x89, 0x85, 0xe3, 0x75, 0xbd, 0xeb, 0xd0, 0xf4, 0x07, 0xda, | ||||
| 	0xcf, 0xe8, 0xe7, 0xf4, 0xb1, 0x9f, 0x54, 0x79, 0xe3, 0xf5, 0x6e, 0xe2, 0x50, 0x1e, 0x68, 0xdf, | ||||
| 	0x3c, 0x3b, 0x73, 0x66, 0xce, 0x1c, 0xcf, 0xce, 0xc2, 0xff, 0xbc, 0x77, 0xed, 0xf7, 0x88, 0x20, | ||||
| 	0x5d, 0xc2, 0xa9, 0xdf, 0xeb, 0x26, 0x51, 0xd6, 0x0f, 0xe3, 0xf2, 0xc4, 0x4b, 0x52, 0x26, 0x18, | ||||
| 	0x6a, 0x2a, 0x87, 0xf3, 0x5f, 0x9f, 0xb1, 0x7e, 0x44, 0x7d, 0x79, 0xde, 0xcd, 0xae, 0x7c, 0x11, | ||||
| 	0x0e, 0x29, 0x17, 0x64, 0x98, 0x4c, 0x42, 0xdd, 0x0f, 0xd0, 0x7e, 0x1b, 0x87, 0x22, 0x24, 0x51, | ||||
| 	0xf8, 0x99, 0x62, 0xfa, 0x31, 0xa3, 0x5c, 0xa0, 0x2d, 0x58, 0x0c, 0x58, 0x7c, 0x15, 0xf6, 0x6d, | ||||
| 	0x6b, 0xc7, 0xda, 0x5b, 0xc1, 0x85, 0x85, 0x1e, 0x41, 0x7b, 0x44, 0xd3, 0xf0, 0x6a, 0x7c, 0x19, | ||||
| 	0xb0, 0x38, 0xa6, 0x81, 0x08, 0x59, 0x6c, 0xd7, 0x76, 0xac, 0xbd, 0x26, 0x5e, 0x9f, 0x38, 0x3a, | ||||
| 	0xe5, 0xf9, 0x51, 0xcd, 0xb6, 0x5c, 0x0c, 0xcb, 0x79, 0xf6, 0xdf, 0x99, 0xd7, 0xfd, 0x6e, 0x41, | ||||
| 	0xbb, 0x93, 0x52, 0x22, 0xe8, 0x05, 0xa7, 0xa9, 0x4a, 0xfd, 0x14, 0x80, 0x0b, 0x22, 0xe8, 0x90, | ||||
| 	0xc6, 0x82, 0xcb, 0xf4, 0xcb, 0xfb, 0x1b, 0x9e, 0xd2, 0xc1, 0x3b, 0x2b, 0x7d, 0xd8, 0x88, 0x43, | ||||
| 	0xaf, 0x60, 0x2d, 0xe3, 0x34, 0x8d, 0xc9, 0x90, 0x5e, 0x16, 0xcc, 0x6a, 0x12, 0x6a, 0x6b, 0xe8, | ||||
| 	0x45, 0x11, 0xd0, 0x91, 0x7e, 0xbc, 0x9a, 0x4d, 0xd9, 0xe8, 0x08, 0x80, 0x7e, 0x4a, 0xc2, 0x94, | ||||
| 	0x48, 0xd2, 0x75, 0x89, 0x76, 0xbc, 0x89, 0xec, 0x9e, 0x92, 0xdd, 0x3b, 0x57, 0xb2, 0x63, 0x23, | ||||
| 	0xda, 0xfd, 0x66, 0xc1, 0x3a, 0xa6, 0x31, 0xbd, 0xb9, 0x7f, 0x27, 0x0e, 0x34, 0x15, 0x31, 0xd9, | ||||
| 	0x42, 0x0b, 0x97, 0xf6, 0xbd, 0x28, 0x52, 0x68, 0x63, 0x3a, 0x62, 0xd7, 0xf4, 0x8f, 0x52, 0x74, | ||||
| 	0x5f, 0xc0, 0x36, 0x66, 0x79, 0x28, 0x66, 0x4c, 0x74, 0x52, 0xda, 0xa3, 0x71, 0x3e, 0x93, 0x5c, | ||||
| 	0x55, 0xfc, 0x77, 0xa6, 0x62, 0x7d, 0xaf, 0x65, 0xe6, 0x76, 0x7f, 0xd4, 0x00, 0x74, 0x59, 0x74, | ||||
| 	0x00, 0x7f, 0x05, 0xf9, 0x88, 0x84, 0x2c, 0xbe, 0x9c, 0x61, 0xda, 0x3a, 0xae, 0xd9, 0x16, 0x46, | ||||
| 	0xca, 0x6d, 0x80, 0x0e, 0x61, 0x33, 0xa5, 0x23, 0x16, 0x54, 0x60, 0xb5, 0x12, 0xb6, 0xa1, 0x03, | ||||
| 	0xa6, 0xab, 0xa5, 0x2c, 0x8a, 0xba, 0x24, 0xb8, 0x36, 0x61, 0x75, 0x5d, 0x4d, 0xb9, 0x0d, 0xd0, | ||||
| 	0x63, 0x58, 0x4f, 0xf3, 0x5f, 0x6f, 0x22, 0x1a, 0x25, 0x62, 0x4d, 0xfa, 0xce, 0xa6, 0xc4, 0x53, | ||||
| 	0x94, 0xed, 0x05, 0xd9, 0x7e, 0x69, 0xe7, 0xe2, 0x68, 0x5e, 0xf6, 0xe2, 0x44, 0x1c, 0x7d, 0x92, | ||||
| 	0x63, 0x15, 0x01, 0x7b, 0x69, 0x82, 0x55, 0x36, 0xb2, 0x61, 0x49, 0x96, 0x22, 0x91, 0xdd, 0x94, | ||||
| 	0x2e, 0x65, 0xba, 0xa7, 0xb0, 0x3a, 0x3d, 0xfa, 0x68, 0x07, 0x96, 0x4f, 0x42, 0x9e, 0x44, 0x64, | ||||
| 	0x7c, 0x9a, 0xff, 0x43, 0xa9, 0x26, 0x36, 0x8f, 0xf2, 0x4a, 0x98, 0x45, 0xf4, 0xd4, 0xf8, 0xc5, | ||||
| 	0xca, 0x76, 0x77, 0x61, 0x65, 0xb2, 0x0b, 0x78, 0xc2, 0x62, 0x4e, 0x6f, 0x5b, 0x06, 0xee, 0x3b, | ||||
| 	0x40, 0xe6, 0xf5, 0x2e, 0xa2, 0xcd, 0xe1, 0xb1, 0x66, 0xe6, 0xdb, 0x81, 0x66, 0x42, 0x38, 0xbf, | ||||
| 	0x61, 0x69, 0x4f, 0x55, 0x55, 0xb6, 0xeb, 0xc2, 0xca, 0xf9, 0x38, 0xa1, 0x65, 0x1e, 0x04, 0x0d, | ||||
| 	0x31, 0x4e, 0x54, 0x0e, 0xf9, 0xed, 0x1e, 0xc2, 0x3f, 0xb7, 0x0c, 0xdf, 0x1d, 0x54, 0x97, 0x60, | ||||
| 	0xe1, 0xf5, 0x30, 0x11, 0xe3, 0xfd, 0x2f, 0x0d, 0x68, 0x9e, 0x14, 0x3b, 0x18, 0xf9, 0xd0, 0xc8, | ||||
| 	0x4b, 0xa2, 0x35, 0x7d, 0x23, 0x64, 0x94, 0xb3, 0xa5, 0x0f, 0xa6, 0x38, 0xbd, 0x01, 0xd0, 0x1d, | ||||
| 	0xa3, 0xbf, 0x75, 0x54, 0x65, 0xcd, 0x39, 0xdb, 0xf3, 0x9d, 0x45, 0xa2, 0xe7, 0xd0, 0x2a, 0xd7, | ||||
| 	0x09, 0x72, 0x74, 0xe8, 0xec, 0x8e, 0x71, 0x66, 0xa9, 0xe5, 0x2b, 0x42, 0x5f, 0x73, 0x93, 0x42, | ||||
| 	0xe5, 0xf2, 0x57, 0xb1, 0x03, 0xd8, 0x9c, 0x2b, 0x1f, 0xda, 0x35, 0xd2, 0xfc, 0xe2, 0x72, 0x3b, | ||||
| 	0x0f, 0xee, 0x8c, 0x2b, 0xfa, 0x7b, 0x06, 0x8d, 0x7c, 0x84, 0xd0, 0xa6, 0x06, 0x18, 0xcf, 0x8b, | ||||
| 	0xa9, 0xef, 0xd4, 0xa4, 0x3d, 0x84, 0x85, 0x4e, 0xc4, 0xf8, 0x9c, 0x3f, 0x52, 0xe9, 0xe5, 0x25, | ||||
| 	0x80, 0x7e, 0x0e, 0x4d, 0x1d, 0x2a, 0x8f, 0x64, 0x05, 0xeb, 0xd6, 0xbf, 0xd6, 0xac, 0xe3, 0xfd, | ||||
| 	0xf7, 0x4f, 0xfa, 0xa1, 0x18, 0x64, 0x5d, 0x2f, 0x60, 0x43, 0x7f, 0x40, 0xf8, 0x20, 0x0c, 0x58, | ||||
| 	0x9a, 0xf8, 0x23, 0x92, 0x45, 0xc2, 0x9f, 0xfb, 0x7a, 0x77, 0x17, 0xe5, 0x0e, 0x3e, 0xf8, 0x19, | ||||
| 	0x00, 0x00, 0xff, 0xff, 0xdb, 0x96, 0x8b, 0x5c, 0xdd, 0x07, 0x00, 0x00, | ||||
| 	// 839 bytes of a gzipped FileDescriptorProto | ||||
| 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x56, 0xdd, 0x8e, 0xdb, 0x44, | ||||
| 	0x14, 0x96, 0xf3, 0xb3, 0x9b, 0x9c, 0x5d, 0xed, 0x26, 0xd3, 0x66, 0x65, 0xb9, 0x85, 0x46, 0x23, | ||||
| 	0x28, 0x8b, 0x10, 0x31, 0xda, 0x82, 0x0a, 0xbd, 0x00, 0xd1, 0x14, 0x15, 0x24, 0x58, 0xa1, 0x49, | ||||
| 	0x7b, 0x83, 0x90, 0xa2, 0x89, 0x33, 0x9b, 0x58, 0xeb, 0x78, 0x8c, 0x67, 0x92, 0x12, 0x9e, 0x80, | ||||
| 	0x37, 0xe0, 0x96, 0x7b, 0x5e, 0x84, 0x87, 0xe1, 0x21, 0x90, 0xc7, 0x1e, 0x7b, 0xfc, 0xb3, 0xad, | ||||
| 	0xd4, 0x85, 0x3b, 0x9f, 0x39, 0xe7, 0x3b, 0xf3, 0x9d, 0x5f, 0x0f, 0xbc, 0x27, 0x96, 0xd7, 0xee, | ||||
| 	0x92, 0x4a, 0xba, 0xa0, 0x82, 0xb9, 0xcb, 0x45, 0x14, 0x6c, 0x57, 0x7e, 0x98, 0x9f, 0x4c, 0xa2, | ||||
| 	0x98, 0x4b, 0x8e, 0x7a, 0x5a, 0xe1, 0x3c, 0x58, 0x71, 0xbe, 0x0a, 0x98, 0xab, 0xce, 0x17, 0xdb, | ||||
| 	0x2b, 0x57, 0xfa, 0x1b, 0x26, 0x24, 0xdd, 0x44, 0xa9, 0x29, 0xfe, 0x19, 0x86, 0xdf, 0x85, 0xbe, | ||||
| 	0xf4, 0x69, 0xe0, 0xff, 0xc6, 0x08, 0xfb, 0x65, 0xcb, 0x84, 0x44, 0x67, 0x70, 0xe0, 0xf1, 0xf0, | ||||
| 	0xca, 0x5f, 0xd9, 0xd6, 0xd8, 0x3a, 0x3f, 0x26, 0x99, 0x84, 0x3e, 0x82, 0xe1, 0x8e, 0xc5, 0xfe, | ||||
| 	0xd5, 0x7e, 0xee, 0xf1, 0x30, 0x64, 0x9e, 0xf4, 0x79, 0x68, 0xb7, 0xc6, 0xd6, 0x79, 0x8f, 0x0c, | ||||
| 	0x52, 0xc5, 0x34, 0x3f, 0x7f, 0xd2, 0xb2, 0x2d, 0x4c, 0xe0, 0x28, 0xf1, 0xfe, 0x5f, 0xfa, 0xc5, | ||||
| 	0x7f, 0x5b, 0x30, 0x9c, 0xc6, 0x8c, 0x4a, 0xf6, 0x52, 0xb0, 0x58, 0xbb, 0xfe, 0x14, 0x40, 0x48, | ||||
| 	0x2a, 0xd9, 0x86, 0x85, 0x52, 0x28, 0xf7, 0x47, 0x17, 0x77, 0x27, 0x3a, 0x0f, 0x93, 0x59, 0xae, | ||||
| 	0x23, 0x86, 0x1d, 0xfa, 0x1a, 0x4e, 0xb7, 0x82, 0xc5, 0x21, 0xdd, 0xb0, 0x79, 0xc6, 0xac, 0xa5, | ||||
| 	0xa0, 0x76, 0x01, 0x7d, 0x99, 0x19, 0x4c, 0x95, 0x9e, 0x9c, 0x6c, 0x4b, 0x32, 0x7a, 0x02, 0xc0, | ||||
| 	0x7e, 0x8d, 0xfc, 0x98, 0x2a, 0xd2, 0x6d, 0x85, 0x76, 0x26, 0x69, 0xda, 0x27, 0x3a, 0xed, 0x93, | ||||
| 	0x17, 0x3a, 0xed, 0xc4, 0xb0, 0xc6, 0x7f, 0x5a, 0x30, 0x20, 0x2c, 0x64, 0xaf, 0x6e, 0x1f, 0x89, | ||||
| 	0x03, 0x3d, 0x4d, 0x4c, 0x85, 0xd0, 0x27, 0xb9, 0x7c, 0x2b, 0x8a, 0x0c, 0x86, 0x84, 0xed, 0xf8, | ||||
| 	0x35, 0xfb, 0x5f, 0x29, 0xe2, 0x2f, 0xe1, 0x3e, 0xe1, 0x89, 0x29, 0xe1, 0x5c, 0x4e, 0x63, 0xb6, | ||||
| 	0x64, 0x61, 0xd2, 0x93, 0x42, 0xdf, 0xf8, 0x6e, 0xe5, 0xc6, 0xf6, 0x79, 0xdf, 0xf4, 0x8d, 0xff, | ||||
| 	0x69, 0x01, 0x14, 0xd7, 0xa2, 0x47, 0x70, 0xc7, 0x4b, 0x5a, 0xc4, 0xe7, 0xe1, 0xbc, 0xc2, 0xb4, | ||||
| 	0xff, 0xb4, 0x65, 0x5b, 0x04, 0x69, 0xb5, 0x01, 0x7a, 0x0c, 0xa3, 0x98, 0xed, 0xb8, 0x57, 0x83, | ||||
| 	0xb5, 0x72, 0xd8, 0xdd, 0xc2, 0xa0, 0x7c, 0x5b, 0xcc, 0x83, 0x60, 0x41, 0xbd, 0x6b, 0x13, 0xd6, | ||||
| 	0x2e, 0x6e, 0xd3, 0x6a, 0x03, 0xf4, 0x31, 0x0c, 0xe2, 0xa4, 0xf4, 0x26, 0xa2, 0x93, 0x23, 0x4e, | ||||
| 	0x95, 0x6e, 0x56, 0x4a, 0x9e, 0xa6, 0x6c, 0x77, 0x55, 0xf8, 0xb9, 0x9c, 0x24, 0xa7, 0xe0, 0x65, | ||||
| 	0x1f, 0xa4, 0xc9, 0x29, 0x4e, 0x12, 0xac, 0x26, 0x60, 0x1f, 0xa6, 0x58, 0x2d, 0x23, 0x1b, 0x0e, | ||||
| 	0xd5, 0x55, 0x34, 0xb0, 0x7b, 0x4a, 0xa5, 0xc5, 0x14, 0x25, 0x53, 0x9f, 0x7d, 0x8d, 0x4a, 0x65, | ||||
| 	0x7c, 0x09, 0x27, 0xe5, 0xb1, 0x40, 0x63, 0x38, 0x7a, 0xe6, 0x8b, 0x28, 0xa0, 0xfb, 0xcb, 0xa4, | ||||
| 	0xbe, 0x2a, 0xd3, 0xc4, 0x3c, 0x4a, 0xfc, 0x11, 0x1e, 0xb0, 0x4b, 0xa3, 0xfc, 0x5a, 0xc6, 0x0f, | ||||
| 	0xe1, 0x38, 0xdd, 0x13, 0x22, 0xe2, 0xa1, 0x60, 0x37, 0x2d, 0x0a, 0xfc, 0x3d, 0x20, 0x73, 0xf4, | ||||
| 	0x33, 0x6b, 0xb3, 0xb1, 0xac, 0x4a, 0xef, 0x3b, 0xd0, 0x8b, 0xa8, 0x10, 0xaf, 0x78, 0xbc, 0xd4, | ||||
| 	0xb7, 0x6a, 0x19, 0x63, 0x38, 0x7e, 0xb1, 0x8f, 0x58, 0xee, 0x07, 0x41, 0x47, 0xee, 0x23, 0xed, | ||||
| 	0x43, 0x7d, 0xe3, 0xc7, 0xf0, 0xce, 0x0d, 0x8d, 0xf9, 0x06, 0xaa, 0x87, 0xd0, 0xfd, 0x66, 0x13, | ||||
| 	0xc9, 0x3d, 0xfe, 0x02, 0xee, 0x3d, 0x67, 0x21, 0x8b, 0xa9, 0x64, 0x4d, 0x78, 0x93, 0xa0, 0x55, | ||||
| 	0x21, 0xb8, 0x80, 0x41, 0xd2, 0x02, 0xbe, 0x97, 0x84, 0x9b, 0x25, 0xfa, 0x2d, 0x83, 0x55, 0x3c, | ||||
| 	0x55, 0xea, 0x54, 0x5f, 0xf6, 0x48, 0x26, 0xe1, 0x3f, 0x2c, 0x18, 0xcd, 0x58, 0xd3, 0xcc, 0xbd, | ||||
| 	0xdd, 0x94, 0x7f, 0x0b, 0x48, 0x28, 0xce, 0xf3, 0x84, 0x56, 0x79, 0xab, 0x3a, 0x65, 0xb4, 0x19, | ||||
| 	0x17, 0x19, 0x88, 0xca, 0x09, 0xfe, 0x11, 0xce, 0xaa, 0xc4, 0x6e, 0x57, 0xf0, 0x8b, 0xbf, 0xba, | ||||
| 	0xd0, 0x7b, 0x96, 0xfd, 0x2a, 0x91, 0x0b, 0x9d, 0xa4, 0xfa, 0xe8, 0xb4, 0x20, 0xa5, 0x0a, 0xe6, | ||||
| 	0x9c, 0x15, 0x07, 0xa5, 0xf6, 0x78, 0x0e, 0x50, 0x34, 0x1f, 0xba, 0x57, 0x58, 0xd5, 0xfe, 0x46, | ||||
| 	0xce, 0xfd, 0x66, 0x65, 0xe6, 0xe8, 0x73, 0xe8, 0xe7, 0x5b, 0x1f, 0x19, 0x39, 0xa9, 0xfe, 0x0a, | ||||
| 	0x9c, 0x2a, 0xb5, 0x64, 0x93, 0x17, 0xdb, 0xd8, 0xa4, 0x50, 0xdb, 0xd1, 0x75, 0xec, 0x1a, 0x46, | ||||
| 	0x8d, 0x9d, 0x8c, 0x1e, 0x1a, 0x6e, 0x5e, 0xb3, 0x83, 0x9d, 0x0f, 0xde, 0x68, 0x97, 0xc5, 0xf7, | ||||
| 	0x19, 0x74, 0x92, 0x69, 0x46, 0xa3, 0x02, 0x60, 0xbc, 0x02, 0xcc, 0xfc, 0x96, 0x86, 0xfe, 0x43, | ||||
| 	0xe8, 0x4e, 0x03, 0x2e, 0x1a, 0x2a, 0x52, 0x8b, 0x65, 0x06, 0x27, 0xe5, 0xd6, 0x40, 0x0f, 0x8c, | ||||
| 	0xd6, 0x6a, 0xea, 0x66, 0x67, 0x7c, 0xb3, 0x41, 0x76, 0xff, 0x0f, 0x70, 0xa7, 0x61, 0x50, 0xeb, | ||||
| 	0x6c, 0xde, 0x2f, 0x0e, 0x5e, 0x37, 0xd8, 0x5f, 0x01, 0x14, 0x2f, 0x2b, 0xb3, 0x56, 0xb5, 0xf7, | ||||
| 	0x56, 0x2d, 0x3e, 0xdc, 0xfe, 0xbd, 0x65, 0x3d, 0xbd, 0xf8, 0xe9, 0x93, 0x95, 0x2f, 0xd7, 0xdb, | ||||
| 	0xc5, 0xc4, 0xe3, 0x1b, 0x77, 0x4d, 0xc5, 0xda, 0xf7, 0x78, 0x1c, 0xb9, 0x3b, 0xba, 0x0d, 0xa4, | ||||
| 	0xdb, 0xf8, 0x10, 0x5c, 0x1c, 0xa8, 0xdf, 0xf9, 0xa3, 0x7f, 0x03, 0x00, 0x00, 0xff, 0xff, 0xf7, | ||||
| 	0xf5, 0x87, 0x73, 0x28, 0x0a, 0x00, 0x00, | ||||
| } | ||||
|  | ||||
| // Reference imports to suppress errors if they are not otherwise used. | ||||
| @@ -747,6 +957,8 @@ type DatabaseClient interface { | ||||
| 	RotateRootCredentials(ctx context.Context, in *RotateRootCredentialsRequest, opts ...grpc.CallOption) (*RotateRootCredentialsResponse, error) | ||||
| 	Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error) | ||||
| 	Close(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) | ||||
| 	SetCredentials(ctx context.Context, in *SetCredentialsRequest, opts ...grpc.CallOption) (*SetCredentialsResponse, error) | ||||
| 	GenerateCredentials(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GenerateCredentialsResponse, error) | ||||
| 	Initialize(ctx context.Context, in *InitializeRequest, opts ...grpc.CallOption) (*Empty, error) | ||||
| } | ||||
|  | ||||
| @@ -821,6 +1033,24 @@ func (c *databaseClient) Close(ctx context.Context, in *Empty, opts ...grpc.Call | ||||
| 	return out, nil | ||||
| } | ||||
|  | ||||
| func (c *databaseClient) SetCredentials(ctx context.Context, in *SetCredentialsRequest, opts ...grpc.CallOption) (*SetCredentialsResponse, error) { | ||||
| 	out := new(SetCredentialsResponse) | ||||
| 	err := c.cc.Invoke(ctx, "/dbplugin.Database/SetCredentials", in, out, opts...) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return out, nil | ||||
| } | ||||
|  | ||||
| func (c *databaseClient) GenerateCredentials(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GenerateCredentialsResponse, error) { | ||||
| 	out := new(GenerateCredentialsResponse) | ||||
| 	err := c.cc.Invoke(ctx, "/dbplugin.Database/GenerateCredentials", in, out, opts...) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return out, nil | ||||
| } | ||||
|  | ||||
| // Deprecated: Do not use. | ||||
| func (c *databaseClient) Initialize(ctx context.Context, in *InitializeRequest, opts ...grpc.CallOption) (*Empty, error) { | ||||
| 	out := new(Empty) | ||||
| @@ -840,9 +1070,46 @@ type DatabaseServer interface { | ||||
| 	RotateRootCredentials(context.Context, *RotateRootCredentialsRequest) (*RotateRootCredentialsResponse, error) | ||||
| 	Init(context.Context, *InitRequest) (*InitResponse, error) | ||||
| 	Close(context.Context, *Empty) (*Empty, error) | ||||
| 	SetCredentials(context.Context, *SetCredentialsRequest) (*SetCredentialsResponse, error) | ||||
| 	GenerateCredentials(context.Context, *Empty) (*GenerateCredentialsResponse, error) | ||||
| 	Initialize(context.Context, *InitializeRequest) (*Empty, error) | ||||
| } | ||||
|  | ||||
| // UnimplementedDatabaseServer can be embedded to have forward compatible implementations. | ||||
| type UnimplementedDatabaseServer struct { | ||||
| } | ||||
|  | ||||
| func (*UnimplementedDatabaseServer) Type(ctx context.Context, req *Empty) (*TypeResponse, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Type not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) RenewUser(ctx context.Context, req *RenewUserRequest) (*Empty, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method RenewUser not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) RevokeUser(ctx context.Context, req *RevokeUserRequest) (*Empty, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method RevokeUser not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) RotateRootCredentials(ctx context.Context, req *RotateRootCredentialsRequest) (*RotateRootCredentialsResponse, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method RotateRootCredentials not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) Init(ctx context.Context, req *InitRequest) (*InitResponse, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Init not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) Close(ctx context.Context, req *Empty) (*Empty, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Close not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) SetCredentials(ctx context.Context, req *SetCredentialsRequest) (*SetCredentialsResponse, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method SetCredentials not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) GenerateCredentials(ctx context.Context, req *Empty) (*GenerateCredentialsResponse, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method GenerateCredentials not implemented") | ||||
| } | ||||
| func (*UnimplementedDatabaseServer) Initialize(ctx context.Context, req *InitializeRequest) (*Empty, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Initialize not implemented") | ||||
| } | ||||
|  | ||||
| func RegisterDatabaseServer(s *grpc.Server, srv DatabaseServer) { | ||||
| 	s.RegisterService(&_Database_serviceDesc, srv) | ||||
| } | ||||
| @@ -973,6 +1240,42 @@ func _Database_Close_Handler(srv interface{}, ctx context.Context, dec func(inte | ||||
| 	return interceptor(ctx, in, info, handler) | ||||
| } | ||||
|  | ||||
| func _Database_SetCredentials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | ||||
| 	in := new(SetCredentialsRequest) | ||||
| 	if err := dec(in); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if interceptor == nil { | ||||
| 		return srv.(DatabaseServer).SetCredentials(ctx, in) | ||||
| 	} | ||||
| 	info := &grpc.UnaryServerInfo{ | ||||
| 		Server:     srv, | ||||
| 		FullMethod: "/dbplugin.Database/SetCredentials", | ||||
| 	} | ||||
| 	handler := func(ctx context.Context, req interface{}) (interface{}, error) { | ||||
| 		return srv.(DatabaseServer).SetCredentials(ctx, req.(*SetCredentialsRequest)) | ||||
| 	} | ||||
| 	return interceptor(ctx, in, info, handler) | ||||
| } | ||||
|  | ||||
| func _Database_GenerateCredentials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | ||||
| 	in := new(Empty) | ||||
| 	if err := dec(in); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if interceptor == nil { | ||||
| 		return srv.(DatabaseServer).GenerateCredentials(ctx, in) | ||||
| 	} | ||||
| 	info := &grpc.UnaryServerInfo{ | ||||
| 		Server:     srv, | ||||
| 		FullMethod: "/dbplugin.Database/GenerateCredentials", | ||||
| 	} | ||||
| 	handler := func(ctx context.Context, req interface{}) (interface{}, error) { | ||||
| 		return srv.(DatabaseServer).GenerateCredentials(ctx, req.(*Empty)) | ||||
| 	} | ||||
| 	return interceptor(ctx, in, info, handler) | ||||
| } | ||||
|  | ||||
| func _Database_Initialize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | ||||
| 	in := new(InitializeRequest) | ||||
| 	if err := dec(in); err != nil { | ||||
| @@ -1023,6 +1326,14 @@ var _Database_serviceDesc = grpc.ServiceDesc{ | ||||
| 			MethodName: "Close", | ||||
| 			Handler:    _Database_Close_Handler, | ||||
| 		}, | ||||
| 		{ | ||||
| 			MethodName: "SetCredentials", | ||||
| 			Handler:    _Database_SetCredentials_Handler, | ||||
| 		}, | ||||
| 		{ | ||||
| 			MethodName: "GenerateCredentials", | ||||
| 			Handler:    _Database_GenerateCredentials_Handler, | ||||
| 		}, | ||||
| 		{ | ||||
| 			MethodName: "Initialize", | ||||
| 			Handler:    _Database_Initialize_Handler, | ||||
|   | ||||
| @@ -52,6 +52,7 @@ message Statements { | ||||
| 	repeated string revocation = 6; | ||||
| 	repeated string rollback	= 7; | ||||
| 	repeated string renewal = 8; | ||||
| 	repeated string rotation = 9; | ||||
| } | ||||
|  | ||||
| message UsernameConfig { | ||||
| @@ -78,6 +79,26 @@ message RotateRootCredentialsResponse { | ||||
|  | ||||
| message Empty {} | ||||
|  | ||||
| message GenerateCredentialsResponse { | ||||
| 	string password = 1; | ||||
| } | ||||
|  | ||||
| message StaticUserConfig{ | ||||
| 	string username = 1; | ||||
| 	string password = 2; | ||||
| 	bool create = 3; | ||||
| } | ||||
|  | ||||
| message SetCredentialsRequest { | ||||
| 	Statements statements = 1; | ||||
| 	StaticUserConfig static_user_config = 2; | ||||
| } | ||||
|  | ||||
| message SetCredentialsResponse { | ||||
| 	string username = 1; | ||||
| 	string password = 2; | ||||
| } | ||||
|  | ||||
| service Database { | ||||
| 	rpc Type(Empty) returns (TypeResponse); | ||||
| 	rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); | ||||
| @@ -86,6 +107,8 @@ service Database { | ||||
| 	rpc RotateRootCredentials(RotateRootCredentialsRequest) returns (RotateRootCredentialsResponse); | ||||
| 	rpc Init(InitRequest) returns (InitResponse); | ||||
| 	rpc Close(Empty) returns (Empty); | ||||
| 	rpc SetCredentials(SetCredentialsRequest) returns (SetCredentialsResponse); | ||||
| 	rpc GenerateCredentials(Empty) returns (GenerateCredentialsResponse); | ||||
| 	 | ||||
| 	rpc Initialize(InitializeRequest) returns (Empty) { | ||||
| 		option deprecated = true; | ||||
|   | ||||
| @@ -86,6 +86,24 @@ func (mw *databaseTracingMiddleware) Close() (err error) { | ||||
| 	return mw.next.Close() | ||||
| } | ||||
|  | ||||
| func (mw *databaseTracingMiddleware) GenerateCredentials(ctx context.Context) (password string, err error) { | ||||
| 	defer func(then time.Time) { | ||||
| 		mw.logger.Trace("generate credentials", "status", "finished", "err", err, "took", time.Since(then)) | ||||
| 	}(time.Now()) | ||||
|  | ||||
| 	mw.logger.Trace("generate credentials", "status", "started") | ||||
| 	return mw.next.GenerateCredentials(ctx) | ||||
| } | ||||
|  | ||||
| func (mw *databaseTracingMiddleware) SetCredentials(ctx context.Context, statements Statements, staticConfig StaticUserConfig) (username, password string, err error) { | ||||
| 	defer func(then time.Time) { | ||||
| 		mw.logger.Trace("set credentials", "status", "finished", "err", err, "took", time.Since(then)) | ||||
| 	}(time.Now()) | ||||
|  | ||||
| 	mw.logger.Trace("set credentials", "status", "started") | ||||
| 	return mw.next.SetCredentials(ctx, statements, staticConfig) | ||||
| } | ||||
|  | ||||
| // ---- Metrics Middleware Domain ---- | ||||
|  | ||||
| // databaseMetricsMiddleware wraps an implementation of Databases and on | ||||
| @@ -201,6 +219,38 @@ func (mw *databaseMetricsMiddleware) Close() (err error) { | ||||
| 	return mw.next.Close() | ||||
| } | ||||
|  | ||||
| func (mw *databaseMetricsMiddleware) GenerateCredentials(ctx context.Context) (password string, err error) { | ||||
| 	defer func(now time.Time) { | ||||
| 		metrics.MeasureSince([]string{"database", "GenerateCredentials"}, now) | ||||
| 		metrics.MeasureSince([]string{"database", mw.typeStr, "GenerateCredentials"}, now) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			metrics.IncrCounter([]string{"database", "GenerateCredentials", "error"}, 1) | ||||
| 			metrics.IncrCounter([]string{"database", mw.typeStr, "GenerateCredentials", "error"}, 1) | ||||
| 		} | ||||
| 	}(time.Now()) | ||||
|  | ||||
| 	metrics.IncrCounter([]string{"database", "GenerateCredentials"}, 1) | ||||
| 	metrics.IncrCounter([]string{"database", mw.typeStr, "GenerateCredentials"}, 1) | ||||
| 	return mw.next.GenerateCredentials(ctx) | ||||
| } | ||||
|  | ||||
| func (mw *databaseMetricsMiddleware) SetCredentials(ctx context.Context, statements Statements, staticConfig StaticUserConfig) (username, password string, err error) { | ||||
| 	defer func(now time.Time) { | ||||
| 		metrics.MeasureSince([]string{"database", "SetCredentials"}, now) | ||||
| 		metrics.MeasureSince([]string{"database", mw.typeStr, "SetCredentials"}, now) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			metrics.IncrCounter([]string{"database", "SetCredentials", "error"}, 1) | ||||
| 			metrics.IncrCounter([]string{"database", mw.typeStr, "SetCredentials", "error"}, 1) | ||||
| 		} | ||||
| 	}(time.Now()) | ||||
|  | ||||
| 	metrics.IncrCounter([]string{"database", "SetCredentials"}, 1) | ||||
| 	metrics.IncrCounter([]string{"database", mw.typeStr, "SetCredentials"}, 1) | ||||
| 	return mw.next.SetCredentials(ctx, statements, staticConfig) | ||||
| } | ||||
|  | ||||
| // ---- Error Sanitizer Middleware Domain ---- | ||||
|  | ||||
| // DatabaseErrorSanitizerMiddleware wraps an implementation of Databases and | ||||
| @@ -273,3 +323,13 @@ func (mw *DatabaseErrorSanitizerMiddleware) sanitize(err error) error { | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (mw *DatabaseErrorSanitizerMiddleware) GenerateCredentials(ctx context.Context) (password string, err error) { | ||||
| 	password, err = mw.next.GenerateCredentials(ctx) | ||||
| 	return password, mw.sanitize(err) | ||||
| } | ||||
|  | ||||
| func (mw *DatabaseErrorSanitizerMiddleware) SetCredentials(ctx context.Context, statements Statements, staticConfig StaticUserConfig) (username, password string, err error) { | ||||
| 	username, password, err = mw.next.SetCredentials(ctx, statements, staticConfig) | ||||
| 	return username, password, mw.sanitize(err) | ||||
| } | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import ( | ||||
|  | ||||
| var ( | ||||
| 	ErrPluginShutdown          = errors.New("plugin shutdown") | ||||
| 	ErrPluginStaticUnsupported = errors.New("database plugin does not support Static Accounts") | ||||
| ) | ||||
|  | ||||
| // ---- gRPC Server domain ---- | ||||
| @@ -115,6 +116,30 @@ func (s *gRPCServer) Close(_ context.Context, _ *Empty) (*Empty, error) { | ||||
| 	return &Empty{}, nil | ||||
| } | ||||
|  | ||||
| func (s *gRPCServer) GenerateCredentials(ctx context.Context, _ *Empty) (*GenerateCredentialsResponse, error) { | ||||
| 	p, err := s.impl.GenerateCredentials(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &GenerateCredentialsResponse{ | ||||
| 		Password: p, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (s *gRPCServer) SetCredentials(ctx context.Context, req *SetCredentialsRequest) (*SetCredentialsResponse, error) { | ||||
|  | ||||
| 	username, password, err := s.impl.SetCredentials(ctx, *req.Statements, *req.StaticUserConfig) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &SetCredentialsResponse{ | ||||
| 		Username: username, | ||||
| 		Password: password, | ||||
| 	}, err | ||||
| } | ||||
|  | ||||
| // ---- gRPC client domain ---- | ||||
|  | ||||
| type gRPCClient struct { | ||||
| @@ -283,3 +308,51 @@ func (c *gRPCClient) Close() error { | ||||
| 	_, err := c.client.Close(c.doneCtx, &Empty{}) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (c *gRPCClient) GenerateCredentials(ctx context.Context) (string, error) { | ||||
| 	ctx, cancel := context.WithCancel(ctx) | ||||
| 	quitCh := pluginutil.CtxCancelIfCanceled(cancel, c.doneCtx) | ||||
| 	defer close(quitCh) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	resp, err := c.client.GenerateCredentials(ctx, &Empty{}) | ||||
| 	if err != nil { | ||||
| 		grpcStatus, ok := status.FromError(err) | ||||
| 		if ok && grpcStatus.Code() == codes.Unimplemented { | ||||
| 			return "", ErrPluginStaticUnsupported | ||||
| 		} | ||||
|  | ||||
| 		if c.doneCtx.Err() != nil { | ||||
| 			return "", ErrPluginShutdown | ||||
| 		} | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return resp.Password, nil | ||||
| } | ||||
| func (c *gRPCClient) SetCredentials(ctx context.Context, statements Statements, staticUser StaticUserConfig) (username, password string, err error) { | ||||
| 	ctx, cancel := context.WithCancel(ctx) | ||||
| 	quitCh := pluginutil.CtxCancelIfCanceled(cancel, c.doneCtx) | ||||
| 	defer close(quitCh) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	resp, err := c.client.SetCredentials(ctx, &SetCredentialsRequest{ | ||||
| 		StaticUserConfig: &staticUser, | ||||
| 		Statements:       &statements, | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		// Fall back to old call if not implemented | ||||
| 		grpcStatus, ok := status.FromError(err) | ||||
| 		if ok && grpcStatus.Code() == codes.Unimplemented { | ||||
| 			return "", "", ErrPluginStaticUnsupported | ||||
| 		} | ||||
|  | ||||
| 		if c.doneCtx.Err() != nil { | ||||
| 			return "", "", ErrPluginShutdown | ||||
| 		} | ||||
| 		return "", "", err | ||||
| 	} | ||||
|  | ||||
| 	return resp.Username, resp.Password, err | ||||
| } | ||||
|   | ||||
| @@ -44,6 +44,19 @@ type Database interface { | ||||
| 	// the API. | ||||
| 	RotateRootCredentials(ctx context.Context, statements []string) (config map[string]interface{}, err error) | ||||
|  | ||||
| 	// GenerateCredentials returns a generated password for the plugin. This is | ||||
| 	// used in combination with SetCredentials to set a specific password for a | ||||
| 	// database user and preserve the password in WAL entries. | ||||
| 	GenerateCredentials(ctx context.Context) (string, error) | ||||
|  | ||||
| 	// SetCredentials uses provided information to create or set the credentials | ||||
| 	// for a database user. Unlike CreateUser, this method requires both a | ||||
| 	// username and a password given instead of generating them. This is used for | ||||
| 	// creating and setting the password of static accounts, as well as rolling | ||||
| 	// back passwords in the database in the event an updated database fails to | ||||
| 	// save in Vault's storage. | ||||
| 	SetCredentials(ctx context.Context, statements Statements, staticConfig StaticUserConfig) (username string, password string, err error) | ||||
|  | ||||
| 	// Init is called on `$ vault write database/config/:db-name`, or when you | ||||
| 	// do a creds call after Vault's been restarted. The config provided won't | ||||
| 	// hold all the keys and values provided in the API call, some will be | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/errwrap" | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/dbutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/parseutil" | ||||
| 	"github.com/mitchellh/mapstructure" | ||||
| @@ -162,3 +163,13 @@ func (c *SQLConnectionProducer) Close() error { | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // SetCredentials uses provided information to set/create a user in the | ||||
| // database. Unlike CreateUser, this method requires a username be provided and | ||||
| // uses the name given, instead of generating a name. This is used for creating | ||||
| // and setting the password of static accounts, as well as rolling back | ||||
| // passwords in the database in the event an updated database fails to save in | ||||
| // Vault's storage. | ||||
| func (c *SQLConnectionProducer) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { | ||||
| 	return "", "", dbutil.Unimplemented() | ||||
| } | ||||
|   | ||||
| @@ -6,10 +6,13 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
| 	"google.golang.org/grpc/codes" | ||||
| 	"google.golang.org/grpc/status" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrEmptyCreationStatement = errors.New("empty creation statements") | ||||
| 	ErrEmptyRotationStatement = errors.New("empty rotation statements") | ||||
| ) | ||||
|  | ||||
| // Query templates a query for us. | ||||
| @@ -50,3 +53,8 @@ func StatementCompatibilityHelper(statements dbplugin.Statements) dbplugin.State | ||||
| 	} | ||||
| 	return statements | ||||
| } | ||||
|  | ||||
| // Unimplemented returns a gRPC error with the Unimplemented code | ||||
| func Unimplemented() error { | ||||
| 	return status.Error(codes.Unimplemented, "Not yet implemented") | ||||
| } | ||||
|   | ||||
| @@ -10,6 +10,8 @@ import ( | ||||
| 	timestamp "github.com/golang/protobuf/ptypes/timestamp" | ||||
| 	logical "github.com/hashicorp/vault/sdk/logical" | ||||
| 	grpc "google.golang.org/grpc" | ||||
| 	codes "google.golang.org/grpc/codes" | ||||
| 	status "google.golang.org/grpc/status" | ||||
| 	math "math" | ||||
| ) | ||||
|  | ||||
| @@ -3036,6 +3038,32 @@ type BackendServer interface { | ||||
| 	Type(context.Context, *Empty) (*TypeReply, error) | ||||
| } | ||||
|  | ||||
| // UnimplementedBackendServer can be embedded to have forward compatible implementations. | ||||
| type UnimplementedBackendServer struct { | ||||
| } | ||||
|  | ||||
| func (*UnimplementedBackendServer) HandleRequest(ctx context.Context, req *HandleRequestArgs) (*HandleRequestReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method HandleRequest not implemented") | ||||
| } | ||||
| func (*UnimplementedBackendServer) SpecialPaths(ctx context.Context, req *Empty) (*SpecialPathsReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method SpecialPaths not implemented") | ||||
| } | ||||
| func (*UnimplementedBackendServer) HandleExistenceCheck(ctx context.Context, req *HandleExistenceCheckArgs) (*HandleExistenceCheckReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method HandleExistenceCheck not implemented") | ||||
| } | ||||
| func (*UnimplementedBackendServer) Cleanup(ctx context.Context, req *Empty) (*Empty, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Cleanup not implemented") | ||||
| } | ||||
| func (*UnimplementedBackendServer) InvalidateKey(ctx context.Context, req *InvalidateKeyArgs) (*Empty, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method InvalidateKey not implemented") | ||||
| } | ||||
| func (*UnimplementedBackendServer) Setup(ctx context.Context, req *SetupArgs) (*SetupReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Setup not implemented") | ||||
| } | ||||
| func (*UnimplementedBackendServer) Type(ctx context.Context, req *Empty) (*TypeReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Type not implemented") | ||||
| } | ||||
|  | ||||
| func RegisterBackendServer(s *grpc.Server, srv BackendServer) { | ||||
| 	s.RegisterService(&_Backend_serviceDesc, srv) | ||||
| } | ||||
| @@ -3265,6 +3293,23 @@ type StorageServer interface { | ||||
| 	Delete(context.Context, *StorageDeleteArgs) (*StorageDeleteReply, error) | ||||
| } | ||||
|  | ||||
| // UnimplementedStorageServer can be embedded to have forward compatible implementations. | ||||
| type UnimplementedStorageServer struct { | ||||
| } | ||||
|  | ||||
| func (*UnimplementedStorageServer) List(ctx context.Context, req *StorageListArgs) (*StorageListReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method List not implemented") | ||||
| } | ||||
| func (*UnimplementedStorageServer) Get(ctx context.Context, req *StorageGetArgs) (*StorageGetReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") | ||||
| } | ||||
| func (*UnimplementedStorageServer) Put(ctx context.Context, req *StoragePutArgs) (*StoragePutReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Put not implemented") | ||||
| } | ||||
| func (*UnimplementedStorageServer) Delete(ctx context.Context, req *StorageDeleteArgs) (*StorageDeleteReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") | ||||
| } | ||||
|  | ||||
| func RegisterStorageServer(s *grpc.Server, srv StorageServer) { | ||||
| 	s.RegisterService(&_Storage_serviceDesc, srv) | ||||
| } | ||||
| @@ -3555,6 +3600,44 @@ type SystemViewServer interface { | ||||
| 	PluginEnv(context.Context, *Empty) (*PluginEnvReply, error) | ||||
| } | ||||
|  | ||||
| // UnimplementedSystemViewServer can be embedded to have forward compatible implementations. | ||||
| type UnimplementedSystemViewServer struct { | ||||
| } | ||||
|  | ||||
| func (*UnimplementedSystemViewServer) DefaultLeaseTTL(ctx context.Context, req *Empty) (*TTLReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method DefaultLeaseTTL not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) MaxLeaseTTL(ctx context.Context, req *Empty) (*TTLReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method MaxLeaseTTL not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) SudoPrivilege(ctx context.Context, req *SudoPrivilegeArgs) (*SudoPrivilegeReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method SudoPrivilege not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) Tainted(ctx context.Context, req *Empty) (*TaintedReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Tainted not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) CachingDisabled(ctx context.Context, req *Empty) (*CachingDisabledReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method CachingDisabled not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) ReplicationState(ctx context.Context, req *Empty) (*ReplicationStateReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method ReplicationState not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) ResponseWrapData(ctx context.Context, req *ResponseWrapDataArgs) (*ResponseWrapDataReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method ResponseWrapData not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) MlockEnabled(ctx context.Context, req *Empty) (*MlockEnabledReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method MlockEnabled not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) LocalMount(ctx context.Context, req *Empty) (*LocalMountReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method LocalMount not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) EntityInfo(ctx context.Context, req *EntityInfoArgs) (*EntityInfoReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method EntityInfo not implemented") | ||||
| } | ||||
| func (*UnimplementedSystemViewServer) PluginEnv(ctx context.Context, req *Empty) (*PluginEnvReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method PluginEnv not implemented") | ||||
| } | ||||
|  | ||||
| func RegisterSystemViewServer(s *grpc.Server, srv SystemViewServer) { | ||||
| 	s.RegisterService(&_SystemView_serviceDesc, srv) | ||||
| } | ||||
|   | ||||
| @@ -113,7 +113,7 @@ func (pq *PriorityQueue) Push(i *Item) error { | ||||
| 	if _, ok := pq.dataMap[i.Key]; ok { | ||||
| 		return ErrDuplicateItem | ||||
| 	} | ||||
| 	// copy the item value(s) so that modifications to the source item does not | ||||
| 	// Copy the item value(s) so that modifications to the source item does not | ||||
| 	// affect the item on the queue | ||||
| 	clone, err := copystructure.Copy(i) | ||||
| 	if err != nil { | ||||
| @@ -126,8 +126,8 @@ func (pq *PriorityQueue) Push(i *Item) error { | ||||
| } | ||||
|  | ||||
| // PopByKey searches the queue for an item with the given key and removes it | ||||
| // from the queue if found. Returns ErrItemNotFound(key) if not found. This | ||||
| // method must fix the queue after removal. | ||||
| // from the queue if found. Returns nil if not found. This method must fix the | ||||
| // queue after removing any key. | ||||
| func (pq *PriorityQueue) PopByKey(key string) (*Item, error) { | ||||
| 	pq.lock.Lock() | ||||
| 	defer pq.lock.Unlock() | ||||
| @@ -137,7 +137,7 @@ func (pq *PriorityQueue) PopByKey(key string) (*Item, error) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	// remove the item the heap and delete it from the dataMap | ||||
| 	// Remove the item the heap and delete it from the dataMap | ||||
| 	itemRaw := heap.Remove(&pq.data, item.index) | ||||
| 	delete(pq.dataMap, key) | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,8 @@ import ( | ||||
| 	proto "github.com/golang/protobuf/proto" | ||||
| 	forwarding "github.com/hashicorp/vault/helper/forwarding" | ||||
| 	grpc "google.golang.org/grpc" | ||||
| 	codes "google.golang.org/grpc/codes" | ||||
| 	status "google.golang.org/grpc/status" | ||||
| 	math "math" | ||||
| ) | ||||
|  | ||||
| @@ -439,6 +441,20 @@ type RequestForwardingServer interface { | ||||
| 	PerformanceStandbyElectionRequest(*PerfStandbyElectionInput, RequestForwarding_PerformanceStandbyElectionRequestServer) error | ||||
| } | ||||
|  | ||||
| // UnimplementedRequestForwardingServer can be embedded to have forward compatible implementations. | ||||
| type UnimplementedRequestForwardingServer struct { | ||||
| } | ||||
|  | ||||
| func (*UnimplementedRequestForwardingServer) ForwardRequest(ctx context.Context, req *forwarding.Request) (*forwarding.Response, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method ForwardRequest not implemented") | ||||
| } | ||||
| func (*UnimplementedRequestForwardingServer) Echo(ctx context.Context, req *EchoRequest) (*EchoReply, error) { | ||||
| 	return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented") | ||||
| } | ||||
| func (*UnimplementedRequestForwardingServer) PerformanceStandbyElectionRequest(req *PerfStandbyElectionInput, srv RequestForwarding_PerformanceStandbyElectionRequestServer) error { | ||||
| 	return status.Errorf(codes.Unimplemented, "method PerformanceStandbyElectionRequest not implemented") | ||||
| } | ||||
|  | ||||
| func RegisterRequestForwardingServer(s *grpc.Server, srv RequestForwardingServer) { | ||||
| 	s.RegisterService(&_RequestForwarding_serviceDesc, srv) | ||||
| } | ||||
|   | ||||
| @@ -397,3 +397,233 @@ $ curl \ | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Create Static Role | ||||
|  | ||||
| This endpoint creates or updates a static role definition. Static Roles are a | ||||
| 1-to-1 mapping of a Vault Role to a user in a database which are automatically | ||||
| rotated based on the configured `rotation_period`. Not all databases support | ||||
| Static Roles, please see the database-specific documentation. | ||||
|  | ||||
| ~> This endpoint distinguishes between `create` and `update` ACL capabilities. | ||||
|  | ||||
| | Method   | Path                         | | ||||
| | :--------------------------- | :--------------------- | | ||||
| | `POST`   | `/database/static-roles/:name`      | | ||||
|  | ||||
| ### Parameters | ||||
|  | ||||
| - `name` `(string: <required>)` – Specifies the name of the role to create. This | ||||
|   is specified as part of the URL. | ||||
|  | ||||
| - `username` `(string: <required>)` – Specifies the database username that this | ||||
|   Vault role corresponds to.  | ||||
|  | ||||
| - `rotation_period` `(string/int: <required>)` – Specifies the amount of time | ||||
|   Vault should wait before rotating the password. The minimum is 5 seconds. | ||||
|  | ||||
| - `db_name` `(string: <required>)` - The name of the database connection to use | ||||
|   for this role. | ||||
|  | ||||
| - `creation_statements` `(list: <required>)` – Specifies the database | ||||
|   statements executed to create and configure a user. See the plugin's API page | ||||
|   for more information on support and formatting for this parameter. | ||||
|  | ||||
| - `revocation_statements` `(list: [])` – Specifies the database statements to | ||||
|   be executed to revoke a user. See the plugin's API page for more information | ||||
|   on support and formatting for this parameter. | ||||
|  | ||||
| - `rollback_statements` `(list: [])` – Specifies the database statements to be | ||||
|   executed rollback a create operation in the event of an error. Not every | ||||
|   plugin type will support this functionality. See the plugin's API page for | ||||
|   more information on support and formatting for this parameter. | ||||
|  | ||||
| - `renew_statements` `(list: [])` – Specifies the database statements to be | ||||
|   executed to renew a user. Not every plugin type will support this | ||||
|   functionality. See the plugin's API page for more information on support and | ||||
|   formatting for this parameter. | ||||
|  | ||||
| - `rotation_statements` `(list: [])` – Specifies the database statements to be | ||||
|   executed to rotate the password for the configured database user. Not every | ||||
|   plugin type will support this functionality. See the plugin's API page for | ||||
|   more information on support and formatting for this parameter. | ||||
|  | ||||
| - `revoke_user_on_delete` `(boolean: false)` – Specifies if Vault should attempt | ||||
|   to revoke the database user associated with this static role, indicated by the | ||||
|   `username`. If `true`, when Vault deletes this Role it will attempt to revoke | ||||
|   the database user using the configured `revocation_statements` if they exist. | ||||
|   Default `false` | ||||
|  | ||||
|  | ||||
|  | ||||
| ### Sample Payload | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "db_name": "mysql", | ||||
|     "username": "static-database-user", | ||||
|     "creation_statements": ["CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'", "GRANT SELECT ON *.* TO '{{name}}'@'%'"], | ||||
|     "rotation_statements": ["ALTER USER "{{name}}" WITH PASSWORD '{{password}}';"], | ||||
|     "rotation_period": "1h" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Sample Request | ||||
|  | ||||
| ``` | ||||
| $ curl \ | ||||
|     --header "X-Vault-Token: ..." \ | ||||
|     --request POST \ | ||||
|     --data @payload.json \ | ||||
|     http://127.0.0.1:8200/v1/database/static-roles/my-static-role | ||||
| ``` | ||||
|  | ||||
| ## Read Static Role | ||||
|  | ||||
| This endpoint queries the static role definition. | ||||
|  | ||||
| | Method   | Path                         | | ||||
| | :--------------------------- | :--------------------- | | ||||
| | `GET`    | `/database/static-roles/:name`    | | ||||
|  | ||||
| ### Parameters | ||||
|  | ||||
| - `name` `(string: <required>)` – Specifies the name of the static role to read. | ||||
|   This is specified as part of the URL. | ||||
|  | ||||
| ### Sample Request | ||||
|  | ||||
| ``` | ||||
| $ curl \ | ||||
|     --header "X-Vault-Token: ..." \ | ||||
|     http://127.0.0.1:8200/v1/database/static-roles/my-static-role | ||||
| ``` | ||||
|  | ||||
| ### Sample Response | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "data": { | ||||
| 		"db_name": "mysql", | ||||
|     "username":"static-user", | ||||
| 		"creation_statements": ["CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';"], "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"], | ||||
|     "rotation_statements": ["ALTER USER "{{name}}" WITH PASSWORD '{{password}}';"], | ||||
|     "rotation_period":"1h", | ||||
| 		"renew_statements": [], | ||||
| 		"revocation_statements": [], | ||||
| 		"rollback_statements": [] | ||||
| 		"revoke_user_on_delete": false, | ||||
| 	}, | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## List Static Roles | ||||
|  | ||||
| This endpoint returns a list of available static roles. Only the role names are | ||||
| returned, not any values. | ||||
|  | ||||
| | Method   | Path                         | | ||||
| | :--------------------------- | :--------------------- | | ||||
| | `LIST`   | `/database/static-roles`          | | ||||
|  | ||||
| ### Sample Request | ||||
|  | ||||
| ``` | ||||
| $ curl \ | ||||
|     --header "X-Vault-Token: ..." \ | ||||
|     --request LIST \ | ||||
|     http://127.0.0.1:8200/v1/database/static-roles | ||||
| ``` | ||||
|  | ||||
| ### Sample Response | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "auth": null, | ||||
|   "data": { | ||||
|     "keys": ["dev-static", "prod-static"] | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Delete Static Role | ||||
|  | ||||
| This endpoint deletes the static role definition and revokes the database user. | ||||
|  | ||||
| | Method   | Path                         | | ||||
| | :--------------------------- | :--------------------- | | ||||
| | `DELETE` | `/database/static-roles/:name`    | | ||||
|  | ||||
| ### Parameters | ||||
|  | ||||
| - `name` `(string: <required>)` – Specifies the name of the static role to | ||||
|   delete. This is specified as part of the URL. | ||||
|  | ||||
| ### Sample Request | ||||
|  | ||||
| ``` | ||||
| $ curl \ | ||||
|     --header "X-Vault-Token: ..." \ | ||||
|     --request DELETE \ | ||||
|     http://127.0.0.1:8200/v1/database/static-roles/my-role | ||||
| ``` | ||||
|  | ||||
| ## Get Static Credentials | ||||
|  | ||||
| This endpoint returns the current credentials based on the named static role. | ||||
|  | ||||
| | Method   | Path                         | | ||||
| | :--------------------------- | :--------------------- | | ||||
| | `GET`    | `/database/static-creds/:name`    | | ||||
|  | ||||
| ### Parameters | ||||
|  | ||||
| - `name` `(string: <required>)` – Specifies the name of the static role to get | ||||
|   credentials for. This is specified as part of the URL. | ||||
|  | ||||
| ### Sample Request | ||||
|  | ||||
| ``` | ||||
| $ curl \ | ||||
|     --header "X-Vault-Token: ..." \ | ||||
|     http://127.0.0.1:8200/v1/database/static-creds/my-static-role | ||||
| ``` | ||||
|  | ||||
| ### Sample Response | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "data": { | ||||
|     "username": "static-user", | ||||
|     "password": "132ae3ef-5a64-7499-351e-bfe59f3a2a21" | ||||
|     "last_vault_rotation": "2019-05-06T15:26:42.525302-05:00", | ||||
|     "rotation_period": 30, | ||||
|     "ttl": 28, | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Rotate Static Role Credentials | ||||
|  | ||||
| This endpoint is used to rotate the Static Role credentials stored for a given | ||||
| role name. While Static Roles are rotated automatically by Vault at configured | ||||
| rotation periods, users can use this endpoint to manually trigger a rotation to | ||||
| change the stored password and reset the TTL of the Static Role's password. | ||||
|  | ||||
| | Method   | Path                          | | ||||
| | :---------------------------- | :--------------------- | | ||||
| | `POST`   | `/database/rotate-role/:name` | | ||||
|  | ||||
| ### Parameters | ||||
|  | ||||
| - `name` `(string: <required>)` – Specifies the name of the Static Role to | ||||
|   trigger the password rotation for. The name is specified as part of the URL. | ||||
|  | ||||
| ### Sample Request | ||||
|  | ||||
| ``` | ||||
| $ curl \ | ||||
|     --header "X-Vault-Token: ..." \ | ||||
|     --request POST \ | ||||
|     http://127.0.0.1:8200/v1/database/rotate-role/my-static-role | ||||
| ``` | ||||
|   | ||||
| @@ -105,4 +105,10 @@ list the plugin does not support that statement type. | ||||
|   functionality. Must be a semicolon-separated string, a base64-encoded | ||||
|   semicolon-separated string, a serialized JSON string array, or a | ||||
|   base64-encoded serialized JSON string array. The '{{name}}' and | ||||
|   '{{expiration}}` values will be substituted. | ||||
|   '{{expiration}}' values will be substituted. | ||||
|  | ||||
| - `rotation_statements` `(list: [])` – Specifies the database statements to be | ||||
|   executed to rotate the password for a given username. Must be a | ||||
|   semicolon-separated string, a base64-encoded semicolon-separated string, a | ||||
|   serialized JSON string array, or a base64-encoded serialized JSON string | ||||
|   array. The '{{name}}' and '{{password}}' values will be substituted. | ||||
|   | ||||
| @@ -27,6 +27,22 @@ it down to the specific instance of a service based on the SQL username. | ||||
| Vault makes use of its own internal revocation system to ensure that users | ||||
| become invalid within a reasonable time of the lease expiring. | ||||
|  | ||||
| ### Static Roles | ||||
|  | ||||
| The database secrets engine supports the concept of "static roles", which are | ||||
| a 1-to-1 mapping of Vault Roles to usernames in a database. The current password | ||||
| for the database user is stored and automatically rotated by Vault on a | ||||
| configurable period of time. This is in contrast to dynamic secrets, where a | ||||
| unique username and password pair are generated with each credential request. | ||||
| When credentials are requested for the Role, Vault returns the current | ||||
| password for the configured database user, allowing anyone with the proper | ||||
| Vault policies to have access to the user account in the database. | ||||
|  | ||||
| Not all database types support static roles at this time. Please consult the | ||||
| specific database documentation on the left navigation to see if a given | ||||
| database backend supports static roles. | ||||
|  | ||||
|  | ||||
| ## Setup | ||||
|  | ||||
| Most secrets engines must be configured in advance before they can perform their | ||||
|   | ||||
| @@ -13,7 +13,8 @@ description: |- | ||||
|  | ||||
| PostgreSQL is one of the supported plugins for the database secrets engine. This | ||||
| plugin generates database credentials dynamically based on configured roles for | ||||
| the PostgreSQL database. | ||||
| the PostgreSQL database, and also supports [Static | ||||
| Roles](/docs/secrets/databases/index.html#static-roles). | ||||
|  | ||||
| See the [database secrets engine](/docs/secrets/databases/index.html) docs for | ||||
| more information about setting up the database secrets engine. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Clint
					Clint