Redshift - Add username customization (#12016)
* username customization for redshift * adding changelog and updating api-docs
This commit is contained in:
parent
72e3d29abc
commit
3c3b6529fd
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
secrets/database/redshift: Add ability to customize dynamic usernames
|
||||
```
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}}";
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue