diff --git a/builtin/logical/database/dbplugin/client.go b/builtin/logical/database/dbplugin/client.go index 0c095f891035..95b614b8073d 100644 --- a/builtin/logical/database/dbplugin/client.go +++ b/builtin/logical/database/dbplugin/client.go @@ -78,10 +78,10 @@ func (dr *databasePluginRPCClient) Type() (string, error) { return fmt.Sprintf("plugin-%s", dbType), err } -func (dr *databasePluginRPCClient) CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (dr *databasePluginRPCClient) CreateUser(statements Statements, usernameConfig UsernameConfig, expiration time.Time) (username string, password string, err error) { req := CreateUserRequest{ Statements: statements, - UsernamePrefix: usernamePrefix, + UsernameConfig: usernameConfig, Expiration: expiration, } diff --git a/builtin/logical/database/dbplugin/databasemiddleware.go b/builtin/logical/database/dbplugin/databasemiddleware.go index 83f57ef87e80..87dfa6c3143d 100644 --- a/builtin/logical/database/dbplugin/databasemiddleware.go +++ b/builtin/logical/database/dbplugin/databasemiddleware.go @@ -22,13 +22,13 @@ func (mw *databaseTracingMiddleware) Type() (string, error) { return mw.next.Type() } -func (mw *databaseTracingMiddleware) CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (mw *databaseTracingMiddleware) CreateUser(statements Statements, usernameConfig UsernameConfig, expiration time.Time) (username string, password string, err error) { defer func(then time.Time) { mw.logger.Trace("database", "operation", "CreateUser", "status", "finished", "type", mw.typeStr, "err", err, "took", time.Since(then)) }(time.Now()) mw.logger.Trace("database", "operation", "CreateUser", "status", "started", "type", mw.typeStr) - return mw.next.CreateUser(statements, usernamePrefix, expiration) + return mw.next.CreateUser(statements, usernameConfig, expiration) } func (mw *databaseTracingMiddleware) RenewUser(statements Statements, username string, expiration time.Time) (err error) { @@ -81,7 +81,7 @@ func (mw *databaseMetricsMiddleware) Type() (string, error) { return mw.next.Type() } -func (mw *databaseMetricsMiddleware) CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (mw *databaseMetricsMiddleware) CreateUser(statements Statements, usernameConfig UsernameConfig, expiration time.Time) (username string, password string, err error) { defer func(now time.Time) { metrics.MeasureSince([]string{"database", "CreateUser"}, now) metrics.MeasureSince([]string{"database", mw.typeStr, "CreateUser"}, now) @@ -94,7 +94,7 @@ func (mw *databaseMetricsMiddleware) CreateUser(statements Statements, usernameP metrics.IncrCounter([]string{"database", "CreateUser"}, 1) metrics.IncrCounter([]string{"database", mw.typeStr, "CreateUser"}, 1) - return mw.next.CreateUser(statements, usernamePrefix, expiration) + return mw.next.CreateUser(statements, usernameConfig, expiration) } func (mw *databaseMetricsMiddleware) RenewUser(statements Statements, username string, expiration time.Time) (err error) { diff --git a/builtin/logical/database/dbplugin/plugin.go b/builtin/logical/database/dbplugin/plugin.go index bc63594ae8c0..9ab7020436ab 100644 --- a/builtin/logical/database/dbplugin/plugin.go +++ b/builtin/logical/database/dbplugin/plugin.go @@ -13,7 +13,7 @@ import ( // Database is the interface that all database objects must implement. type Database interface { Type() (string, error) - CreateUser(statements Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) + CreateUser(statements Statements, usernameConfig UsernameConfig, expiration time.Time) (username string, password string, err error) RenewUser(statements Statements, username string, expiration time.Time) error RevokeUser(statements Statements, username string) error @@ -29,6 +29,13 @@ type Statements struct { RenewStatements string `json:"renew_statements" mapstructure:"renew_statements" structs:"renew_statements"` } +// UsernameConfig is used to configure prefixes for the username to be +// generated. +type UsernameConfig struct { + DisplayName string + RoleName string +} + // PluginFactory is used to build plugin database types. It wraps the database // object in a logging and metrics middleware. func PluginFactory(pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) { @@ -89,7 +96,7 @@ func PluginFactory(pluginName string, sys pluginutil.LookRunnerUtil, logger log. // This prevents users from executing bad plugins or executing a plugin // directory. It is a UX feature, not a security feature. var handshakeConfig = plugin.HandshakeConfig{ - ProtocolVersion: 1, + ProtocolVersion: 2, MagicCookieKey: "VAULT_DATABASE_PLUGIN", MagicCookieValue: "926a0820-aea2-be28-51d6-83cdf00e8edb", } @@ -117,7 +124,7 @@ type InitializeRequest struct { type CreateUserRequest struct { Statements Statements - UsernamePrefix string + UsernameConfig UsernameConfig Expiration time.Time } diff --git a/builtin/logical/database/dbplugin/plugin_test.go b/builtin/logical/database/dbplugin/plugin_test.go index c95e119e0ca0..d0e7073d87e9 100644 --- a/builtin/logical/database/dbplugin/plugin_test.go +++ b/builtin/logical/database/dbplugin/plugin_test.go @@ -21,19 +21,19 @@ type mockPlugin struct { } func (m *mockPlugin) Type() (string, error) { return "mock", nil } -func (m *mockPlugin) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (m *mockPlugin) CreateUser(statements dbplugin.Statements, usernameConf dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { err = errors.New("err") - if usernamePrefix == "" || expiration.IsZero() { + if usernameConf.DisplayName == "" || expiration.IsZero() { return "", "", err } - if _, ok := m.users[usernamePrefix]; ok { + if _, ok := m.users[usernameConf.DisplayName]; ok { return "", "", err } - m.users[usernamePrefix] = []string{password} + m.users[usernameConf.DisplayName] = []string{password} - return usernamePrefix, "test", nil + return usernameConf.DisplayName, "test", nil } func (m *mockPlugin) RenewUser(statements dbplugin.Statements, username string, expiration time.Time) error { err := errors.New("err") @@ -162,7 +162,12 @@ func TestPlugin_CreateUser(t *testing.T) { t.Fatalf("err: %s", err) } - us, pw, err := db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute)) + usernameConf := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + us, pw, err := db.CreateUser(dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -172,7 +177,7 @@ func TestPlugin_CreateUser(t *testing.T) { // try and save the same user again to verify it saved the first time, this // should return an error - _, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute)) + _, _, err = db.CreateUser(dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) if err == nil { t.Fatal("expected an error, user wasn't created correctly") } @@ -198,7 +203,12 @@ func TestPlugin_RenewUser(t *testing.T) { t.Fatalf("err: %s", err) } - us, _, err := db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute)) + usernameConf := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + us, _, err := db.CreateUser(dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -229,7 +239,12 @@ func TestPlugin_RevokeUser(t *testing.T) { t.Fatalf("err: %s", err) } - us, _, err := db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute)) + usernameConf := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + us, _, err := db.CreateUser(dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -241,7 +256,7 @@ func TestPlugin_RevokeUser(t *testing.T) { } // Try adding the same username back so we can verify it was removed - _, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute)) + _, _, err = db.CreateUser(dbplugin.Statements{}, usernameConf, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } diff --git a/builtin/logical/database/dbplugin/server.go b/builtin/logical/database/dbplugin/server.go index 9546d092c276..381f0ae2a1f4 100644 --- a/builtin/logical/database/dbplugin/server.go +++ b/builtin/logical/database/dbplugin/server.go @@ -42,7 +42,7 @@ func (ds *databasePluginRPCServer) Type(_ struct{}, resp *string) error { func (ds *databasePluginRPCServer) CreateUser(args *CreateUserRequest, resp *CreateUserResponse) error { var err error - resp.Username, resp.Password, err = ds.impl.CreateUser(args.Statements, args.UsernamePrefix, args.Expiration) + resp.Username, resp.Password, err = ds.impl.CreateUser(args.Statements, args.UsernameConfig, args.Expiration) return err } diff --git a/builtin/logical/database/path_creds_create.go b/builtin/logical/database/path_creds_create.go index 7bc7dfa6fe7e..6fb61a3e5209 100644 --- a/builtin/logical/database/path_creds_create.go +++ b/builtin/logical/database/path_creds_create.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/hashicorp/vault/builtin/logical/database/dbplugin" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -74,8 +75,13 @@ func (b *databaseBackend) pathCredsCreateRead() framework.OperationFunc { expiration := time.Now().Add(role.DefaultTTL) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: req.DisplayName, + RoleName: name, + } + // Create the user - username, password, err := db.CreateUser(role.Statements, req.DisplayName, expiration) + username, password, err := db.CreateUser(role.Statements, usernameConfig, expiration) // Unlock unlockFunc() if err != nil { diff --git a/helper/builtinplugins/builtin.go b/helper/builtinplugins/builtin.go index f1def367a753..40f3b130730e 100644 --- a/helper/builtinplugins/builtin.go +++ b/helper/builtinplugins/builtin.go @@ -13,10 +13,10 @@ type BuiltinFactory func() (interface{}, error) var plugins map[string]BuiltinFactory = map[string]BuiltinFactory{ // These four plugins all use the same mysql implementation but with // different username settings passed by the constructor. - "mysql-database-plugin": mysql.New(mysql.DisplayNameLen, mysql.UsernameLen), - "mysql-aurora-database-plugin": mysql.New(mysql.LegacyDisplayNameLen, mysql.LegacyUsernameLen), - "mysql-rds-database-plugin": mysql.New(mysql.LegacyDisplayNameLen, mysql.LegacyUsernameLen), - "mysql-legacy-database-plugin": mysql.New(mysql.LegacyDisplayNameLen, mysql.LegacyUsernameLen), + "mysql-database-plugin": mysql.New(mysql.MetadataLen, mysql.UsernameLen), + "mysql-aurora-database-plugin": mysql.New(mysql.LegacyMetadataLen, mysql.LegacyUsernameLen), + "mysql-rds-database-plugin": mysql.New(mysql.LegacyMetadataLen, mysql.LegacyUsernameLen), + "mysql-legacy-database-plugin": mysql.New(mysql.LegacyMetadataLen, mysql.LegacyUsernameLen), "postgresql-database-plugin": postgresql.New, "mssql-database-plugin": mssql.New, diff --git a/plugins/database/cassandra/cassandra.go b/plugins/database/cassandra/cassandra.go index cafb2545cfb1..3ed59a8465c8 100644 --- a/plugins/database/cassandra/cassandra.go +++ b/plugins/database/cassandra/cassandra.go @@ -32,7 +32,12 @@ func New() (interface{}, error) { connProducer := &cassandraConnectionProducer{} connProducer.Type = cassandraTypeName - credsProducer := &cassandraCredentialsProducer{} + credsProducer := &credsutil.SQLCredentialsProducer{ + DisplayNameLen: 15, + RoleNameLen: 15, + UsernameLen: 100, + Separator: "_", + } dbType := &Cassandra{ ConnectionProducer: connProducer, @@ -70,7 +75,7 @@ func (c *Cassandra) getConnection() (*gocql.Session, error) { // CreateUser generates the username/password on the underlying Cassandra secret backend as instructed by // the CreationStatement provided. -func (c *Cassandra) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (c *Cassandra) CreateUser(statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { // Grab the lock c.Lock() defer c.Unlock() @@ -90,10 +95,12 @@ func (c *Cassandra) CreateUser(statements dbplugin.Statements, usernamePrefix st rollbackCQL = defaultUserDeletionCQL } - username, err = c.GenerateUsername(usernamePrefix) + username, err = c.GenerateUsername(usernameConfig) if err != nil { return "", "", err } + // Cassandra doesn't like the uppercase usernames + username = strings.ToLower(username) password, err = c.GeneratePassword() if err != nil { diff --git a/plugins/database/cassandra/cassandra_test.go b/plugins/database/cassandra/cassandra_test.go index b10008e3519d..17f7d0a92a09 100644 --- a/plugins/database/cassandra/cassandra_test.go +++ b/plugins/database/cassandra/cassandra_test.go @@ -126,7 +126,12 @@ func TestCassandra_CreateUser(t *testing.T) { CreationStatements: testCassandraRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -161,7 +166,12 @@ func TestMyCassandra_RenewUser(t *testing.T) { CreationStatements: testCassandraRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -201,7 +211,12 @@ func TestCassandra_RevokeUser(t *testing.T) { CreationStatements: testCassandraRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } diff --git a/plugins/database/cassandra/credentials_producer.go b/plugins/database/cassandra/credentials_producer.go deleted file mode 100644 index 8e9c3a509adf..000000000000 --- a/plugins/database/cassandra/credentials_producer.go +++ /dev/null @@ -1,37 +0,0 @@ -package cassandra - -import ( - "fmt" - "strings" - "time" - - uuid "github.com/hashicorp/go-uuid" -) - -// cassandraCredentialsProducer implements CredentialsProducer and provides an -// interface for cassandra databases to generate user information. -type cassandraCredentialsProducer struct{} - -func (ccp *cassandraCredentialsProducer) GenerateUsername(displayName string) (string, error) { - userUUID, err := uuid.GenerateUUID() - if err != nil { - return "", err - } - username := fmt.Sprintf("vault_%s_%s_%d", displayName, userUUID, time.Now().Unix()) - username = strings.Replace(username, "-", "_", -1) - - return username, nil -} - -func (ccp *cassandraCredentialsProducer) GeneratePassword() (string, error) { - password, err := uuid.GenerateUUID() - if err != nil { - return "", err - } - - return password, nil -} - -func (ccp *cassandraCredentialsProducer) GenerateExpiration(ttl time.Time) (string, error) { - return "", nil -} diff --git a/plugins/database/mongodb/credentials_producer.go b/plugins/database/mongodb/credentials_producer.go deleted file mode 100644 index 80dc2c3d39d1..000000000000 --- a/plugins/database/mongodb/credentials_producer.go +++ /dev/null @@ -1,36 +0,0 @@ -package mongodb - -import ( - "fmt" - "time" - - uuid "github.com/hashicorp/go-uuid" -) - -// mongoDBCredentialsProducer implements CredentialsProducer and provides an -// interface for databases to generate user information. -type mongoDBCredentialsProducer struct{} - -func (cp *mongoDBCredentialsProducer) GenerateUsername(displayName string) (string, error) { - userUUID, err := uuid.GenerateUUID() - if err != nil { - return "", err - } - - username := fmt.Sprintf("vault-%s-%s", displayName, userUUID) - - return username, nil -} - -func (cp *mongoDBCredentialsProducer) GeneratePassword() (string, error) { - password, err := uuid.GenerateUUID() - if err != nil { - return "", err - } - - return password, nil -} - -func (cp *mongoDBCredentialsProducer) GenerateExpiration(ttl time.Time) (string, error) { - return "", nil -} diff --git a/plugins/database/mongodb/mongodb.go b/plugins/database/mongodb/mongodb.go index 5d7aa09b1c71..31fdd85cc82a 100644 --- a/plugins/database/mongodb/mongodb.go +++ b/plugins/database/mongodb/mongodb.go @@ -29,7 +29,12 @@ func New() (interface{}, error) { connProducer := &mongoDBConnectionProducer{} connProducer.Type = mongoDBTypeName - credsProducer := &mongoDBCredentialsProducer{} + credsProducer := &credsutil.SQLCredentialsProducer{ + DisplayNameLen: 15, + RoleNameLen: 15, + UsernameLen: 100, + Separator: "-", + } dbType := &MongoDB{ ConnectionProducer: connProducer, @@ -72,7 +77,7 @@ func (m *MongoDB) getConnection() (*mgo.Session, error) { // // JSON Example: // { "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] } -func (m *MongoDB) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (m *MongoDB) CreateUser(statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { // Grab the lock m.Lock() defer m.Unlock() @@ -86,7 +91,7 @@ func (m *MongoDB) CreateUser(statements dbplugin.Statements, usernamePrefix stri return "", "", err } - username, err = m.GenerateUsername(usernamePrefix) + username, err = m.GenerateUsername(usernameConfig) if err != nil { return "", "", err } diff --git a/plugins/database/mongodb/mongodb_test.go b/plugins/database/mongodb/mongodb_test.go index 1fa14aa37fc4..95f6e90888c3 100644 --- a/plugins/database/mongodb/mongodb_test.go +++ b/plugins/database/mongodb/mongodb_test.go @@ -114,7 +114,12 @@ func TestMongoDB_CreateUser(t *testing.T) { CreationStatements: testMongoDBRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -146,7 +151,12 @@ func TestMongoDB_RevokeUser(t *testing.T) { CreationStatements: testMongoDBRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } diff --git a/plugins/database/mssql/mssql.go b/plugins/database/mssql/mssql.go index 9b22aa87cdfa..2f4a628b94d2 100644 --- a/plugins/database/mssql/mssql.go +++ b/plugins/database/mssql/mssql.go @@ -29,7 +29,9 @@ func New() (interface{}, error) { credsProducer := &credsutil.SQLCredentialsProducer{ DisplayNameLen: 20, + RoleNameLen: 20, UsernameLen: 128, + Separator: "-", } dbType := &MSSQL{ @@ -68,7 +70,7 @@ func (m *MSSQL) getConnection() (*sql.DB, error) { // CreateUser generates the username/password on the underlying MSSQL secret backend as instructed by // the CreationStatement provided. -func (m *MSSQL) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (m *MSSQL) CreateUser(statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { // Grab the lock m.Lock() defer m.Unlock() @@ -83,7 +85,7 @@ func (m *MSSQL) CreateUser(statements dbplugin.Statements, usernamePrefix string return "", "", dbutil.ErrEmptyCreationStatement } - username, err = m.GenerateUsername(usernamePrefix) + username, err = m.GenerateUsername(usernameConfig) if err != nil { return "", "", err } diff --git a/plugins/database/mssql/mssql_test.go b/plugins/database/mssql/mssql_test.go index 830e38abbd33..e524455cc333 100644 --- a/plugins/database/mssql/mssql_test.go +++ b/plugins/database/mssql/mssql_test.go @@ -63,8 +63,13 @@ func TestMSSQL_CreateUser(t *testing.T) { t.Fatalf("err: %s", err) } + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + // Test with no configured Creation Statememt - _, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute)) + _, _, err = db.CreateUser(dbplugin.Statements{}, usernameConfig, time.Now().Add(time.Minute)) if err == nil { t.Fatal("Expected error when no creation statement is provided") } @@ -73,7 +78,7 @@ func TestMSSQL_CreateUser(t *testing.T) { CreationStatements: testMSSQLRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -104,7 +109,12 @@ func TestMSSQL_RevokeUser(t *testing.T) { CreationStatements: testMSSQLRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(2*time.Second)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(2*time.Second)) if err != nil { t.Fatalf("err: %s", err) } @@ -123,7 +133,7 @@ func TestMSSQL_RevokeUser(t *testing.T) { t.Fatal("Credentials were not revoked") } - username, password, err = db.CreateUser(statements, "test", time.Now().Add(2*time.Second)) + username, password, err = db.CreateUser(statements, usernameConfig, time.Now().Add(2*time.Second)) if err != nil { t.Fatalf("err: %s", err) } diff --git a/plugins/database/mysql/mysql.go b/plugins/database/mysql/mysql.go index b875af520d56..31a7b7c7d60e 100644 --- a/plugins/database/mysql/mysql.go +++ b/plugins/database/mysql/mysql.go @@ -23,10 +23,10 @@ const ( ) var ( - DisplayNameLen int = 10 - LegacyDisplayNameLen int = 4 - UsernameLen int = 32 - LegacyUsernameLen int = 16 + MetadataLen int = 10 + LegacyMetadataLen int = 4 + UsernameLen int = 32 + LegacyUsernameLen int = 16 ) type MySQL struct { @@ -35,14 +35,16 @@ type MySQL struct { } // New implements builtinplugins.BuiltinFactory -func New(displayLen, usernameLen int) func() (interface{}, error) { +func New(metadataLen, usernameLen int) func() (interface{}, error) { return func() (interface{}, error) { connProducer := &connutil.SQLConnectionProducer{} connProducer.Type = mySQLTypeName credsProducer := &credsutil.SQLCredentialsProducer{ - DisplayNameLen: displayLen, + DisplayNameLen: metadataLen, + RoleNameLen: metadataLen, UsernameLen: usernameLen, + Separator: "-", } dbType := &MySQL{ @@ -56,7 +58,7 @@ func New(displayLen, usernameLen int) func() (interface{}, error) { // Run instantiates a MySQL object, and runs the RPC server for the plugin func Run(apiTLSConfig *api.TLSConfig) error { - f := New(DisplayNameLen, UsernameLen) + f := New(MetadataLen, UsernameLen) dbType, err := f() if err != nil { return err @@ -80,7 +82,7 @@ func (m *MySQL) getConnection() (*sql.DB, error) { return db.(*sql.DB), nil } -func (m *MySQL) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (m *MySQL) CreateUser(statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { // Grab the lock m.Lock() defer m.Unlock() @@ -95,7 +97,7 @@ func (m *MySQL) CreateUser(statements dbplugin.Statements, usernamePrefix string return "", "", dbutil.ErrEmptyCreationStatement } - username, err = m.GenerateUsername(usernamePrefix) + username, err = m.GenerateUsername(usernameConfig) if err != nil { return "", "", err } diff --git a/plugins/database/mysql/mysql_test.go b/plugins/database/mysql/mysql_test.go index 72dbd81560ad..616859be8b66 100644 --- a/plugins/database/mysql/mysql_test.go +++ b/plugins/database/mysql/mysql_test.go @@ -66,7 +66,7 @@ func TestMySQL_Initialize(t *testing.T) { "connection_url": connURL, } - f := New(DisplayNameLen, UsernameLen) + f := New(MetadataLen, UsernameLen) dbRaw, _ := f() db := dbRaw.(*MySQL) connProducer := db.ConnectionProducer.(*connutil.SQLConnectionProducer) @@ -94,7 +94,7 @@ func TestMySQL_CreateUser(t *testing.T) { "connection_url": connURL, } - f := New(DisplayNameLen, UsernameLen) + f := New(MetadataLen, UsernameLen) dbRaw, _ := f() db := dbRaw.(*MySQL) @@ -103,8 +103,13 @@ func TestMySQL_CreateUser(t *testing.T) { t.Fatalf("err: %s", err) } + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + // Test with no configured Creation Statememt - _, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute)) + _, _, err = db.CreateUser(dbplugin.Statements{}, usernameConfig, time.Now().Add(time.Minute)) if err == nil { t.Fatal("Expected error when no creation statement is provided") } @@ -113,7 +118,7 @@ func TestMySQL_CreateUser(t *testing.T) { CreationStatements: testMySQLRoleWildCard, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -131,7 +136,7 @@ func TestMySQL_RevokeUser(t *testing.T) { "connection_url": connURL, } - f := New(DisplayNameLen, UsernameLen) + f := New(MetadataLen, UsernameLen) dbRaw, _ := f() db := dbRaw.(*MySQL) @@ -144,7 +149,12 @@ func TestMySQL_RevokeUser(t *testing.T) { CreationStatements: testMySQLRoleWildCard, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -164,7 +174,7 @@ func TestMySQL_RevokeUser(t *testing.T) { } statements.CreationStatements = testMySQLRoleWildCard - username, password, err = db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + username, password, err = db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } diff --git a/plugins/database/postgresql/postgresql.go b/plugins/database/postgresql/postgresql.go index 69bfe3405959..b1ebc9987ac8 100644 --- a/plugins/database/postgresql/postgresql.go +++ b/plugins/database/postgresql/postgresql.go @@ -29,8 +29,10 @@ func New() (interface{}, error) { connProducer.Type = postgreSQLTypeName credsProducer := &credsutil.SQLCredentialsProducer{ - DisplayNameLen: 10, + DisplayNameLen: 8, + RoleNameLen: 8, UsernameLen: 63, + Separator: "-", } dbType := &PostgreSQL{ @@ -71,7 +73,7 @@ func (p *PostgreSQL) getConnection() (*sql.DB, error) { return db.(*sql.DB), nil } -func (p *PostgreSQL) CreateUser(statements dbplugin.Statements, usernamePrefix string, expiration time.Time) (username string, password string, err error) { +func (p *PostgreSQL) CreateUser(statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { if statements.CreationStatements == "" { return "", "", dbutil.ErrEmptyCreationStatement } @@ -80,7 +82,7 @@ func (p *PostgreSQL) CreateUser(statements dbplugin.Statements, usernamePrefix s p.Lock() defer p.Unlock() - username, err = p.GenerateUsername(usernamePrefix) + username, err = p.GenerateUsername(usernameConfig) if err != nil { return "", "", err } diff --git a/plugins/database/postgresql/postgresql_test.go b/plugins/database/postgresql/postgresql_test.go index 3fd441bc59bd..944a3b7b5c4c 100644 --- a/plugins/database/postgresql/postgresql_test.go +++ b/plugins/database/postgresql/postgresql_test.go @@ -101,8 +101,13 @@ func TestPostgreSQL_CreateUser(t *testing.T) { t.Fatalf("err: %s", err) } + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + // Test with no configured Creation Statememt - _, _, err = db.CreateUser(dbplugin.Statements{}, "test", time.Now().Add(time.Minute)) + _, _, err = db.CreateUser(dbplugin.Statements{}, usernameConfig, time.Now().Add(time.Minute)) if err == nil { t.Fatal("Expected error when no creation statement is provided") } @@ -111,7 +116,7 @@ func TestPostgreSQL_CreateUser(t *testing.T) { CreationStatements: testPostgresRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -121,7 +126,7 @@ func TestPostgreSQL_CreateUser(t *testing.T) { } statements.CreationStatements = testPostgresReadOnlyRole - username, password, err = db.CreateUser(statements, "test", time.Now().Add(time.Minute)) + username, password, err = db.CreateUser(statements, usernameConfig, time.Now().Add(time.Minute)) if err != nil { t.Fatalf("err: %s", err) } @@ -150,7 +155,12 @@ func TestPostgreSQL_RenewUser(t *testing.T) { CreationStatements: testPostgresRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(2*time.Second)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(2*time.Second)) if err != nil { t.Fatalf("err: %s", err) } @@ -171,7 +181,7 @@ func TestPostgreSQL_RenewUser(t *testing.T) { t.Fatalf("Could not connect with new credentials: %s", err) } statements.RenewStatements = defaultPostgresRenewSQL - username, password, err = db.CreateUser(statements, "test", time.Now().Add(2*time.Second)) + username, password, err = db.CreateUser(statements, usernameConfig, time.Now().Add(2*time.Second)) if err != nil { t.Fatalf("err: %s", err) } @@ -213,7 +223,12 @@ func TestPostgreSQL_RevokeUser(t *testing.T) { CreationStatements: testPostgresRole, } - username, password, err := db.CreateUser(statements, "test", time.Now().Add(2*time.Second)) + usernameConfig := dbplugin.UsernameConfig{ + DisplayName: "test", + RoleName: "test", + } + + username, password, err := db.CreateUser(statements, usernameConfig, time.Now().Add(2*time.Second)) if err != nil { t.Fatalf("err: %s", err) } @@ -232,7 +247,7 @@ func TestPostgreSQL_RevokeUser(t *testing.T) { t.Fatal("Credentials were not revoked") } - username, password, err = db.CreateUser(statements, "test", time.Now().Add(2*time.Second)) + username, password, err = db.CreateUser(statements, usernameConfig, time.Now().Add(2*time.Second)) if err != nil { t.Fatalf("err: %s", err) } diff --git a/plugins/helper/database/credsutil/credsutil.go b/plugins/helper/database/credsutil/credsutil.go index bc35617ac215..01b5f3d259db 100644 --- a/plugins/helper/database/credsutil/credsutil.go +++ b/plugins/helper/database/credsutil/credsutil.go @@ -1,12 +1,64 @@ package credsutil -import "time" +import ( + "crypto/rand" + "time" + + "github.com/hashicorp/vault/builtin/logical/database/dbplugin" +) // CredentialsProducer can be used as an embeded interface in the Database // definition. It implements the methods for generating user information for a // particular database type and is used in all the builtin database types. type CredentialsProducer interface { - GenerateUsername(displayName string) (string, error) + GenerateUsername(usernameConfig dbplugin.UsernameConfig) (string, error) GeneratePassword() (string, error) GenerateExpiration(ttl time.Time) (string, error) } + +// RandomAlphaNumericOfLen returns a random byte slice of characters [A-Za-z0-9] +// of the provided length. +func RandomAlphaNumericOfLen(len int) ([]byte, error) { + retBytes := make([]byte, len) + size := 0 + + for size < len { + // Extend the len of the random byte slice to lower odds of having to + // re-roll. + c := len + 3 + bArr := make([]byte, c) + _, err := rand.Read(bArr) + if err != nil { + return nil, err + } + + for _, b := range bArr { + if size == len { + break + } + + /** + * Each byte will be in [0, 256), but we only care about: + * + * [48, 57] 0-9 + * [65, 90] A-Z + * [97, 122] a-z + * + * Which means that the highest bit will always be zero, since the last byte with high bit + * zero is 01111111 = 127 which is higher than 122. Lower our odds of having to re-roll a byte by + * dividing by two (right bit shift of 1). + */ + + b = b >> 1 + + // The byte is any of 0-9 A-Z a-z + byteIsAllowable := (b >= 48 && b <= 57) || (b >= 65 && b <= 90) || (b >= 97 && b <= 122) + if byteIsAllowable { + retBytes[size] = b + size++ + } + } + } + + return retBytes, nil +} diff --git a/plugins/helper/database/credsutil/credsutil_test.go b/plugins/helper/database/credsutil/credsutil_test.go new file mode 100644 index 000000000000..c49aab59ca3d --- /dev/null +++ b/plugins/helper/database/credsutil/credsutil_test.go @@ -0,0 +1,30 @@ +package credsutil + +import ( + "bytes" + "testing" +) + +// RandomAlphaNumericOfLen returns a random byte slice of characters [A-Za-z0-9] +// of the provided length. +func TestRandomAlphaNumericOfLen(t *testing.T) { + s, err := RandomAlphaNumericOfLen(1) + if err != nil { + t.Fatal("Unexpected error: %s", err) + } + if len(s) != 1 { + t.Fatal("Unexpected length of string, expected 1, got string: %s", s) + } + + s, err = RandomAlphaNumericOfLen(10) + if err != nil { + t.Fatal("Unexpected error: %s", err) + } + if len(s) != 10 { + t.Fatal("Unexpected length of string, expected 10, got string: %s", s) + } + + if bytes.Equal(s, make([]byte, 10)) { + t.Fatal("returned byte slice is empty") + } +} diff --git a/plugins/helper/database/credsutil/sql.go b/plugins/helper/database/credsutil/sql.go index a7929ccb1a80..ecd9b306a9ec 100644 --- a/plugins/helper/database/credsutil/sql.go +++ b/plugins/helper/database/credsutil/sql.go @@ -2,26 +2,37 @@ package credsutil import ( "fmt" + "strings" "time" uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/builtin/logical/database/dbplugin" ) // SQLCredentialsProducer implements CredentialsProducer and provides a generic credentials producer for most sql database types. type SQLCredentialsProducer struct { DisplayNameLen int + RoleNameLen int UsernameLen int + Separator string } -func (scp *SQLCredentialsProducer) GenerateUsername(displayName string) (string, error) { +func (scp *SQLCredentialsProducer) GenerateUsername(config dbplugin.UsernameConfig) (string, error) { + displayName := config.DisplayName if scp.DisplayNameLen > 0 && len(displayName) > scp.DisplayNameLen { displayName = displayName[:scp.DisplayNameLen] } - userUUID, err := uuid.GenerateUUID() + roleName := config.RoleName + if scp.RoleNameLen > 0 && len(roleName) > scp.RoleNameLen { + roleName = roleName[:scp.RoleNameLen] + } + + userUUID, err := RandomAlphaNumericOfLen(20) if err != nil { return "", err } - username := fmt.Sprintf("v-%s-%s", displayName, userUUID) + + username := strings.Join([]string{"v", displayName, roleName, string(userUUID), fmt.Sprint(time.Now().UTC().Unix())}, scp.Separator) if scp.UsernameLen > 0 && len(username) > scp.UsernameLen { username = username[:scp.UsernameLen] } diff --git a/website/source/docs/internals/plugins.html.md b/website/source/docs/internals/plugins.html.md index 937eeda54de5..02f151249189 100644 --- a/website/source/docs/internals/plugins.html.md +++ b/website/source/docs/internals/plugins.html.md @@ -57,7 +57,19 @@ Upon adding a new plugin, the plugin name, SHA256 sum of the executable, and the command that should be used to run the plugin must be provided. The catalog will make sure the executable referenced in the command exists in the plugin directory. When added to the catalog the plugin is not automatically executed, -it instead becomes visible to backends and can be executed by them. +it instead becomes visible to backends and can be executed by them. For more +information on the plugin catalog please see the [Plugin Catalog API +docs](/api/system/plugins-catalog.html). + +An example plugin submission looks like: + +``` +$ vault write sys/plugins/catalog/myplugin-database-plugin \ + sha_256= \ + command="myplugin" +Success! Data written to: sys/plugins/catalog/myplugin-database-plugin +``` + ### Plugin Execution When a backend wants to run a plugin, it first looks up the plugin, by name, in diff --git a/website/source/docs/secrets/databases/index.html.md b/website/source/docs/secrets/databases/index.html.md index c88699c44705..ee6ef86a085a 100644 --- a/website/source/docs/secrets/databases/index.html.md +++ b/website/source/docs/secrets/databases/index.html.md @@ -90,6 +90,12 @@ password 8cab931c-d62e-a73d-60d3-5ee85139cd66 username v-root-e2978cd0- ``` +## Custom Plugins + +This backend allows custom database types to be run through the exposed plugin +interface. Please see the [Custom database +plugin](/docs/secrets/databases/custom.html) for more information. + ## API The Database secret backend has a full HTTP API. Please see the [Database secret