mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 02:57:59 +00:00
MySQL - Add username customization (#10834)
This commit is contained in:
@@ -52,7 +52,6 @@ import (
|
||||
dbMysql "github.com/hashicorp/vault/plugins/database/mysql"
|
||||
dbPostgres "github.com/hashicorp/vault/plugins/database/postgresql"
|
||||
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/logical"
|
||||
)
|
||||
@@ -94,10 +93,10 @@ func newRegistry() *registry {
|
||||
databasePlugins: map[string]BuiltinFactory{
|
||||
// These four plugins all use the same mysql implementation but with
|
||||
// different username settings passed by the constructor.
|
||||
"mysql-database-plugin": dbMysql.New(dbMysql.MetadataLen, dbMysql.MetadataLen, dbMysql.UsernameLen),
|
||||
"mysql-aurora-database-plugin": dbMysql.New(credsutil.NoneLength, dbMysql.LegacyMetadataLen, dbMysql.LegacyUsernameLen),
|
||||
"mysql-rds-database-plugin": dbMysql.New(credsutil.NoneLength, dbMysql.LegacyMetadataLen, dbMysql.LegacyUsernameLen),
|
||||
"mysql-legacy-database-plugin": dbMysql.New(credsutil.NoneLength, dbMysql.LegacyMetadataLen, dbMysql.LegacyUsernameLen),
|
||||
"mysql-database-plugin": dbMysql.New(dbMysql.DefaultUserNameTemplate),
|
||||
"mysql-aurora-database-plugin": dbMysql.New(dbMysql.DefaultLegacyUserNameTemplate),
|
||||
"mysql-rds-database-plugin": dbMysql.New(dbMysql.DefaultLegacyUserNameTemplate),
|
||||
"mysql-legacy-database-plugin": dbMysql.New(dbMysql.DefaultLegacyUserNameTemplate),
|
||||
|
||||
"cassandra-database-plugin": dbCass.New,
|
||||
"couchbase-database-plugin": dbCouchbase.New,
|
||||
|
||||
@@ -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) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -37,7 +37,6 @@ type mySQLConnectionProducer struct {
|
||||
|
||||
RawConfig map[string]interface{}
|
||||
maxConnectionLifetime time.Duration
|
||||
Legacy bool
|
||||
Initialized bool
|
||||
db *sql.DB
|
||||
sync.Mutex
|
||||
|
||||
@@ -124,7 +124,7 @@ ssl-key=/etc/mysql/server-key.pem`
|
||||
|
||||
// //////////////////////////////////////////////////////
|
||||
// Test
|
||||
mysql := newMySQL(MetadataLen, MetadataLen, UsernameLen)
|
||||
mysql := newMySQL(DefaultUserNameTemplate)
|
||||
|
||||
conf := map[string]interface{}{
|
||||
"connection_url": retURL,
|
||||
|
||||
@@ -19,7 +19,7 @@ func main() {
|
||||
// Run instantiates a MySQL object, and runs the RPC server for the plugin
|
||||
func Run() error {
|
||||
var f func() (interface{}, error)
|
||||
f = mysql.New(mysql.MetadataLen, mysql.MetadataLen, mysql.UsernameLen)
|
||||
f = mysql.New(mysql.DefaultUserNameTemplate)
|
||||
dbType, err := f()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/hashicorp/vault/plugins/database/mysql"
|
||||
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
||||
"github.com/hashicorp/vault/sdk/database/helper/credsutil"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -20,7 +19,7 @@ func main() {
|
||||
// Run instantiates a MySQL object, and runs the RPC server for the plugin
|
||||
func Run() error {
|
||||
var f func() (interface{}, error)
|
||||
f = mysql.New(credsutil.NoneLength, mysql.LegacyMetadataLen, mysql.LegacyUsernameLen)
|
||||
f = mysql.New(mysql.DefaultLegacyUserNameTemplate)
|
||||
dbType, err := f()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -8,11 +8,10 @@ import (
|
||||
"strings"
|
||||
|
||||
stdmysql "github.com/go-sql-driver/mysql"
|
||||
"github.com/hashicorp/errwrap"
|
||||
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/helper/strutil"
|
||||
"github.com/hashicorp/vault/sdk/helper/template"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,13 +25,9 @@ const (
|
||||
`
|
||||
|
||||
mySQLTypeName = "mysql"
|
||||
)
|
||||
|
||||
var (
|
||||
MetadataLen int = 10
|
||||
LegacyMetadataLen int = 4
|
||||
UsernameLen int = 32
|
||||
LegacyUsernameLen int = 16
|
||||
DefaultUserNameTemplate = `{{ printf "v-%s-%s-%s-%s" (.DisplayName | truncate 10) (.RoleName | truncate 10) (random 20) (unix_time) | truncate 32 }}`
|
||||
DefaultLegacyUserNameTemplate = `{{ printf "v-%s-%s-%s" (.RoleName | truncate 4) (random 20) | truncate 16 }}`
|
||||
)
|
||||
|
||||
var _ dbplugin.Database = (*MySQL)(nil)
|
||||
@@ -40,15 +35,17 @@ var _ dbplugin.Database = (*MySQL)(nil)
|
||||
type MySQL struct {
|
||||
*mySQLConnectionProducer
|
||||
|
||||
displayNameLen int
|
||||
roleNameLen int
|
||||
maxUsernameLen int
|
||||
usernameProducer template.StringTemplate
|
||||
defaultUsernameTemplate string
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
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{}
|
||||
|
||||
return &MySQL{
|
||||
mySQLConnectionProducer: connProducer,
|
||||
displayNameLen: displayNameLen,
|
||||
roleNameLen: roleNameLen,
|
||||
maxUsernameLen: maxUsernameLen,
|
||||
defaultUsernameTemplate: defaultUsernameTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
err := m.mySQLConnectionProducer.Initialize(ctx, req.Config, req.VerifyConnection)
|
||||
usernameTemplate, err := strutil.GetString(req.Config, "username_template")
|
||||
if err != nil {
|
||||
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{
|
||||
Config: req.Config,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -96,7 +114,7 @@ func (m *MySQL) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (dbplu
|
||||
return dbplugin.NewUserResponse{}, dbutil.ErrEmptyCreationStatement
|
||||
}
|
||||
|
||||
username, err := m.generateUsername(req)
|
||||
username, err := m.usernameProducer.Generate(req.UsernameConfig)
|
||||
if err != nil {
|
||||
return dbplugin.NewUserResponse{}, err
|
||||
}
|
||||
@@ -122,19 +140,6 @@ func (m *MySQL) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (dbplu
|
||||
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) {
|
||||
// Grab the read lock
|
||||
m.Lock()
|
||||
|
||||
@@ -3,6 +3,7 @@ package mysql
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -10,213 +11,465 @@ import (
|
||||
stdmysql "github.com/go-sql-driver/mysql"
|
||||
mysqlhelper "github.com/hashicorp/vault/helper/testhelpers/mysql"
|
||||
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/dbutil"
|
||||
"github.com/hashicorp/vault/sdk/helper/strutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var _ dbplugin.Database = (*MySQL)(nil)
|
||||
|
||||
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 {
|
||||
createStmts []string
|
||||
rootPassword string
|
||||
}
|
||||
|
||||
tests := map[string]testCase{
|
||||
"create name": {
|
||||
createStmts: []string{`
|
||||
CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
|
||||
GRANT SELECT ON *.* TO '{{name}}'@'%';`,
|
||||
},
|
||||
},
|
||||
"create username": {
|
||||
createStmts: []string{`
|
||||
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;
|
||||
`,
|
||||
"non-special characters in root password": {
|
||||
rootPassword: "B44a30c4C04D0aAaE140",
|
||||
},
|
||||
"special characters in root password": {
|
||||
rootPassword: "#secret!%25#{@}",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
password, err := credsutil.RandomAlphaNumeric(32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate password: %s", err)
|
||||
testInitialize(t, test.rootPassword)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
DisplayName: "test",
|
||||
RoleName: "test",
|
||||
DisplayName: displayName,
|
||||
RoleName: roleName,
|
||||
},
|
||||
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),
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
userResp, err := db.NewUser(context.Background(), createReq)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
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,
|
||||
}
|
||||
|
||||
if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err != nil {
|
||||
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||
initReq := dbplugin.InitializeRequest{
|
||||
Config: connectionDetails,
|
||||
VerifyConnection: true,
|
||||
}
|
||||
|
||||
// Test a second time to make sure usernames don't collide
|
||||
userResp, err = db.NewUser(context.Background(), createReq)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
db := newMySQL(DefaultUserNameTemplate)
|
||||
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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err != nil {
|
||||
t.Fatalf("Could not connect with new credentials: %s", err)
|
||||
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,
|
||||
}
|
||||
|
||||
initReq := dbplugin.InitializeRequest{
|
||||
Config: connectionDetails,
|
||||
VerifyConnection: true,
|
||||
}
|
||||
|
||||
db := newMySQL(DefaultLegacyUserNameTemplate)
|
||||
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,
|
||||
}
|
||||
|
||||
db := newMySQL(MetadataLen, MetadataLen, UsernameLen)
|
||||
db := newMySQL(DefaultUserNameTemplate)
|
||||
defer db.Close()
|
||||
_, err := db.Initialize(context.Background(), initReq)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -335,7 +589,8 @@ func TestMySQL_DeleteUser(t *testing.T) {
|
||||
VerifyConnection: true,
|
||||
}
|
||||
|
||||
db := newMySQL(MetadataLen, MetadataLen, UsernameLen)
|
||||
db := newMySQL(DefaultUserNameTemplate)
|
||||
defer db.Close()
|
||||
_, err := db.Initialize(context.Background(), initReq)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -444,7 +699,8 @@ func TestMySQL_UpdateUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
db := newMySQL(MetadataLen, MetadataLen, UsernameLen)
|
||||
db := newMySQL(DefaultUserNameTemplate)
|
||||
defer db.Close()
|
||||
_, err := db.Initialize(context.Background(), initReq)
|
||||
if err != nil {
|
||||
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) {
|
||||
t.Helper()
|
||||
db, err := sql.Open("mysql", connURL)
|
||||
|
||||
@@ -2061,7 +2061,7 @@ func (m *mockBuiltinRegistry) Get(name string, pluginType consts.PluginType) (fu
|
||||
if name == "postgresql-database-plugin" {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user