Redshift - Add username customization (#12016)

* username customization for redshift

* adding changelog and updating api-docs
This commit is contained in:
MilenaHC 2021-07-08 10:29:12 -05:00 committed by GitHub
parent 72e3d29abc
commit 3c3b6529fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 13 deletions

3
changelog/12016.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
secrets/database/redshift: Add ability to customize dynamic usernames
```

View File

@ -10,10 +10,10 @@ import (
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
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/connutil" "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/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/helper/dbtxn" "github.com/hashicorp/vault/sdk/helper/dbtxn"
"github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/helper/template"
"github.com/lib/pq" "github.com/lib/pq"
) )
@ -31,6 +31,7 @@ ALTER USER "{{name}}" VALID UNTIL '{{expiration}}';
defaultRotateRootCredentialsSQL = ` defaultRotateRootCredentialsSQL = `
ALTER USER "{{name}}" WITH PASSWORD '{{password}}'; 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) var _ dbplugin.Database = (*RedShift)(nil)
@ -58,6 +59,8 @@ func newRedshift() *RedShift {
type RedShift struct { type RedShift struct {
*connutil.SQLConnectionProducer *connutil.SQLConnectionProducer
usernameProducer template.StringTemplate
} }
func (r *RedShift) secretValues() map[string]string { 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) 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{ return dbplugin.InitializeResponse{
Config: conf, Config: conf,
}, nil }, nil
@ -105,15 +127,7 @@ func (r *RedShift) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (db
r.Lock() r.Lock()
defer r.Unlock() defer r.Unlock()
usernameOpts := []credsutil.UsernameOpt{ username, err := r.usernameProducer.Generate(req.UsernameConfig)
credsutil.DisplayName(req.UsernameConfig.DisplayName, 8),
credsutil.RoleName(req.UsernameConfig.RoleName, 8),
credsutil.MaxLength(63),
credsutil.Separator("-"),
credsutil.ToLower(),
}
username, err := credsutil.GenerateUsername(usernameOpts...)
if err != nil { if err != nil {
return dbplugin.NewUserResponse{}, err return dbplugin.NewUserResponse{}, err
} }

View File

@ -11,12 +11,11 @@ import (
"time" "time"
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
"github.com/hashicorp/vault/sdk/helper/dbtxn" "github.com/hashicorp/vault/sdk/helper/dbtxn"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/lib/pq" "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() 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 = ` const testRedshiftRole = `
CREATE USER "{{name}}" WITH PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; CREATE USER "{{name}}" WITH PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";

View File

@ -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. - `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 ### Sample Payload
```json ```json