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"
|
"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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}}";
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue