RabbitMQ - Add username customization (#11899)

* add username customization for rabbitmq

* add changelog for rabbitmq

* Update builtin/logical/rabbitmq/path_config_connection.go

Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>

* updating API docs

* moved to changelog folder

Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
MilenaHC 2021-06-22 14:50:46 -05:00 committed by GitHub
parent 022ccc2657
commit 5483eba5fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 329 additions and 12 deletions

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/template"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
rabbithole "github.com/michaelklishin/rabbit-hole" rabbithole "github.com/michaelklishin/rabbit-hole"
) )
@ -38,6 +39,10 @@ func pathConfigConnection(b *backend) *framework.Path {
Type: framework.TypeString, Type: framework.TypeString,
Description: "Name of the password policy to use to generate passwords for dynamic credentials.", Description: "Name of the password policy to use to generate passwords for dynamic credentials.",
}, },
"username_template": {
Type: framework.TypeString,
Description: "Template describing how dynamic usernames are generated.",
},
}, },
Callbacks: map[logical.Operation]framework.OperationFunc{ Callbacks: map[logical.Operation]framework.OperationFunc{
@ -65,6 +70,19 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
return logical.ErrorResponse("missing password"), nil return logical.ErrorResponse("missing password"), nil
} }
usernameTemplate := data.Get("username_template").(string)
if usernameTemplate != "" {
up, err := template.NewTemplate(template.Template(usernameTemplate))
if err != nil {
return logical.ErrorResponse("unable to initialize username template: %w", err), nil
}
_, err = up.Generate(UsernameMetadata{})
if err != nil {
return logical.ErrorResponse("invalid username template: %w", err), nil
}
}
passwordPolicy := data.Get("password_policy").(string) passwordPolicy := data.Get("password_policy").(string)
// Don't check the connection_url if verification is disabled // Don't check the connection_url if verification is disabled
@ -88,6 +106,7 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
Username: username, Username: username,
Password: password, Password: password,
PasswordPolicy: passwordPolicy, PasswordPolicy: passwordPolicy,
UsernameTemplate: usernameTemplate,
} }
err := writeConfig(ctx, req.Storage, config) err := writeConfig(ctx, req.Storage, config)
if err != nil { if err != nil {
@ -140,6 +159,9 @@ type connectionConfig struct {
// PasswordPolicy for generating passwords for dynamic credentials // PasswordPolicy for generating passwords for dynamic credentials
PasswordPolicy string `json:"password_policy"` PasswordPolicy string `json:"password_policy"`
// UsernameTemplate for storing the raw template in Vault's backing data store
UsernameTemplate string `json:"username_template"`
} }
const pathConfigConnectionHelpSyn = ` const pathConfigConnectionHelpSyn = `

View File

@ -0,0 +1,104 @@
package rabbitmq
import (
"context"
"reflect"
"testing"
"github.com/hashicorp/vault/sdk/logical"
)
func TestBackend_ConfigConnection_DefaultUsernameTemplate(t *testing.T) {
var resp *logical.Response
var err error
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b := Backend()
if err = b.Setup(context.Background(), config); err != nil {
t.Fatal(err)
}
configData := map[string]interface{}{
"connection_uri": "uri",
"username": "username",
"password": "password",
"verify_connection": "false",
}
configReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/connection",
Storage: config.StorageView,
Data: configData,
}
resp, err = b.HandleRequest(context.Background(), configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr:%s", resp, err)
}
if resp != nil {
t.Fatal("expected a nil response")
}
actualConfig, err := readConfig(context.Background(), config.StorageView)
if err != nil {
t.Fatalf("unable to read configuration: %v", err)
}
expectedConfig := connectionConfig{
URI: "uri",
Username: "username",
Password: "password",
UsernameTemplate: "",
}
if !reflect.DeepEqual(actualConfig, expectedConfig) {
t.Fatalf("Expected: %#v\nActual: %#v", expectedConfig, actualConfig)
}
}
func TestBackend_ConfigConnection_CustomUsernameTemplate(t *testing.T) {
var resp *logical.Response
var err error
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b := Backend()
if err = b.Setup(context.Background(), config); err != nil {
t.Fatal(err)
}
configData := map[string]interface{}{
"connection_uri": "uri",
"username": "username",
"password": "password",
"verify_connection": "false",
"username_template": "{{ .DisplayName }}",
}
configReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/connection",
Storage: config.StorageView,
Data: configData,
}
resp, err = b.HandleRequest(context.Background(), configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr:%s", resp, err)
}
if resp != nil {
t.Fatal("expected a nil response")
}
actualConfig, err := readConfig(context.Background(), config.StorageView)
if err != nil {
t.Fatalf("unable to read configuration: %v", err)
}
expectedConfig := connectionConfig{
URI: "uri",
Username: "username",
Password: "password",
UsernameTemplate: "{{ .DisplayName }}",
}
if !reflect.DeepEqual(actualConfig, expectedConfig) {
t.Fatalf("Expected: %#v\nActual: %#v", expectedConfig, actualConfig)
}
}

View File

