diff --git a/changelog/12016.txt b/changelog/12016.txt new file mode 100644 index 000000000..4b57569df --- /dev/null +++ b/changelog/12016.txt @@ -0,0 +1,3 @@ +```release-note:feature +secrets/database/redshift: Add ability to customize dynamic usernames +``` diff --git a/plugins/database/redshift/redshift.go b/plugins/database/redshift/redshift.go index 6cd96bf5d..59f6b1298 100644 --- a/plugins/database/redshift/redshift.go +++ b/plugins/database/redshift/redshift.go @@ -10,10 +10,10 @@ import ( "github.com/hashicorp/go-multierror" dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/database/helper/connutil" - "github.com/hashicorp/vault/sdk/database/helper/credsutil" "github.com/hashicorp/vault/sdk/database/helper/dbutil" "github.com/hashicorp/vault/sdk/helper/dbtxn" "github.com/hashicorp/vault/sdk/helper/strutil" + "github.com/hashicorp/vault/sdk/helper/template" "github.com/lib/pq" ) @@ -31,6 +31,7 @@ ALTER USER "{{name}}" VALID UNTIL '{{expiration}}'; defaultRotateRootCredentialsSQL = ` ALTER USER "{{name}}" WITH PASSWORD '{{password}}'; ` + defaultUserNameTemplate = `{{ printf "v-%s-%s-%s-%s" (.DisplayName | truncate 8) (.RoleName | truncate 8) (random 20) (unix_time) | truncate 63 | lowercase }}` ) var _ dbplugin.Database = (*RedShift)(nil) @@ -58,6 +59,8 @@ func newRedshift() *RedShift { type RedShift struct { *connutil.SQLConnectionProducer + + usernameProducer template.StringTemplate } func (r *RedShift) secretValues() map[string]string { @@ -78,6 +81,25 @@ func (r *RedShift) Initialize(ctx context.Context, req dbplugin.InitializeReques return dbplugin.InitializeResponse{}, fmt.Errorf("error initializing db: %w", err) } + usernameTemplate, err := strutil.GetString(req.Config, "username_template") + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("failed to retrieve username_template: %w", err) + } + if usernameTemplate == "" { + usernameTemplate = defaultUserNameTemplate + } + + up, err := template.NewTemplate(template.Template(usernameTemplate)) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("unable to initialize username template: %w", err) + } + r.usernameProducer = up + + _, err = r.usernameProducer.Generate(dbplugin.UsernameMetadata{}) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("invalid username template: %w", err) + } + return dbplugin.InitializeResponse{ Config: conf, }, nil @@ -105,15 +127,7 @@ func (r *RedShift) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (db r.Lock() defer r.Unlock() - usernameOpts := []credsutil.UsernameOpt{ - credsutil.DisplayName(req.UsernameConfig.DisplayName, 8), - credsutil.RoleName(req.UsernameConfig.RoleName, 8), - credsutil.MaxLength(63), - credsutil.Separator("-"), - credsutil.ToLower(), - } - - username, err := credsutil.GenerateUsername(usernameOpts...) + username, err := r.usernameProducer.Generate(req.UsernameConfig) if err != nil { return dbplugin.NewUserResponse{}, err } diff --git a/plugins/database/redshift/redshift_test.go b/plugins/database/redshift/redshift_test.go index 17aeb06cd..c8a9cf77b 100644 --- a/plugins/database/redshift/redshift_test.go +++ b/plugins/database/redshift/redshift_test.go @@ -11,12 +11,11 @@ import ( "time" "github.com/hashicorp/go-uuid" - + dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" "github.com/hashicorp/vault/sdk/helper/dbtxn" - - dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/lib/pq" + "github.com/stretchr/testify/require" ) /* @@ -377,6 +376,103 @@ func testCredsExist(t testing.TB, url, username, password string) error { return db.Ping() } +func TestRedshift_DefaultUsernameTemplate(t *testing.T) { + if os.Getenv(vaultACC) != "1" { + t.SkipNow() + } + + connURL, url, _, _, err := redshiftEnv() + if err != nil { + t.Fatal(err) + } + + connectionDetails := map[string]interface{}{ + "connection_url": connURL, + } + + db := newRedshift() + dbtesting.AssertInitialize(t, db, dbplugin.InitializeRequest{ + Config: connectionDetails, + VerifyConnection: true, + }) + + usernameConfig := dbplugin.UsernameMetadata{ + DisplayName: "test", + RoleName: "test", + } + + const password = "SuperSecurePa55w0rd!" + for _, commands := range [][]string{{testRedshiftRole}, {testRedshiftReadOnlyRole}} { + resp := dbtesting.AssertNewUser(t, db, dbplugin.NewUserRequest{ + UsernameConfig: usernameConfig, + Password: password, + Statements: dbplugin.Statements{ + Commands: commands, + }, + Expiration: time.Now().Add(5 * time.Minute), + }) + username := resp.Username + + if resp.Username == "" { + t.Fatalf("Missing username") + } + + testCredsExist(t, url, username, password) + + require.Regexp(t, `^v-test-test-[a-z0-9]{20}-[0-9]{10}$`, resp.Username) + } + dbtesting.AssertClose(t, db) +} + +func TestRedshift_CustomUsernameTemplate(t *testing.T) { + if os.Getenv(vaultACC) != "1" { + t.SkipNow() + } + + connURL, url, _, _, err := redshiftEnv() + if err != nil { + t.Fatal(err) + } + + connectionDetails := map[string]interface{}{ + "connection_url": connURL, + "username_template": "{{.DisplayName}}-{{random 10}}", + } + + db := newRedshift() + dbtesting.AssertInitialize(t, db, dbplugin.InitializeRequest{ + Config: connectionDetails, + VerifyConnection: true, + }) + + usernameConfig := dbplugin.UsernameMetadata{ + DisplayName: "test", + RoleName: "test", + } + + const password = "SuperSecurePa55w0rd!" + for _, commands := range [][]string{{testRedshiftRole}, {testRedshiftReadOnlyRole}} { + resp := dbtesting.AssertNewUser(t, db, dbplugin.NewUserRequest{ + UsernameConfig: usernameConfig, + Password: password, + Statements: dbplugin.Statements{ + Commands: commands, + }, + Expiration: time.Now().Add(5 * time.Minute), + }) + username := resp.Username + + if resp.Username == "" { + t.Fatalf("Missing username") + } + + testCredsExist(t, url, username, password) + + require.Regexp(t, `^test-[a-zA-Z0-9]{10}$`, resp.Username) + } + dbtesting.AssertClose(t, db) +} + const testRedshiftRole = ` CREATE USER "{{name}}" WITH PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; diff --git a/website/content/api-docs/secret/databases/redshift.mdx b/website/content/api-docs/secret/databases/redshift.mdx index 9cc6bc357..868676740 100644 --- a/website/content/api-docs/secret/databases/redshift.mdx +++ b/website/content/api-docs/secret/databases/redshift.mdx @@ -44,6 +44,8 @@ has a number of parameters to further configure a connection. - `password` `(string: "")` - The root credential password used in the connection URL. +- `username_template` `(string)` - [Template](/docs/concepts/username-templating) describing how dynamic usernames are generated. + ### Sample Payload ```json