mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 18:48:08 +00:00 
			
		
		
		
	 35667f93a7
			
		
	
	35667f93a7
	
	
	
		
			
			* 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
		
			
				
	
	
		
			350 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			350 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package database
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"net/rpc"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 
 | |
| 	log "github.com/hashicorp/go-hclog"
 | |
| 
 | |
| 	"github.com/hashicorp/errwrap"
 | |
| 	uuid "github.com/hashicorp/go-uuid"
 | |
| 	"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/"
 | |
| 	databaseRolePath       = "role/"
 | |
| 	databaseStaticRolePath = "static-role/"
 | |
| )
 | |
| 
 | |
| type dbPluginInstance struct {
 | |
| 	sync.RWMutex
 | |
| 	dbplugin.Database
 | |
| 
 | |
| 	id     string
 | |
| 	name   string
 | |
| 	closed bool
 | |
| }
 | |
| 
 | |
| func (dbi *dbPluginInstance) Close() error {
 | |
| 	dbi.Lock()
 | |
| 	defer dbi.Unlock()
 | |
| 
 | |
| 	if dbi.closed {
 | |
| 		return nil
 | |
| 	}
 | |
| 	dbi.closed = true
 | |
| 
 | |
| 	return dbi.Database.Close()
 | |
| }
 | |
| 
 | |
| func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
 | |
| 	b := Backend(conf)
 | |
| 	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
 | |
| }
 | |
| 
 | |
