Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RabbitMQ - Add username customization #11899

Merged
merged 6 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions 11899.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
secret/rabbitmq: Add ability to customize dynamic usernames
```
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: "When a request is made, username_template allows Vault to recognize the argument",
MilenaHC marked this conversation as resolved.
Show resolved Hide resolved
},
},

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))
tomhjp marked this conversation as resolved.
Show resolved Hide resolved
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) {
MilenaHC marked this conversation as resolved.
Show resolved Hide resolved
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