Skip to content

Commit

Permalink
RabbitMQ - Add username customization (#11899)
Browse files Browse the repository at this point in the history
* add username customization for rabbitmq

* add changelog for rabbitmq

* Update builtin/logical/rabbitmq/path_config_connection.go

Co-authored-by: Tom Proctor <[email protected]>

* updating API docs

* moved to changelog folder

Co-authored-by: Tom Proctor <[email protected]>
  • Loading branch information
MilenaHC and tomhjp authored Jun 22, 2021
1 parent 99ea0fb commit de4295b
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 11 deletions.
30 changes: 26 additions & 4 deletions builtin/logical/rabbitmq/path_config_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/template"
"github.com/hashicorp/vault/sdk/logical"
rabbithole "github.com/michaelklishin/rabbit-hole"
)
Expand Down Expand Up @@ -38,6 +39,10 @@ func pathConfigConnection(b *backend) *framework.Path {
Type: framework.TypeString,
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{
Expand Down Expand Up @@ -65,6 +70,19 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
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)

// Don't check the connection_url if verification is disabled
Expand All @@ -84,10 +102,11 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request

// Store it
config := connectionConfig{
URI: uri,
Username: username,
Password: password,
PasswordPolicy: passwordPolicy,
URI: uri,
Username: username,
Password: password,
PasswordPolicy: passwordPolicy,
UsernameTemplate: usernameTemplate,
}
err := writeConfig(ctx, req.Storage, config)
if err != nil {
Expand Down Expand Up @@ -140,6 +159,9 @@ type connectionConfig struct {

// PasswordPolicy for generating passwords for dynamic credentials
PasswordPolicy string `json:"password_policy"`

// UsernameTemplate for storing the raw template in Vault's backing data store
UsernameTemplate string `json:"username_template"`
}

const pathConfigConnectionHelpSyn = `
Expand Down
104 changes: 104 additions & 0 deletions builtin/logical/rabbitmq/path_config_connection_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
38 changes: 31 additions & 7 deletions builtin/logical/rabbitmq/path_role_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import (
"fmt"
"io/ioutil"

"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/template"
"github.com/hashicorp/vault/sdk/logical"
rabbithole "github.com/michaelklishin/rabbit-hole"
)

const (
defaultUserNameTemplate = `{{ printf "%s-%s" (.DisplayName) (uuid) }}`
)

func pathCreds(b *backend) *framework.Path {
return &framework.Path{
Pattern: "creds/" + framework.GenericNameRegex("name"),
Expand Down Expand Up @@ -46,17 +50,31 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr
return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil
}

// Ensure username is unique
uuidVal, err := uuid.GenerateUUID()
config, err := readConfig(ctx, req.Storage)
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to read configuration: %w", err)
}
username := fmt.Sprintf("%s-%s", req.DisplayName, uuidVal)

config, err := readConfig(ctx, req.Storage)
usernameTemplate := config.UsernameTemplate
if usernameTemplate == "" {
usernameTemplate = defaultUserNameTemplate
}

up, err := template.NewTemplate(template.Template(usernameTemplate))
if err != nil {
return nil, fmt.Errorf("unable to read configuration: %w", err)
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)
if err != nil {
Expand Down Expand Up @@ -189,6 +207,12 @@ func isIn200s(respStatus int) bool {
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 = `
Request RabbitMQ credentials for a certain role.
`
Expand Down
Loading

0 comments on commit de4295b

Please sign in to comment.