| func Backend(conf *logical.BackendConfig) *databaseBackend {
 | |
| 	var b databaseBackend
 | |
| 	b.Backend = &framework.Backend{
 | |
| 		Help: strings.TrimSpace(backendHelp),
 | |
| 
 | |
| 		PathsSpecial: &logical.Paths{
 | |
| 			LocalStorage: []string{
 | |
| 				framework.WALPrefix,
 | |
| 			},
 | |
| 			SealWrapStorage: []string{
 | |
| 				"config/*",
 | |
| 				"static-role/*",
 | |
| 			},
 | |
| 		},
 | |
| 		Paths: framework.PathAppend(
 | |
| 			[]*framework.Path{
 | |
| 				pathListPluginConnection(&b),
 | |
| 				pathConfigurePluginConnection(&b),
 | |
| 				pathResetConnection(&b),
 | |
| 			},
 | |
| 			pathListRoles(&b),
 | |
| 			pathRoles(&b),
 | |
| 			pathCredsCreate(&b),
 | |
| 			pathRotateCredentials(&b),
 | |
| 		),
 | |
| 
 | |
| 		Secrets: []*framework.Secret{
 | |
| 			secretCreds(&b),
 | |
| 		},
 | |
| 		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
 | |
| }
 | |
| 
 | |
| type databaseBackend struct {
 | |
| 	connections map[string]*dbPluginInstance
 | |
| 	logger      log.Logger
 | |
| 
 | |
| 	*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) {
 | |
| 	entry, err := s.Get(ctx, fmt.Sprintf("config/%s", name))
 | |
| 	if err != nil {
 | |
| 		return nil, errwrap.Wrapf("failed to read connection configuration: {{err}}", err)
 | |
| 	}
 | |
| 	if entry == nil {
 | |
| 		return nil, fmt.Errorf("failed to find entry for connection with name: %q", name)
 | |
| 	}
 | |
| 
 | |
| 	var config DatabaseConfig
 | |
| 	if err := entry.DecodeJSON(&config); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &config, nil
 | |
| }
 | |
| 
 | |
| type upgradeStatements struct {
 | |
| 	// This json tag has a typo in it, the new version does not. This
 | |
| 	// necessitates this upgrade logic.
 | |
| 	CreationStatements   string `json:"creation_statments"`
 | |
| 	RevocationStatements string `json:"revocation_statements"`
 | |
| 	RollbackStatements   string `json:"rollback_statements"`
 | |
| 	RenewStatements      string `json:"renew_statements"`
 | |
| }
 | |
| 
 | |
| type upgradeCheck struct {
 | |
| 	// This json tag has a typo in it, the new version does not. This
 | |
| 	// necessitates this upgrade logic.
 | |
| 	Statements *upgradeStatements `json:"statments,omitempty"`
 | |
| }
 | |
| 
 | |
| func (b *databaseBackend) Role(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) {
 | |
| 	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
 | |
| 	}
 | |
| 	if entry == nil {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	var upgradeCh upgradeCheck
 | |
| 	if err := entry.DecodeJSON(&upgradeCh); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	var result roleEntry
 | |
| 	if err := entry.DecodeJSON(&result); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	switch {
 | |
| 	case upgradeCh.Statements != nil:
 | |
| 		var stmts dbplugin.Statements
 | |
| 		if upgradeCh.Statements.CreationStatements != "" {
 | |
| 			stmts.Creation = []string{upgradeCh.Statements.CreationStatements}
 | |
| 		}
 | |
| 		if upgradeCh.Statements.RevocationStatements != "" {
 | |
| 			stmts.Revocation = []string{upgradeCh.Statements.RevocationStatements}
 | |
| 		}
 | |
| 		if upgradeCh.Statements.RollbackStatements != "" {
 | |
| 			stmts.Rollback = []string{upgradeCh.Statements.RollbackStatements}
 | |
| 		}
 | |
| 		if upgradeCh.Statements.RenewStatements != "" {
 | |
| 			stmts.Renewal = []string{upgradeCh.Statements.RenewStatements}
 | |
| 		}
 | |
| 		result.Statements = stmts
 | |
| 	}
 | |
| 
 | |
| 	result.Statements.Revocation = strutil.RemoveEmpty(result.Statements.Revocation)
 | |
| 
 | |
| 	// For backwards compatibility, copy the values back into the string form
 | |
| 	// of the fields
 | |
| 	result.Statements = dbutil.StatementCompatibilityHelper(result.Statements)
 | |
| 
 | |
| 	return &result, nil
 | |
| }
 | |
| 
 | |
| func (b *databaseBackend) invalidate(ctx context.Context, key string) {
 | |
| 	switch {
 | |
| 	case strings.HasPrefix(key, databaseConfigPath):
 | |
| 		name := strings.TrimPrefix(key, databaseConfigPath)
 | |
| 		b.ClearConnection(name)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (b *databaseBackend) GetConnection(ctx context.Context, s logical.Storage, name string) (*dbPluginInstance, error) {
 | |
| 	b.RLock()
 | |
| 	unlockFunc := b.RUnlock
 | |
| 	defer func() { unlockFunc() }()
 | |
| 
 | |
| 	db, ok := b.connections[name]
 | |
| 	if ok {
 | |
| 		return db, nil
 | |
| 	}
 | |
| 
 | |
| 	// Upgrade lock
 | |
| 	b.RUnlock()
 | |
| 	b.Lock()
 | |
| 	unlockFunc = b.Unlock
 | |
| 
 | |
| 	db, ok = b.connections[name]
 | |
| 	if ok {
 | |
| 		return db, nil
 | |
| 	}
 | |
| 
 | |
| 	config, err := b.DatabaseConfig(ctx, s, name)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	dbp, err := dbplugin.PluginFactory(ctx, config.PluginName, b.System(), b.logger)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	_, err = dbp.Init(ctx, config.ConnectionDetails, true)
 | |
| 	if err != nil {
 | |
| 		dbp.Close()
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	id, err := uuid.GenerateUUID()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	db = &dbPluginInstance{
 | |
| 		Database: dbp,
 | |
| 		name:     name,
 | |
| 		id:       id,
 | |
| 	}
 | |
| 
 | |
| 	b.connections[name] = db
 | |
| 	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 {
 | |
| 	b.Lock()
 | |
| 	defer b.Unlock()
 | |
| 	return b.clearConnection(name)
 | |
| }
 | |
| 
 | |
| func (b *databaseBackend) clearConnection(name string) error {
 | |
| 	db, ok := b.connections[name]
 | |
| 	if ok {
 | |
| 		// Ignore error here since the database client is always killed
 | |
| 		db.Close()
 | |
| 		delete(b.connections, name)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (b *databaseBackend) CloseIfShutdown(db *dbPluginInstance, err error) {
 | |
| 	// Plugin has shutdown, close it so next call can reconnect.
 | |
| 	switch err {
 | |
| 	case rpc.ErrShutdown, dbplugin.ErrPluginShutdown:
 | |
| 		// Put this in a goroutine so that requests can run with the read or write lock
 | |
| 		// and simply defer the unlock.  Since we are attaching the instance and matching
 | |
| 		// the id in the connection map, we can safely do this.
 | |
| 		go func() {
 | |
| 			b.Lock()
 | |
| 			defer b.Unlock()
 | |
| 			db.Close()
 | |
| 
 | |
| 			// Ensure we are deleting the correct connection
 | |
| 			mapDB, ok := b.connections[db.name]
 | |
| 			if ok && db.id == mapDB.id {
 | |
| 				delete(b.connections, db.name)
 | |
| 			}
 | |
| 		}()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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()
 | |
| 
 | |
| 	for _, db := range b.connections {
 | |
| 		db.Close()
 | |
| 	}
 | |
| 	b.connections = make(map[string]*dbPluginInstance)
 | |
| }
 | |
| 
 | |
| const backendHelp = `
 | |
| The database backend supports using many different databases
 | |
| as secret backends, including but not limited to:
 | |
| cassandra, mssql, mysql, postgres
 | |
| 
 | |
| After mounting this backend, configure it using the endpoints within
 | |
| the "database/config/" path.
 | |
| `
 |