Use the role name in the db username (#2812)

This commit is contained in:
Brian Kassouf
2017-06-06 06:49:49 -07:00
committed by Jeff Mitchell
parent 2631bde3ef
commit abc900157b
24 changed files with 291 additions and 147 deletions

View File

@@ -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,
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -23,8 +23,8 @@ const (
)
var (
DisplayNameLen int = 10
LegacyDisplayNameLen int = 4
MetadataLen int = 10
LegacyMetadataLen int = 4
UsernameLen int = 32
LegacyUsernameLen int = 16
)
@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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]
}

View File

@@ -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=<expected SHA256 Hex value of the plugin binary> \
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

View File

@@ -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