mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	Migrate MySQL db to v5 database engine (#10110)
* Migrate MySQL db to v5 database engine
This commit is contained in:
		| @@ -37,6 +37,7 @@ type mySQLConnectionProducer struct { | ||||
|  | ||||
| 	RawConfig             map[string]interface{} | ||||
| 	maxConnectionLifetime time.Duration | ||||
| 	Legacy                bool | ||||
| 	Initialized           bool | ||||
| 	db                    *sql.DB | ||||
| 	sync.Mutex | ||||
| @@ -158,8 +159,8 @@ func (c *mySQLConnectionProducer) Connection(ctx context.Context) (interface{}, | ||||
| 	return c.db, nil | ||||
| } | ||||
|  | ||||
| func (c *mySQLConnectionProducer) SecretValues() map[string]interface{} { | ||||
| 	return map[string]interface{}{ | ||||
| func (c *mySQLConnectionProducer) SecretValues() map[string]string { | ||||
| 	return map[string]string{ | ||||
| 		c.Password: "[password]", | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -124,7 +124,7 @@ ssl-key=/etc/mysql/server-key.pem` | ||||
|  | ||||
| 	// ////////////////////////////////////////////////////// | ||||
| 	// Test | ||||
| 	mysql := new(25, 25, 25) | ||||
| 	mysql := new(false) | ||||
|  | ||||
| 	conf := map[string]interface{}{ | ||||
| 		"connection_url":      retURL, | ||||
|   | ||||
| @@ -4,15 +4,17 @@ import ( | ||||
| 	"context" | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	stdmysql "github.com/go-sql-driver/mysql" | ||||
| 	"github.com/hashicorp/errwrap" | ||||
| 	"github.com/hashicorp/vault/api" | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/credsutil" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/dbutil" | ||||
| 	"github.com/hashicorp/vault/sdk/database/newdbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/strutil" | ||||
|  | ||||
| 	stdmysql "github.com/go-sql-driver/mysql" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -35,37 +37,30 @@ var ( | ||||
| 	LegacyUsernameLen int = 16 | ||||
| ) | ||||
|  | ||||
| var _ dbplugin.Database = (*MySQL)(nil) | ||||
| var _ newdbplugin.Database = (*MySQL)(nil) | ||||
|  | ||||
| type MySQL struct { | ||||
| 	*mySQLConnectionProducer | ||||
| 	credsutil.CredentialsProducer | ||||
| 	legacy bool | ||||
| } | ||||
|  | ||||
| // New implements builtinplugins.BuiltinFactory | ||||
| func New(displayNameLen, roleNameLen, usernameLen int) func() (interface{}, error) { | ||||
| func New(legacy bool) func() (interface{}, error) { | ||||
| 	return func() (interface{}, error) { | ||||
| 		db := new(displayNameLen, roleNameLen, usernameLen) | ||||
| 		db := new(legacy) | ||||
| 		// Wrap the plugin with middleware to sanitize errors | ||||
| 		dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.SecretValues) | ||||
| 		dbType := newdbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.SecretValues) | ||||
|  | ||||
| 		return dbType, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func new(displayNameLen, roleNameLen, usernameLen int) *MySQL { | ||||
| func new(legacy bool) *MySQL { | ||||
| 	connProducer := &mySQLConnectionProducer{} | ||||
|  | ||||
| 	credsProducer := &credsutil.SQLCredentialsProducer{ | ||||
| 		DisplayNameLen: displayNameLen, | ||||
| 		RoleNameLen:    roleNameLen, | ||||
| 		UsernameLen:    usernameLen, | ||||
| 		Separator:      "-", | ||||
| 	} | ||||
|  | ||||
| 	return &MySQL{ | ||||
| 		mySQLConnectionProducer: connProducer, | ||||
| 		CredentialsProducer:     credsProducer, | ||||
| 		legacy:                  legacy, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -81,17 +76,13 @@ func RunLegacy(apiTLSConfig *api.TLSConfig) error { | ||||
|  | ||||
| func runCommon(legacy bool, apiTLSConfig *api.TLSConfig) error { | ||||
| 	var f func() (interface{}, error) | ||||
| 	if legacy { | ||||
| 		f = New(credsutil.NoneLength, LegacyMetadataLen, LegacyUsernameLen) | ||||
| 	} else { | ||||
| 		f = New(MetadataLen, MetadataLen, UsernameLen) | ||||
| 	} | ||||
| 	f = New(legacy) | ||||
| 	dbType, err := f() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	dbplugin.Serve(dbType.(dbplugin.Database), api.VaultPluginTLSProvider(apiTLSConfig)) | ||||
| 	newdbplugin.Serve(dbType.(newdbplugin.Database), api.VaultPluginTLSProvider(apiTLSConfig)) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -109,27 +100,30 @@ func (m *MySQL) getConnection(ctx context.Context) (*sql.DB, error) { | ||||
| 	return db.(*sql.DB), nil | ||||
| } | ||||
|  | ||||
| func (m *MySQL) CreateUser(ctx context.Context, statements dbplugin.Statements, usernameConfig dbplugin.UsernameConfig, expiration time.Time) (username string, password string, err error) { | ||||
| 	statements = dbutil.StatementCompatibilityHelper(statements) | ||||
|  | ||||
| 	if len(statements.Creation) == 0 { | ||||
| 		return "", "", dbutil.ErrEmptyCreationStatement | ||||
| 	} | ||||
|  | ||||
| 	username, err = m.GenerateUsername(usernameConfig) | ||||
| func (m *MySQL) Initialize(ctx context.Context, req newdbplugin.InitializeRequest) (newdbplugin.InitializeResponse, error) { | ||||
| 	err := m.mySQLConnectionProducer.Initialize(ctx, req.Config, req.VerifyConnection) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 		return newdbplugin.InitializeResponse{}, err | ||||
| 	} | ||||
| 	resp := newdbplugin.InitializeResponse{ | ||||
| 		Config: req.Config, | ||||
| 	} | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| func (m *MySQL) NewUser(ctx context.Context, req newdbplugin.NewUserRequest) (newdbplugin.NewUserResponse, error) { | ||||
| 	if len(req.Statements.Commands) == 0 { | ||||
| 		return newdbplugin.NewUserResponse{}, dbutil.ErrEmptyCreationStatement | ||||
| 	} | ||||
|  | ||||
| 	password, err = m.GeneratePassword() | ||||
| 	username, err := m.generateUsername(req) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 		return newdbplugin.NewUserResponse{}, err | ||||
| 	} | ||||
|  | ||||
| 	expirationStr, err := m.GenerateExpiration(expiration) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 	password := req.Password | ||||
|  | ||||
| 	expirationStr := req.Expiration.Format("2006-01-02 15:04:05-0700") | ||||
|  | ||||
| 	queryMap := map[string]string{ | ||||
| 		"name":       username, | ||||
| @@ -138,31 +132,53 @@ func (m *MySQL) CreateUser(ctx context.Context, statements dbplugin.Statements, | ||||
| 		"expiration": expirationStr, | ||||
| 	} | ||||
|  | ||||
| 	if err := m.executePreparedStatmentsWithMap(ctx, statements.Creation, queryMap); err != nil { | ||||
| 		return "", "", err | ||||
| 	if err := m.executePreparedStatementsWithMap(ctx, req.Statements.Commands, queryMap); err != nil { | ||||
| 		return newdbplugin.NewUserResponse{}, err | ||||
| 	} | ||||
| 	return username, password, nil | ||||
|  | ||||
| 	resp := newdbplugin.NewUserResponse{ | ||||
| 		Username: username, | ||||
| 	} | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| // NOOP | ||||
| func (m *MySQL) RenewUser(ctx context.Context, statements dbplugin.Statements, username string, expiration time.Time) error { | ||||
| 	return nil | ||||
| func (m *MySQL) generateUsername(req newdbplugin.NewUserRequest) (string, error) { | ||||
| 	var dispNameLen, roleNameLen, maxLen int | ||||
|  | ||||
| 	if m.legacy { | ||||
| 		dispNameLen = LegacyUsernameLen | ||||
| 		roleNameLen = LegacyMetadataLen | ||||
| 		maxLen = LegacyUsernameLen | ||||
| 	} else { | ||||
| 		dispNameLen = UsernameLen | ||||
| 		roleNameLen = MetadataLen | ||||
| 		maxLen = UsernameLen | ||||
| 	} | ||||
|  | ||||
| 	username, err := credsutil.GenerateUsername( | ||||
| 		credsutil.DisplayName(req.UsernameConfig.DisplayName, dispNameLen), | ||||
| 		credsutil.RoleName(req.UsernameConfig.RoleName, roleNameLen), | ||||
| 		credsutil.MaxLength(maxLen), | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return "", errwrap.Wrapf("error generating username: {{err}}", err) | ||||
| 	} | ||||
|  | ||||
| 	return username, nil | ||||
| } | ||||
|  | ||||
| func (m *MySQL) RevokeUser(ctx context.Context, statements dbplugin.Statements, username string) error { | ||||
| func (m *MySQL) DeleteUser(ctx context.Context, req newdbplugin.DeleteUserRequest) (newdbplugin.DeleteUserResponse, error) { | ||||
| 	// Grab the read lock | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
|  | ||||
| 	statements = dbutil.StatementCompatibilityHelper(statements) | ||||
|  | ||||
| 	// Get the connection | ||||
| 	db, err := m.getConnection(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return newdbplugin.DeleteUserResponse{}, err | ||||
| 	} | ||||
|  | ||||
| 	revocationStmts := statements.Revocation | ||||
| 	revocationStmts := req.Statements.Commands | ||||
| 	// Use a default SQL statement for revocation if one cannot be fetched from the role | ||||
| 	if len(revocationStmts) == 0 { | ||||
| 		revocationStmts = []string{defaultMysqlRevocationStmts} | ||||
| @@ -171,7 +187,7 @@ func (m *MySQL) RevokeUser(ctx context.Context, statements dbplugin.Statements, | ||||
| 	// Start a transaction | ||||
| 	tx, err := db.BeginTx(ctx, nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return newdbplugin.DeleteUserResponse{}, err | ||||
| 	} | ||||
| 	defer tx.Rollback() | ||||
|  | ||||
| @@ -185,101 +201,44 @@ func (m *MySQL) RevokeUser(ctx context.Context, statements dbplugin.Statements, | ||||
| 			// This is not a prepared statement because not all commands are supported | ||||
| 			// 1295: This command is not supported in the prepared statement protocol yet | ||||
| 			// Reference https://mariadb.com/kb/en/mariadb/prepare-statement/ | ||||
| 			query = strings.Replace(query, "{{name}}", username, -1) | ||||
| 			query = strings.Replace(query, "{{username}}", username, -1) | ||||
| 			query = strings.Replace(query, "{{name}}", req.Username, -1) | ||||
| 			query = strings.Replace(query, "{{username}}", req.Username, -1) | ||||
| 			_, err = tx.ExecContext(ctx, query) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 				return newdbplugin.DeleteUserResponse{}, err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Commit the transaction | ||||
| 	if err := tx.Commit(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	err = tx.Commit() | ||||
| 	return newdbplugin.DeleteUserResponse{}, err | ||||
| } | ||||
|  | ||||
| func (m *MySQL) RotateRootCredentials(ctx context.Context, statements []string) (map[string]interface{}, error) { | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
|  | ||||
| 	if len(m.Username) == 0 || len(m.Password) == 0 { | ||||
| 		return nil, errors.New("username and password are required to rotate") | ||||
| func (m *MySQL) UpdateUser(ctx context.Context, req newdbplugin.UpdateUserRequest) (newdbplugin.UpdateUserResponse, error) { | ||||
| 	if req.Password == nil && req.Expiration == nil { | ||||
| 		return newdbplugin.UpdateUserResponse{}, fmt.Errorf("no change requested") | ||||
| 	} | ||||
|  | ||||
| 	rotateStatements := statements | ||||
| 	if len(rotateStatements) == 0 { | ||||
| 		rotateStatements = []string{defaultMySQLRotateCredentialsSQL} | ||||
| 	} | ||||
|  | ||||
| 	db, err := m.getConnection(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	tx, err := db.BeginTx(ctx, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		tx.Rollback() | ||||
| 	}() | ||||
|  | ||||
| 	password, err := m.GeneratePassword() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for _, stmt := range rotateStatements { | ||||
| 		for _, query := range strutil.ParseArbitraryStringSlice(stmt, ";") { | ||||
| 			query = strings.TrimSpace(query) | ||||
| 			if len(query) == 0 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// This is not a prepared statement because not all commands are supported | ||||
| 			// 1295: This command is not supported in the prepared statement protocol yet | ||||
| 			// Reference https://mariadb.com/kb/en/mariadb/prepare-statement/ | ||||
| 			query = strings.Replace(query, "{{username}}", m.Username, -1) | ||||
| 			query = strings.Replace(query, "{{name}}", m.Username, -1) | ||||
| 			query = strings.Replace(query, "{{password}}", password, -1) | ||||
|  | ||||
| 			if _, err := tx.ExecContext(ctx, query); err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 	if req.Password != nil { | ||||
| 		err := m.changeUserPassword(ctx, req.Username, req.Password.NewPassword, req.Password.Statements.Commands) | ||||
| 		if err != nil { | ||||
| 			return newdbplugin.UpdateUserResponse{}, fmt.Errorf("failed to change password: %w", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := tx.Commit(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// Expiration change/update is currently a no-op | ||||
|  | ||||
| 	if err := db.Close(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	m.RawConfig["password"] = password | ||||
| 	return m.RawConfig, nil | ||||
| 	return newdbplugin.UpdateUserResponse{}, nil | ||||
| } | ||||
|  | ||||
| // SetCredentials uses provided information to set the password to a user in the | ||||
| // database. Unlike CreateUser, this method requires a username be provided and | ||||
| // uses the name given, instead of generating a name. This is used for setting | ||||
| // the password of static accounts, as well as rolling back passwords in the | ||||
| // database in the event an updated database fails to save in Vault's storage. | ||||
| func (m *MySQL) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { | ||||
| 	rotateStatements := statements.Rotation | ||||
| 	if len(rotateStatements) == 0 { | ||||
| 		rotateStatements = []string{defaultMySQLRotateCredentialsSQL} | ||||
| func (m *MySQL) changeUserPassword(ctx context.Context, username, password string, rotateStatements []string) error { | ||||
| 	if username == "" || password == "" { | ||||
| 		return errors.New("must provide both username and password") | ||||
| 	} | ||||
|  | ||||
| 	username = staticUser.Username | ||||
| 	password = staticUser.Password | ||||
| 	if username == "" || password == "" { | ||||
| 		return "", "", errors.New("must provide both username and password") | ||||
| 	if len(rotateStatements) == 0 { | ||||
| 		rotateStatements = []string{defaultMySQLRotateCredentialsSQL} | ||||
| 	} | ||||
|  | ||||
| 	queryMap := map[string]string{ | ||||
| @@ -288,16 +247,16 @@ func (m *MySQL) SetCredentials(ctx context.Context, statements dbplugin.Statemen | ||||
| 		"password": password, | ||||
| 	} | ||||
|  | ||||
| 	if err := m.executePreparedStatmentsWithMap(ctx, rotateStatements, queryMap); err != nil { | ||||
| 		return "", "", err | ||||
| 	if err := m.executePreparedStatementsWithMap(ctx, rotateStatements, queryMap); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return username, password, nil | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // executePreparedStatmentsWithMap loops through the given templated SQL statements and | ||||
| // executePreparedStatementsWithMap loops through the given templated SQL statements and | ||||
| // applies the map to them, interpolating values into the templates, returning | ||||
| // the resulting username and password | ||||
| func (m *MySQL) executePreparedStatmentsWithMap(ctx context.Context, statements []string, queryMap map[string]string) error { | ||||
| func (m *MySQL) executePreparedStatementsWithMap(ctx context.Context, statements []string, queryMap map[string]string) error { | ||||
| 	// Grab the lock | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
|   | ||||
| @@ -9,13 +9,14 @@ import ( | ||||
|  | ||||
| 	stdmysql "github.com/go-sql-driver/mysql" | ||||
| 	mysqlhelper "github.com/hashicorp/vault/helper/testhelpers/mysql" | ||||
| 	"github.com/hashicorp/vault/sdk/database/dbplugin" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/credsutil" | ||||
| 	"github.com/hashicorp/vault/sdk/database/helper/dbutil" | ||||
| 	"github.com/hashicorp/vault/sdk/database/newdbplugin" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/strutil" | ||||
| ) | ||||
|  | ||||
| var _ dbplugin.Database = (*MySQL)(nil) | ||||
| var _ newdbplugin.Database = (*MySQL)(nil) | ||||
|  | ||||
| func TestMySQL_Initialize(t *testing.T) { | ||||
| 	cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret") | ||||
| @@ -25,8 +26,13 @@ func TestMySQL_Initialize(t *testing.T) { | ||||
| 		"connection_url": connURL, | ||||
| 	} | ||||
|  | ||||
| 	db := new(MetadataLen, MetadataLen, UsernameLen) | ||||
| 	_, err := db.Init(context.Background(), connectionDetails, true) | ||||
| 	initReq := newdbplugin.InitializeRequest{ | ||||
| 		Config:           connectionDetails, | ||||
| 		VerifyConnection: true, | ||||
| 	} | ||||
|  | ||||
| 	db := new(false) | ||||
| 	_, err := db.Initialize(context.Background(), initReq) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
| @@ -46,7 +52,13 @@ func TestMySQL_Initialize(t *testing.T) { | ||||
| 		"max_open_connections": "5", | ||||
| 	} | ||||
|  | ||||
| 	_, err = db.Init(context.Background(), connectionDetails, true) | ||||
| 	initReq = newdbplugin.InitializeRequest{ | ||||
| 		Config:           connectionDetails, | ||||
| 		VerifyConnection: true, | ||||
| 	} | ||||
|  | ||||
| 	db = new(false) | ||||
| 	_, err = db.Initialize(context.Background(), initReq) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
| @@ -54,22 +66,31 @@ func TestMySQL_Initialize(t *testing.T) { | ||||
|  | ||||
| func TestMySQL_CreateUser(t *testing.T) { | ||||
| 	t.Run("missing creation statements", func(t *testing.T) { | ||||
| 		db := new(MetadataLen, MetadataLen, UsernameLen) | ||||
| 		db := new(false) | ||||
|  | ||||
| 		usernameConfig := dbplugin.UsernameConfig{ | ||||
| 			DisplayName: "test-long-displayname", | ||||
| 			RoleName:    "test-long-rolename", | ||||
| 		password, err := credsutil.RandomAlphaNumeric(32, false) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("unable to generate password: %s", err) | ||||
| 		} | ||||
|  | ||||
| 		username, password, err := db.CreateUser(context.Background(), dbplugin.Statements{}, usernameConfig, time.Now().Add(time.Minute)) | ||||
| 		createReq := newdbplugin.NewUserRequest{ | ||||
| 			UsernameConfig: newdbplugin.UsernameMetadata{ | ||||
| 				DisplayName: "test", | ||||
| 				RoleName:    "test", | ||||
| 			}, | ||||
| 			Statements: newdbplugin.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 username != "" { | ||||
| 			t.Fatalf("expected empty username, got [%s]", username) | ||||
| 		} | ||||
| 		if password != "" { | ||||
| 			t.Fatalf("expected empty password, got [%s]", password) | ||||
| 		if userResp.Username != "" { | ||||
| 			t.Fatalf("expected empty username, got [%s]", userResp.Username) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| @@ -82,8 +103,13 @@ func TestMySQL_CreateUser(t *testing.T) { | ||||
| 			"connection_url": connURL, | ||||
| 		} | ||||
|  | ||||
| 		db := new(MetadataLen, MetadataLen, UsernameLen) | ||||
| 		_, err := db.Init(context.Background(), connectionDetails, true) | ||||
| 		initReq := newdbplugin.InitializeRequest{ | ||||
| 			Config:           connectionDetails, | ||||
| 			VerifyConnection: true, | ||||
| 		} | ||||
|  | ||||
| 		db := new(false) | ||||
| 		_, err := db.Initialize(context.Background(), initReq) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("err: %s", err) | ||||
| 		} | ||||
| @@ -100,8 +126,13 @@ func TestMySQL_CreateUser(t *testing.T) { | ||||
| 			"connection_url": connURL, | ||||
| 		} | ||||
|  | ||||
| 		db := new(credsutil.NoneLength, LegacyMetadataLen, LegacyUsernameLen) | ||||
| 		_, err := db.Init(context.Background(), connectionDetails, true) | ||||
| 		initReq := newdbplugin.InitializeRequest{ | ||||
| 			Config:           connectionDetails, | ||||
| 			VerifyConnection: true, | ||||
| 		} | ||||
|  | ||||
| 		db := new(true) | ||||
| 		_, err := db.Initialize(context.Background(), initReq) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("err: %s", err) | ||||
| 		} | ||||
| @@ -152,31 +183,39 @@ func testCreateUser(t *testing.T, db *MySQL, connURL string) { | ||||
|  | ||||
| 	for name, test := range tests { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			usernameConfig := dbplugin.UsernameConfig{ | ||||
| 				DisplayName: "test-long-displayname", | ||||
| 				RoleName:    "test-long-rolename", | ||||
| 			password, err := credsutil.RandomAlphaNumeric(32, false) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("unable to generate password: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			statements := dbplugin.Statements{ | ||||
| 				Creation: test.createStmts, | ||||
| 			createReq := newdbplugin.NewUserRequest{ | ||||
| 				UsernameConfig: newdbplugin.UsernameMetadata{ | ||||
| 					DisplayName: "test", | ||||
| 					RoleName:    "test", | ||||
| 				}, | ||||
| 				Statements: newdbplugin.Statements{ | ||||
| 					Commands: test.createStmts, | ||||
| 				}, | ||||
| 				Password:   password, | ||||
| 				Expiration: time.Now().Add(time.Minute), | ||||
| 			} | ||||
|  | ||||
| 			username, password, err := db.CreateUser(context.Background(), statements, usernameConfig, time.Now().Add(time.Minute)) | ||||
| 			userResp, err := db.NewUser(context.Background(), createReq) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("err: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, username, password); err != nil { | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err != nil { | ||||
| 				t.Fatalf("Could not connect with new credentials: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			// Test a second time to make sure usernames don't collide | ||||
| 			username, password, err = db.CreateUser(context.Background(), statements, usernameConfig, time.Now().Add(time.Minute)) | ||||
| 			userResp, err = db.NewUser(context.Background(), createReq) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("err: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, username, password); err != nil { | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err != nil { | ||||
| 				t.Fatalf("Could not connect with new credentials: %s", err) | ||||
| 			} | ||||
| 		}) | ||||
| @@ -207,8 +246,6 @@ func TestMySQL_RotateRootCredentials(t *testing.T) { | ||||
| 			cleanup, connURL := mysqlhelper.PrepareTestContainer(t, false, "secret") | ||||
| 			defer cleanup() | ||||
|  | ||||
| 			connURL = strings.Replace(connURL, "root:secret", `{{username}}:{{password}}`, -1) | ||||
|  | ||||
| 			connectionDetails := map[string]interface{}{ | ||||
| 				"connection_url": connURL, | ||||
| 				"username":       "root", | ||||
| @@ -219,8 +256,13 @@ func TestMySQL_RotateRootCredentials(t *testing.T) { | ||||
| 			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||
| 			defer cancel() | ||||
|  | ||||
| 			db := new(MetadataLen, MetadataLen, UsernameLen) | ||||
| 			_, err := db.Init(ctx, connectionDetails, true) | ||||
| 			initReq := newdbplugin.InitializeRequest{ | ||||
| 				Config:           connectionDetails, | ||||
| 				VerifyConnection: true, | ||||
| 			} | ||||
|  | ||||
| 			db := new(false) | ||||
| 			_, err := db.Initialize(context.Background(), initReq) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("err: %s", err) | ||||
| 			} | ||||
| @@ -229,12 +271,28 @@ func TestMySQL_RotateRootCredentials(t *testing.T) { | ||||
| 				t.Fatal("Database should be initialized") | ||||
| 			} | ||||
|  | ||||
| 			newConf, err := db.RotateRootCredentials(ctx, test.statements) | ||||
| 			updateReq := newdbplugin.UpdateUserRequest{ | ||||
| 				Username: "root", | ||||
| 				Password: &newdbplugin.ChangePassword{ | ||||
| 					NewPassword: "different_sercret", | ||||
| 					Statements: newdbplugin.Statements{ | ||||
| 						Commands: test.statements, | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			_, err = db.UpdateUser(ctx, updateReq) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("err: %v", err) | ||||
| 			} | ||||
| 			if newConf["password"] == "secret" { | ||||
| 				t.Fatal("password was not updated") | ||||
| 			err = mysqlhelper.TestCredsExist(t, connURL, updateReq.Username, updateReq.Password.NewPassword) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Could not connect with new credentials: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			// verify old password doesn't work | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, updateReq.Username, "secret"); err == nil { | ||||
| 				t.Fatalf("Should not be able to connect with initial credentials") | ||||
| 			} | ||||
|  | ||||
| 			err = db.Close() | ||||
| @@ -245,7 +303,7 @@ func TestMySQL_RotateRootCredentials(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMySQL_RevokeUser(t *testing.T) { | ||||
| func TestMySQL_DeleteUser(t *testing.T) { | ||||
| 	type testCase struct { | ||||
| 		revokeStmts []string | ||||
| 	} | ||||
| @@ -273,57 +331,71 @@ func TestMySQL_RevokeUser(t *testing.T) { | ||||
| 		"connection_url": connURL, | ||||
| 	} | ||||
|  | ||||
| 	// Give a timeout just in case the test decides to be problematic | ||||
| 	initCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||
| 	defer cancel() | ||||
| 	initReq := newdbplugin.InitializeRequest{ | ||||
| 		Config:           connectionDetails, | ||||
| 		VerifyConnection: true, | ||||
| 	} | ||||
|  | ||||
| 	db := new(MetadataLen, MetadataLen, UsernameLen) | ||||
| 	_, err := db.Init(initCtx, connectionDetails, true) | ||||
| 	db := new(false) | ||||
| 	_, err := db.Initialize(context.Background(), initReq) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	for name, test := range tests { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			statements := dbplugin.Statements{ | ||||
| 				Creation: []string{` | ||||
| 					CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; | ||||
| 					GRANT SELECT ON *.* TO '{{name}}'@'%';`, | ||||
| 				}, | ||||
| 				Revocation: test.revokeStmts, | ||||
| 			password, err := credsutil.RandomAlphaNumeric(32, false) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("unable to generate password: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			usernameConfig := dbplugin.UsernameConfig{ | ||||
| 				DisplayName: "test", | ||||
| 				RoleName:    "test", | ||||
| 			createReq := newdbplugin.NewUserRequest{ | ||||
| 				UsernameConfig: newdbplugin.UsernameMetadata{ | ||||
| 					DisplayName: "test", | ||||
| 					RoleName:    "test", | ||||
| 				}, | ||||
| 				Statements: newdbplugin.Statements{ | ||||
| 					Commands: []string{` | ||||
| 						CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; | ||||
| 						GRANT SELECT ON *.* TO '{{name}}'@'%';`, | ||||
| 					}, | ||||
| 				}, | ||||
| 				Password:   password, | ||||
| 				Expiration: time.Now().Add(time.Minute), | ||||
| 			} | ||||
|  | ||||
| 			// Give a timeout just in case the test decides to be problematic | ||||
| 			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||
| 			defer cancel() | ||||
|  | ||||
| 			username, password, err := db.CreateUser(ctx, statements, usernameConfig, time.Now().Add(time.Minute)) | ||||
| 			userResp, err := db.NewUser(ctx, createReq) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("err: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, username, password); err != nil { | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err != nil { | ||||
| 				t.Fatalf("Could not connect with new credentials: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			err = db.RevokeUser(context.Background(), statements, username) | ||||
| 			deleteReq := newdbplugin.DeleteUserRequest{ | ||||
| 				Username: userResp.Username, | ||||
| 				Statements: newdbplugin.Statements{ | ||||
| 					Commands: test.revokeStmts, | ||||
| 				}, | ||||
| 			} | ||||
| 			_, err = db.DeleteUser(context.Background(), deleteReq) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("err: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, username, password); err == nil { | ||||
| 				t.Fatal("Credentials were not revoked") | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, userResp.Username, password); err == nil { | ||||
| 				t.Fatalf("Credentials were not revoked!") | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMySQL_SetCredentials(t *testing.T) { | ||||
| func TestMySQL_UpdateUser(t *testing.T) { | ||||
| 	type testCase struct { | ||||
| 		rotateStmts []string | ||||
| 	} | ||||
| @@ -356,7 +428,7 @@ func TestMySQL_SetCredentials(t *testing.T) { | ||||
| 				GRANT SELECT ON *.* TO '{{name}}'@'%';` | ||||
|  | ||||
| 			createTestMySQLUser(t, connURL, dbUser, initPassword, createStatements) | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, dbUser, "password"); err != nil { | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, dbUser, initPassword); err != nil { | ||||
| 				t.Fatalf("Could not connect with credentials: %s", err) | ||||
| 			} | ||||
|  | ||||
| @@ -364,40 +436,40 @@ func TestMySQL_SetCredentials(t *testing.T) { | ||||
| 				"connection_url": connURL, | ||||
| 			} | ||||
|  | ||||
| 			initReq := newdbplugin.InitializeRequest{ | ||||
| 				Config:           connectionDetails, | ||||
| 				VerifyConnection: true, | ||||
| 			} | ||||
|  | ||||
| 			// Give a timeout just in case the test decides to be problematic | ||||
| 			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||
| 			defer cancel() | ||||
|  | ||||
| 			db := new(MetadataLen, MetadataLen, UsernameLen) | ||||
| 			_, err := db.Init(ctx, connectionDetails, true) | ||||
| 			db := new(false) | ||||
| 			_, err := db.Initialize(context.Background(), initReq) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("err: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			newPassword, err := db.GenerateCredentials(ctx) | ||||
| 			newPassword, err := credsutil.RandomAlphaNumeric(32, false) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("unable to generate password: %s", err) | ||||
| 			} | ||||
|  | ||||
| 			userConfig := dbplugin.StaticUserConfig{ | ||||
| 			updateReq := newdbplugin.UpdateUserRequest{ | ||||
| 				Username: dbUser, | ||||
| 				Password: newPassword, | ||||
| 				Password: &newdbplugin.ChangePassword{ | ||||
| 					NewPassword: newPassword, | ||||
| 					Statements: newdbplugin.Statements{ | ||||
| 						Commands: test.rotateStmts, | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			statements := dbplugin.Statements{ | ||||
| 				Rotation: test.rotateStmts, | ||||
| 			} | ||||
|  | ||||
| 			username, password, err := db.SetCredentials(ctx, statements, userConfig) | ||||
| 			_, err = db.UpdateUser(ctx, updateReq) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("err: %s", err) | ||||
| 			} | ||||
| 			if username != userConfig.Username { | ||||
| 				t.Fatalf("expected username [%s], got [%s]", userConfig.Username, username) | ||||
| 			} | ||||
| 			if password != userConfig.Password { | ||||
| 				t.Fatalf("expected password [%s] got [%s]", userConfig.Password, password) | ||||
| 			} | ||||
|  | ||||
| 			// verify new password works | ||||
| 			if err := mysqlhelper.TestCredsExist(t, connURL, dbUser, newPassword); err != nil { | ||||
| @@ -425,7 +497,7 @@ func TestMySQL_Initialize_ReservedChars(t *testing.T) { | ||||
| 		"password":       pw, | ||||
| 	} | ||||
|  | ||||
| 	db := new(MetadataLen, MetadataLen, UsernameLen) | ||||
| 	db := new(false) | ||||
| 	_, err := db.Init(context.Background(), connectionDetails, true) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Lauren Voswinkel
					Lauren Voswinkel