MySQL - Add username customization (#10834)

This commit is contained in:
Michael Golowka
2021-02-11 14:08:32 -07:00
committed by GitHub
parent b08870db30
commit 7bfe785092
9 changed files with 479 additions and 250 deletions

View File

@@ -52,7 +52,6 @@ import (
dbMysql "github.com/hashicorp/vault/plugins/database/mysql" dbMysql "github.com/hashicorp/vault/plugins/database/mysql"
dbPostgres "github.com/hashicorp/vault/plugins/database/postgresql" dbPostgres "github.com/hashicorp/vault/plugins/database/postgresql"
dbRedshift "github.com/hashicorp/vault/plugins/database/redshift" dbRedshift "github.com/hashicorp/vault/plugins/database/redshift"
"github.com/hashicorp/vault/sdk/database/helper/credsutil"
"github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
) )
@@ -94,10 +93,10 @@ func newRegistry() *registry {
databasePlugins: map[string]BuiltinFactory{ databasePlugins: map[string]BuiltinFactory{
// These four plugins all use the same mysql implementation but with // These four plugins all use the same mysql implementation but with
// different username settings passed by the constructor. // different username settings passed by the constructor.
"mysql-database-plugin": dbMysql.New(dbMysql.MetadataLen, dbMysql.MetadataLen, dbMysql.UsernameLen), "mysql-database-plugin": dbMysql.New(dbMysql.DefaultUserNameTemplate),
"mysql-aurora-database-plugin": dbMysql.New(credsutil.NoneLength, dbMysql.LegacyMetadataLen, dbMysql.LegacyUsernameLen), "mysql-aurora-database-plugin": dbMysql.New(dbMysql.DefaultLegacyUserNameTemplate),
"mysql-rds-database-plugin": dbMysql.New(credsutil.NoneLength, dbMysql.LegacyMetadataLen, dbMysql.LegacyUsernameLen), "mysql-rds-database-plugin": dbMysql.New(dbMysql.DefaultLegacyUserNameTemplate),
"mysql-legacy-database-plugin": dbMysql.New(credsutil.NoneLength, dbMysql.LegacyMetadataLen, dbMysql.LegacyUsernameLen), "mysql-legacy-database-plugin": dbMysql.New(dbMysql.DefaultLegacyUserNameTemplate),
"cassandra-database-plugin": dbCass.New, "cassandra-database-plugin": dbCass.New,
"couchbase-database-plugin": dbCouchbase.New, "couchbase-database-plugin": dbCouchbase.New,

View File

@@ -40,7 +40,7 @@ func PrepareTestContainer(t *testing.T, legacy bool, pw string) (func(), string)
svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
hostIP := docker.NewServiceHostPort(host, port) hostIP := docker.NewServiceHostPort(host, port)
connString := fmt.Sprintf("root:%s@(%s)/mysql?parseTime=true", pw, hostIP.Address()) connString := fmt.Sprintf("root:%s@tcp(%s)/mysql?parseTime=true", pw, hostIP.Address())
db, err := sql.Open("mysql", connString) db, err := sql.Open("mysql", connString)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -37,7 +37,6 @@ type mySQLConnectionProducer struct {
RawConfig map[string]interface{} RawConfig map[string]interface{}
maxConnectionLifetime time.Duration maxConnectionLifetime time.Duration
Legacy bool
Initialized bool Initialized bool
db *sql.DB db *sql.DB
sync.Mutex sync.Mutex

View File

@@ -124,7 +124,7 @@ ssl-key=/etc/mysql/server-key.pem`
// ////////////////////////////////////////////////////// // //////////////////////////////////////////////////////
// Test // Test
mysql := newMySQL(MetadataLen, MetadataLen, UsernameLen) mysql := newMySQL(DefaultUserNameTemplate)
conf := map[string]interface{}{ conf := map[string]interface{}{
"connection_url": retURL, "connection_url": retURL,

View File

@@ -19,7 +19,7 @@ func main() {
// Run instantiates a MySQL object, and runs the RPC server for the plugin // Run instantiates a MySQL object, and runs the RPC server for the plugin
func Run() error { func Run() error {
var f func() (interface{}, error) var f func() (interface{}, error)
f = mysql.New(mysql.MetadataLen, mysql.MetadataLen, mysql.UsernameLen) f = mysql.New(mysql.DefaultUserNameTemplate)
dbType, err := f() dbType, err := f()
if err != nil { if err != nil {
return err return err

View File

@@ -6,7 +6,6 @@ import (
"github.com/hashicorp/vault/plugins/database/mysql" "github.com/hashicorp/vault/plugins/database/mysql"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/database/helper/credsutil"
) )
func main() { func main() {
@@ -20,7 +19,7 @@ func main() {
// Run instantiates a MySQL object, and runs the RPC server for the plugin // Run instantiates a MySQL object, and runs the RPC server for the plugin
func Run() error { func Run() error {
var f func() (interface{}, error) var f func() (interface{}, error)
f = mysql.New(credsutil.NoneLength, mysql.LegacyMetadataLen, mysql.LegacyUsernameLen) f = mysql.New(mysql.DefaultLegacyUserNameTemplate)
dbType, err := f() dbType, err := f()
if err != nil { if err != nil {
return err return err

View File

@@ -8,11 +8,10 @@ import (
"strings" "strings"
stdmysql "github.com/go-sql-driver/mysql" stdmysql "github.com/go-sql-driver/mysql"
"github.com/hashicorp/errwrap"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/database/helper/credsutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil" "github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/helper/template"
) )
const ( const (
@@ -26,13 +25,9 @@ const (
` `
mySQLTypeName = "mysql" mySQLTypeName = "mysql"
)
var ( DefaultUserNameTemplate = `{{ printf "v-%s-%s-%s-%s" (.DisplayName | truncate 10) (.RoleName | truncate 10) (random 20) (unix_time) | truncate 32 }}`
MetadataLen int = 10 DefaultLegacyUserNameTemplate = `{{ printf "v-%s-%s-%s" (.RoleName | truncate 4) (random 20) | truncate 16 }}`
LegacyMetadataLen int = 4
UsernameLen int = 32
LegacyUsernameLen int = 16
) )
var _ dbplugin.Database = (*MySQL)(nil) var _ dbplugin.Database = (*MySQL)(nil)
@@ -40,15 +35,17 @@ var _ dbplugin.Database = (*MySQL)(nil)
type MySQL struct { type MySQL struct {
*mySQLConnectionProducer *mySQLConnectionProducer
displayNameLen int usernameProducer template.StringTemplate
roleNameLen int defaultUsernameTemplate string
maxUsernameLen int
} }
// New implements builtinplugins.BuiltinFactory // New implements builtinplugins.BuiltinFactory
func New(displayNameLen int, roleNameLen int, maxUsernameLen int) func() (interface{}, error) { func New(defaultUsernameTemplate string) func() (interface{}, error) {
return func() (interface{}, error) { return func() (interface{}, error) {
db := newMySQL(displayNameLen, roleNameLen, maxUsernameLen) if defaultUsernameTemplate == "" {
return nil, fmt.Errorf("missing default username template")
}
db := newMySQL(defaultUsernameTemplate)
// Wrap the plugin with middleware to sanitize errors // Wrap the plugin with middleware to sanitize errors
dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.SecretValues) dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.SecretValues)
@@ -56,14 +53,12 @@ func New(displayNameLen int, roleNameLen int, maxUsernameLen int) func() (interf
} }
} }
func newMySQL(displayNameLen int, roleNameLen int, maxUsernameLen int) *MySQL { func newMySQL(defaultUsernameTemplate string) *MySQL {
connProducer := &mySQLConnectionProducer{} connProducer := &mySQLConnectionProducer{}
return &MySQL{ return &MySQL{
mySQLConnectionProducer: connProducer, mySQLConnectionProducer: connProducer,
displayNameLen: displayNameLen, defaultUsernameTemplate: defaultUsernameTemplate,
roleNameLen: roleNameLen,
maxUsernameLen: maxUsernameLen,
} }
} }
@@ -81,13 +76,36 @@ func (m *MySQL) getConnection(ctx context.Context) (*sql.DB, error) {
} }
func (m *MySQL) Initialize(ctx context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) { func (m *MySQL) Initialize(ctx context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) {
err := m.mySQLConnectionProducer.Initialize(ctx, req.Config, req.VerifyConnection) usernameTemplate, err := strutil.GetString(req.Config, "username_template")
if err != nil { if err != nil {
return dbplugin.InitializeResponse{}, err return dbplugin.InitializeResponse{}, err
} }
if usernameTemplate == "" {
usernameTemplate = m.defaultUsernameTemplate
}
up, err := template.NewTemplate(template.Template(usernameTemplate))
if err != nil {
return dbplugin.InitializeResponse{}, fmt.Errorf("unable to initialize username template: %w", err)
}
m.usernameProducer = up
_, err = m.usernameProducer.Generate(dbplugin.UsernameMetadata{})
if err != nil {
return dbplugin.InitializeResponse{}, fmt.Errorf("invalid username template: %w", err)
}
err = m.mySQLConnectionProducer.Initialize(ctx, req.Config, req.VerifyConnection)
if err != nil {
return dbplugin.InitializeResponse{}, err
}
resp := dbplugin.InitializeResponse{ resp := dbplugin.InitializeResponse{
Config: req.Config, Config: req.Config,
} }
return resp, nil return resp, nil
} }
@@ -96,7 +114,7 @@ func (m *MySQL) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (dbplu
return dbplugin.NewUserResponse{}, dbutil.ErrEmptyCreationStatement return dbplugin.NewUserResponse{}, dbutil.ErrEmptyCreationStatement
} }
username, err := m.generateUsername(req) username, err := m.usernameProducer.Generate(req.UsernameConfig)
if err != nil { if err != nil {
return dbplugin.NewUserResponse{}, err return dbplugin.NewUserResponse{}, err
} }
@@ -122,19 +140,6 @@ func (m *MySQL) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (dbplu
return resp, nil return resp, nil
} }
func (m *MySQL) generateUsername(req dbplugin.NewUserRequest) (string, error) {
username, err := credsutil.GenerateUsername(
credsutil.DisplayName(req.UsernameConfig.DisplayName, m.displayNameLen),
credsutil.RoleName(req.UsernameConfig.RoleName, m.roleNameLen),
credsutil.MaxLength(m.maxUsernameLen),
)
if err != nil {
return "", errwrap.Wrapf("error generating username: {{err}}", err)
}
return username, nil
}
func (m *MySQL) DeleteUser(ctx context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { func (m *MySQL) DeleteUser(ctx context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) {
// Grab the read lock // Grab the read lock
m.Lock() m.Lock()

View File

@@ -3,6 +3,7 @@ package mysql
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -10,213 +11,465 @@ import (
stdmysql "github.com/go-sql-driver/mysql" stdmysql "github.com/go-sql-driver/mysql"
mysqlhelper "github.com/hashicorp/vault/helper/testhelpers/mysql" mysqlhelper "github.com/hashicorp/vault/helper/testhelpers/mysql"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
"github.com/hashicorp/vault/sdk/database/helper/credsutil" "github.com/hashicorp/vault/sdk/database/helper/credsutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil" "github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/stretchr/testify/require"
) )
var _ dbplugin.Database = (*MySQL)(nil) var _ dbplugin.Database = (*MySQL)(nil)
func TestMySQL_Initialize(t *testing.T) { func TestMySQL_Initialize(t *testing.T) {
cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret")
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
initReq := dbplugin.InitializeRequest{
Config: connectionDetails,
VerifyConnection: true,
}
db := newMySQL(MetadataLen, MetadataLen, UsernameLen)
_, err := db.Initialize(context.Background(), initReq)
if err != nil {
t.Fatalf("err: %s", err)
}
if !db.Initialized {
t.Fatal("Database should be initialized")
}
err = db.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
// Test decoding a string value for max_open_connections
connectionDetails = map[string]interface{}{
"connection_url": connURL,
"max_open_connections": "5",
}
initReq = dbplugin.InitializeRequest{
Config: connectionDetails,
VerifyConnection: true,
}
db = newMySQL(MetadataLen, MetadataLen, UsernameLen)
_, err = db.Initialize(context.Background(), initReq)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestMySQL_CreateUser(t *testing.T) {
t.Run("missing creation statements", func(t *testing.T) {
db := newMySQL(MetadataLen, MetadataLen, UsernameLen)
password, err := credsutil.RandomAlphaNumeric(32, false)
if err != nil {
t.Fatalf("unable to generate password: %s", err)
}
createReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "test",
RoleName: "test",
},
Statements: dbplugin.Statements{
Commands: []string{},
},
Password: password,
Expiration: time.Now().Add(time.Minute),
}
userResp, err := db.NewUser(context.Background(), createReq)
if err == nil {
t.Fatalf("expected err, got nil")
}
if userResp.Username != "" {
t.Fatalf("expected empty username, got [%s]", userResp.Username)
}
})
t.Run("non-legacy", func(t *testing.T) {
// Shared test container for speed - there should not be any overlap between the tests
cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret")
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
initReq := dbplugin.InitializeRequest{
Config: connectionDetails,
VerifyConnection: true,
}
db := newMySQL(MetadataLen, MetadataLen, UsernameLen)
_, err := db.Initialize(context.Background(), initReq)
if err != nil {
t.Fatalf("err: %s", err)
}
testCreateUser(t, db, connURL)
})
t.Run("legacy", func(t *testing.T) {
// Shared test container for speed - there should not be any overlap between the tests
cleanup, connURL := mysqlhelper.PrepareTestContainer(t, true, "secret")
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
initReq := dbplugin.InitializeRequest{
Config: connectionDetails,
VerifyConnection: true,
}
db := newMySQL(credsutil.NoneLength, LegacyMetadataLen, LegacyUsernameLen)
_, err := db.Initialize(context.Background(), initReq)
if err != nil {
t.Fatalf("err: %s", err)
}
testCreateUser(t, db, connURL)
})
}
func testCreateUser(t *testing.T, db *MySQL, connURL string) {
type testCase struct { type testCase struct {
createStmts []string rootPassword string
} }
tests := map[string]testCase{ tests := map[string]testCase{
"create name": { "non-special characters in root password": {
createStmts: []string{` rootPassword: "B44a30c4C04D0aAaE140",
CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{name}}'@'%';`,
},
}, },
"create username": { "special characters in root password": {
createStmts: []string{` rootPassword: "#secret!%25#{@}",
CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{username}}'@'%';`,
},
},
"prepared statement name": {
createStmts: []string{`
CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{name}}'@'%'");
PREPARE grantStmt from @grants;
EXECUTE grantStmt;
DEALLOCATE PREPARE grantStmt;
`,
},
},
"prepared statement username": {
createStmts: []string{`
CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';
set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{username}}'@'%'");
PREPARE grantStmt from @grants;
EXECUTE grantStmt;
DEALLOCATE PREPARE grantStmt;
`,
},
}, },
} }
for name, test := range tests { for name, test := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
password, err := credsutil.RandomAlphaNumeric(32, false) testInitialize(t, test.rootPassword)
if err != nil { })
t.Fatalf("unable to generate password: %s", err) }
} }
createReq := dbplugin.NewUserRequest{ func testInitialize(t *testing.T, rootPassword string) {
cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, rootPassword)
defer cleanup()
mySQLConfig, err := stdmysql.ParseDSN(connURL)
if err != nil {
panic(fmt.Sprintf("Test failure: connection URL is invalid: %s", err))
}
rootUser := mySQLConfig.User
mySQLConfig.User = "{{username}}"
mySQLConfig.Passwd = "{{password}}"
tmplConnURL := mySQLConfig.FormatDSN()
type testCase struct {
initRequest dbplugin.InitializeRequest
expectedResp dbplugin.InitializeResponse
expectErr bool
expectInitialized bool
}
tests := map[string]testCase{
"missing connection_url": {
initRequest: dbplugin.InitializeRequest{
Config: map[string]interface{}{},
VerifyConnection: true,
},
expectedResp: dbplugin.InitializeResponse{},
expectErr: true,
expectInitialized: false,
},
"basic config": {
initRequest: dbplugin.InitializeRequest{
Config: map[string]interface{}{
"connection_url": connURL,
},
VerifyConnection: true,
},
expectedResp: dbplugin.InitializeResponse{
Config: map[string]interface{}{
"connection_url": connURL,
},
},
expectErr: false,
expectInitialized: true,
},
"username and password replacement in connection_url": {
initRequest: dbplugin.InitializeRequest{
Config: map[string]interface{}{
"connection_url": tmplConnURL,
"username": rootUser,
"password": rootPassword,
},
VerifyConnection: true,
},
expectedResp: dbplugin.InitializeResponse{
Config: map[string]interface{}{
"connection_url": tmplConnURL,
"username": rootUser,
"password": rootPassword,
},
},
expectErr: false,
expectInitialized: true,
},
"invalid username template": {
initRequest: dbplugin.InitializeRequest{
Config: map[string]interface{}{
"connection_url": connURL,
"username_template": "{{.FieldThatDoesNotExist}}",
},
VerifyConnection: true,
},
expectedResp: dbplugin.InitializeResponse{},
expectErr: true,
expectInitialized: false,
},
"bad username template": {
initRequest: dbplugin.InitializeRequest{
Config: map[string]interface{}{
"connection_url": connURL,
"username_template": "{{ .DisplayName", // Explicitly bad template
},
VerifyConnection: true,
},
expectedResp: dbplugin.InitializeResponse{},
expectErr: true,
expectInitialized: false,
},
"custom username template": {
initRequest: dbplugin.InitializeRequest{
Config: map[string]interface{}{
"connection_url": connURL,
"username_template": "foo-{{random 10}}-{{.DisplayName}}",
},
VerifyConnection: true,
},
expectedResp: dbplugin.InitializeResponse{
Config: map[string]interface{}{
"connection_url": connURL,
"username_template": "foo-{{random 10}}-{{.DisplayName}}",
},
},
expectErr: false,
expectInitialized: true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
db := newMySQL(DefaultUserNameTemplate)
defer dbtesting.AssertClose(t, db)
initResp, err := db.Initialize(context.Background(), test.initRequest)
if test.expectErr && err == nil {
t.Fatalf("err expected, got nil")
}
if !test.expectErr && err != nil {
t.Fatalf("no error expected, got: %s", err)
}
require.Equal(t, test.expectedResp, initResp)
require.Equal(t, test.expectInitialized, db.Initialized, "Initialized variable not set correctly")
})
}
}
func TestMySQL_NewUser_nonLegacy(t *testing.T) {
displayName := "token"
roleName := "testrole"
type testCase struct {
usernameTemplate string
newUserReq dbplugin.NewUserRequest
expectedUsernameRegex string
expectErr bool
}
tests := map[string]testCase{
"name statements": {
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{ UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "test", DisplayName: displayName,
RoleName: "test", RoleName: roleName,
}, },
Statements: dbplugin.Statements{ Statements: dbplugin.Statements{
Commands: test.createStmts, Commands: []string{
`CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{name}}'@'%';`,
},
}, },
Password: password, Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute), Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^v-token-testrole-[a-zA-Z0-9]{15}$`,
expectErr: false,
},
"username statements": {
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{username}}'@'%';`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^v-token-testrole-[a-zA-Z0-9]{15}$`,
expectErr: false,
},
"prepared name statements": {
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{name}}'@'%'");
PREPARE grantStmt from @grants;
EXECUTE grantStmt;
DEALLOCATE PREPARE grantStmt;`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^v-token-testrole-[a-zA-Z0-9]{15}$`,
expectErr: false,
},
"prepared username statements": {
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';
set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{username}}'@'%'");
PREPARE grantStmt from @grants;
EXECUTE grantStmt;
DEALLOCATE PREPARE grantStmt;`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^v-token-testrole-[a-zA-Z0-9]{15}$`,
expectErr: false,
},
"custom username template": {
usernameTemplate: "foo-{{random 10}}-{{.RoleName | uppercase}}",
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{username}}'@'%';`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^foo-[a-zA-Z0-9]{10}-TESTROLE$`,
expectErr: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret")
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
"username_template": test.usernameTemplate,
} }
userResp, err := db.NewUser(context.Background(), createReq) initReq := dbplugin.InitializeRequest{
if err != nil { Config: connectionDetails,
t.Fatalf("err: %s", err) VerifyConnection: true,
} }
if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err != nil { db := newMySQL(DefaultUserNameTemplate)
t.Fatalf("Could not connect with new credentials: %s", err) defer db.Close()
_, err := db.Initialize(context.Background(), initReq)
require.NoError(t, err)
userResp, err := db.NewUser(context.Background(), test.newUserReq)
if test.expectErr && err == nil {
t.Fatalf("err expected, got nil")
}
if !test.expectErr && err != nil {
t.Fatalf("no error expected, got: %s", err)
}
require.Regexp(t, test.expectedUsernameRegex, userResp.Username)
err = mysqlhelper.TestCredsExist(t, connURL, userResp.Username, test.newUserReq.Password)
require.NoError(t, err, "Failed to connect with credentials")
})
}
}
func TestMySQL_NewUser_legacy(t *testing.T) {
displayName := "token"
roleName := "testrole"
type testCase struct {
usernameTemplate string
newUserReq dbplugin.NewUserRequest
expectedUsernameRegex string
expectErr bool
}
tests := map[string]testCase{
"name statements": {
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{name}}'@'%';`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^v-test-[a-zA-Z0-9]{9}$`,
expectErr: false,
},
"username statements": {
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{username}}'@'%';`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^v-test-[a-zA-Z0-9]{9}$`,
expectErr: false,
},
"prepared name statements": {
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{name}}'@'%'");
PREPARE grantStmt from @grants;
EXECUTE grantStmt;
DEALLOCATE PREPARE grantStmt;`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^v-test-[a-zA-Z0-9]{9}$`,
expectErr: false,
},
"prepared username statements": {
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';
set @grants=CONCAT("GRANT SELECT ON ", "*", ".* TO '{{username}}'@'%'");
PREPARE grantStmt from @grants;
EXECUTE grantStmt;
DEALLOCATE PREPARE grantStmt;`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^v-test-[a-zA-Z0-9]{9}$`,
expectErr: false,
},
"custom username template": {
usernameTemplate: `{{printf "foo-%s-%s" (random 5) (.RoleName | uppercase) | truncate 16}}`,
newUserReq: dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: displayName,
RoleName: roleName,
},
Statements: dbplugin.Statements{
Commands: []string{
`CREATE USER '{{username}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{username}}'@'%';`,
},
},
Password: "09g8hanbdfkVSM",
Expiration: time.Now().Add(time.Minute),
},
expectedUsernameRegex: `^foo-[a-zA-Z0-9]{5}-TESTRO$`,
expectErr: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret")
defer cleanup()
connectionDetails := map[string]interface{}{
"connection_url": connURL,
"username_template": test.usernameTemplate,
} }
// Test a second time to make sure usernames don't collide initReq := dbplugin.InitializeRequest{
userResp, err = db.NewUser(context.Background(), createReq) Config: connectionDetails,
if err != nil { VerifyConnection: true,
t.Fatalf("err: %s", err)
} }
if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err != nil { db := newMySQL(DefaultLegacyUserNameTemplate)
t.Fatalf("Could not connect with new credentials: %s", err) defer db.Close()
_, err := db.Initialize(context.Background(), initReq)
require.NoError(t, err)
userResp, err := db.NewUser(context.Background(), test.newUserReq)
if test.expectErr && err == nil {
t.Fatalf("err expected, got nil")
} }
if !test.expectErr && err != nil {
t.Fatalf("no error expected, got: %s", err)
}
require.Regexp(t, test.expectedUsernameRegex, userResp.Username)
err = mysqlhelper.TestCredsExist(t, connURL, userResp.Username, test.newUserReq.Password)
require.NoError(t, err, "Failed to connect with credentials")
}) })
} }
} }
@@ -260,7 +513,8 @@ func TestMySQL_RotateRootCredentials(t *testing.T) {
VerifyConnection: true, VerifyConnection: true,
} }
db := newMySQL(MetadataLen, MetadataLen, UsernameLen) db := newMySQL(DefaultUserNameTemplate)
defer db.Close()
_, err := db.Initialize(context.Background(), initReq) _, err := db.Initialize(context.Background(), initReq)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@@ -335,7 +589,8 @@ func TestMySQL_DeleteUser(t *testing.T) {
VerifyConnection: true, VerifyConnection: true,
} }
db := newMySQL(MetadataLen, MetadataLen, UsernameLen) db := newMySQL(DefaultUserNameTemplate)
defer db.Close()
_, err := db.Initialize(context.Background(), initReq) _, err := db.Initialize(context.Background(), initReq)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@@ -444,7 +699,8 @@ func TestMySQL_UpdateUser(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
db := newMySQL(MetadataLen, MetadataLen, UsernameLen) db := newMySQL(DefaultUserNameTemplate)
defer db.Close()
_, err := db.Initialize(context.Background(), initReq) _, err := db.Initialize(context.Background(), initReq)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@@ -483,35 +739,6 @@ func TestMySQL_UpdateUser(t *testing.T) {
} }
} }
func TestMySQL_Initialize_ReservedChars(t *testing.T) {
pw := "#secret!%25#{@}"
cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, pw)
defer cleanup()
// Revert password set to test replacement by db.Init
connURL = strings.ReplaceAll(connURL, pw, "{{password}}")
connectionDetails := map[string]interface{}{
"connection_url": connURL,
"password": pw,
}
db := newMySQL(MetadataLen, MetadataLen, UsernameLen)
_, err := db.Init(context.Background(), connectionDetails, true)
if err != nil {
t.Fatalf("err: %s", err)
}
if !db.Initialized {
t.Fatal("Database should be initialized")
}
err = db.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
}
func createTestMySQLUser(t *testing.T, connURL, username, password, query string) { func createTestMySQLUser(t *testing.T, connURL, username, password, query string) {
t.Helper() t.Helper()
db, err := sql.Open("mysql", connURL) db, err := sql.Open("mysql", connURL)

View File

@@ -2061,7 +2061,7 @@ func (m *mockBuiltinRegistry) Get(name string, pluginType consts.PluginType) (fu
if name == "postgresql-database-plugin" { if name == "postgresql-database-plugin" {
return dbPostgres.New, true return dbPostgres.New, true
} }
return dbMysql.New(dbMysql.MetadataLen, dbMysql.MetadataLen, dbMysql.UsernameLen), true return dbMysql.New(dbMysql.DefaultUserNameTemplate), true
} }
// Keys only supports getting a realistic list of the keys for database plugins. // Keys only supports getting a realistic list of the keys for database plugins.