@ -5,12 +5,16 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/template"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
rabbithole "github.com/michaelklishin/rabbit-hole" rabbithole "github.com/michaelklishin/rabbit-hole"
) )
const (
defaultUserNameTemplate = `{{ printf "%s-%s" (.DisplayName) (uuid) }}`
)
func pathCreds(b *backend) *framework.Path { func pathCreds(b *backend) *framework.Path {
return &framework.Path{ return &framework.Path{
Pattern: "creds/" + framework.GenericNameRegex("name"), Pattern: "creds/" + framework.GenericNameRegex("name"),
@ -46,18 +50,32 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr
return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil
} }
// Ensure username is unique
uuidVal, err := uuid.GenerateUUID()
if err != nil {
return nil, err
}
username := fmt.Sprintf("%s-%s", req.DisplayName, uuidVal)
config, err := readConfig(ctx, req.Storage) config, err := readConfig(ctx, req.Storage)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read configuration: %w", err) return nil, fmt.Errorf("unable to read configuration: %w", err)
} }
usernameTemplate := config.UsernameTemplate
if usernameTemplate == "" {
usernameTemplate = defaultUserNameTemplate
}
up, err := template.NewTemplate(template.Template(usernameTemplate))
if err != nil {
return nil, fmt.Errorf("unable to initialize username template: %w", err)
}
um := UsernameMetadata{
DisplayName: req.DisplayName,
RoleName: name,
}
username, err := up.Generate(um)
if err != nil {
return nil, fmt.Errorf("failed to generate username: %w", err)
}
fmt.Printf("username: %s\n", username)
password, err := b.generatePassword(ctx, config.PasswordPolicy) password, err := b.generatePassword(ctx, config.PasswordPolicy)
if err != nil { if err != nil {
return nil, err return nil, err
@ -189,6 +207,12 @@ func isIn200s(respStatus int) bool {
return respStatus >= 200 && respStatus < 300 return respStatus >= 200 && respStatus < 300
} }
// UsernameMetadata is metadata the database plugin can use to generate a username
type UsernameMetadata struct {
DisplayName string
RoleName string
}
const pathRoleCreateReadHelpSyn = ` const pathRoleCreateReadHelpSyn = `
Request RabbitMQ credentials for a certain role. Request RabbitMQ credentials for a certain role.
` `

View File

@ -0,0 +1,161 @@
package rabbitmq
import (
"context"
"testing"
"github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/require"
)
func TestBackend_RoleCreate_DefaultUsernameTemplate(t *testing.T) {
cleanup, connectionURI := prepareRabbitMQTestContainer(t)
defer cleanup()
var resp *logical.Response
var err error
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b := Backend()
if err = b.Setup(context.Background(), config); err != nil {
t.Fatal(err)
}
configData := map[string]interface{}{
"connection_uri": connectionURI,
"username": "guest",
"password": "guest",
"username_template": "",
}
configReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/connection",
Storage: config.StorageView,
Data: configData,
}
resp, err = b.HandleRequest(context.Background(), configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr:%s", resp, err)
}
if resp != nil {
t.Fatal("expected a nil response")
}
roleData := map[string]interface{}{
"name": "foo",
"tags": "bar",
}
roleReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/foo",
Storage: config.StorageView,
Data: roleData,
}
resp, err = b.HandleRequest(context.Background(), roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr:%s", resp, err)
}
if resp != nil {
t.Fatal("expected a nil response")
}
credsReq := &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/foo",
Storage: config.StorageView,
DisplayName: "token",
}
resp, err = b.HandleRequest(context.Background(), credsReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr:%s", resp, err)
}
if resp == nil {
t.Fatal("missing creds response")
}
if resp.Data == nil {
t.Fatalf("missing creds data")
}
username, exists := resp.Data["username"]
if !exists {
t.Fatalf("missing username in response")
}
require.Regexp(t, `^token-[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$`, username)
}
func TestBackend_RoleCreate_CustomUsernameTemplate(t *testing.T) {
cleanup, connectionURI := prepareRabbitMQTestContainer(t)
defer cleanup()
var resp *logical.Response
var err error
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b := Backend()
if err = b.Setup(context.Background(), config); err != nil {
t.Fatal(err)
}
configData := map[string]interface{}{
"connection_uri": connectionURI,
"username": "guest",
"password": "guest",
"username_template": "foo-{{ .DisplayName }}",
}
configReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/connection",
Storage: config.StorageView,
Data: configData,
}
resp, err = b.HandleRequest(context.Background(), configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr:%s", resp, err)
}
if resp != nil {
t.Fatal("expected a nil response")
}
roleData := map[string]interface{}{
"name": "foo",
"tags": "bar",
}
roleReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/foo",
Storage: config.StorageView,
Data: roleData,
}
resp, err = b.HandleRequest(context.Background(), roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr:%s", resp, err)
}
if resp != nil {
t.Fatal("expected a nil response")
}
credsReq := &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/foo",
Storage: config.StorageView,
DisplayName: "token",
}
resp, err = b.HandleRequest(context.Background(), credsReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr:%s", resp, err)
}
if resp == nil {
t.Fatal("missing creds response")
}
if resp.Data == nil {
t.Fatalf("missing creds data")
}
username, exists := resp.Data["username"]
if !exists {
t.Fatalf("missing username in response")
}
require.Regexp(t, `^foo-token$`, username)
}

3
changelog/11899.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
secret/rabbitmq: Add ability to customize dynamic usernames
```

View File

@ -36,6 +36,9 @@ RabbitMQ.
- `password_policy` `(string: "")` - Specifies a [password policy](/docs/concepts/password-policies) to - `password_policy` `(string: "")` - Specifies a [password policy](/docs/concepts/password-policies) to
use when creating dynamic credentials. Defaults to generating an alphanumeric password if not set. use when creating dynamic credentials. Defaults to generating an alphanumeric password if not set.
- `username_template` `(string)` - [Template](/docs/concepts/username-templating) describing how
dynamic usernames are generated.
### Sample Payload ### Sample Payload
```json ```json