Skip to content

Commit

Permalink
Integrate password policies into RabbitMQ secret engine (#9143)
Browse files Browse the repository at this point in the history
* Add password policies to RabbitMQ & update docs
* Also updates some parts of the password policies to aid/fix testing
  • Loading branch information
pcman312 authored and andaley committed Jul 17, 2020
1 parent d6c8baf commit 1b7523a
Show file tree
Hide file tree
Showing 13 changed files with 262 additions and 79 deletions.
11 changes: 1 addition & 10 deletions builtin/logical/rabbitmq/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rabbitmq

import (
"context"
"fmt"
"strings"
"sync"

Expand Down Expand Up @@ -73,18 +72,10 @@ func (b *backend) Client(ctx context.Context, s logical.Storage) (*rabbithole.Cl
b.lock.RUnlock()

// Otherwise, attempt to make connection
entry, err := s.Get(ctx, "config/connection")
connConfig, err := readConfig(ctx, s)
if err != nil {
return nil, err
}
if entry == nil {
return nil, fmt.Errorf("configure the client connection with config/connection first")
}

var connConfig connectionConfig
if err := entry.DecodeJSON(&connConfig); err != nil {
return nil, err
}

b.lock.Lock()
defer b.lock.Unlock()
Expand Down
56 changes: 42 additions & 14 deletions builtin/logical/rabbitmq/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/hashicorp/vault/helper/testhelpers/docker"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/random"
"github.com/hashicorp/vault/sdk/logical"
rabbithole "github.com/michaelklishin/rabbit-hole"
"github.com/mitchellh/mapstructure"
Expand All @@ -27,6 +28,8 @@ const (
testTags = "administrator"
testVHosts = `{"/": {"configure": ".*", "write": ".*", "read": ".*"}}`
testVHostTopics = `{"/": {"amq.topic": {"write": ".*", "read": ".*"}}}`

roleName = "web"
)

func prepareRabbitMQTestContainer(t *testing.T) (func(), string, int) {
Expand Down Expand Up @@ -89,9 +92,9 @@ func TestBackend_basic(t *testing.T) {
PreCheck: testAccPreCheckFunc(t, uri),
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, uri),
testAccStepConfig(t, uri, ""),
testAccStepRole(t),
testAccStepReadCreds(t, b, uri, "web"),
testAccStepReadCreds(t, b, uri, roleName),
},
})

Expand All @@ -111,10 +114,10 @@ func TestBackend_returnsErrs(t *testing.T) {
PreCheck: testAccPreCheckFunc(t, uri),
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, uri),
testAccStepConfig(t, uri, ""),
{
Operation: logical.CreateOperation,
Path: "roles/web",
Path: fmt.Sprintf("roles/%s", roleName),
Data: map[string]interface{}{
"tags": testTags,
"vhosts": `{"invalid":{"write": ".*", "read": ".*"}}`,
Expand All @@ -123,7 +126,7 @@ func TestBackend_returnsErrs(t *testing.T) {
},
{
Operation: logical.ReadOperation,
Path: "creds/web",
Path: fmt.Sprintf("creds/%s", roleName),
ErrorOk: true,
},
},
Expand All @@ -144,11 +147,35 @@ func TestBackend_roleCrud(t *testing.T) {
PreCheck: testAccPreCheckFunc(t, uri),
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, uri),
testAccStepConfig(t, uri, ""),
testAccStepRole(t),
testAccStepReadRole(t, roleName, testTags, testVHosts, testVHostTopics),
testAccStepDeleteRole(t, roleName),
testAccStepReadRole(t, roleName, "", "", ""),
},
})
}

func TestBackend_roleWithPasswordPolicy(t *testing.T) {
if os.Getenv(logicaltest.TestEnvVar) == "" {
t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar))
return
}

backendConfig := logical.TestBackendConfig()
backendConfig.System.(*logical.StaticSystemView).SetPasswordPolicy("testpolicy", random.DefaultStringGenerator)
b, _ := Factory(context.Background(), backendConfig)

cleanup, uri, _ := prepareRabbitMQTestContainer(t)
defer cleanup()

logicaltest.Test(t, logicaltest.TestCase{
PreCheck: testAccPreCheckFunc(t, uri),
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, uri, "testpolicy"),
testAccStepRole(t),
testAccStepReadRole(t, "web", testTags, testVHosts, testVHostTopics),
testAccStepDeleteRole(t, "web"),
testAccStepReadRole(t, "web", "", "", ""),
testAccStepReadCreds(t, b, uri, roleName),
},
})
}
Expand All @@ -161,7 +188,7 @@ func testAccPreCheckFunc(t *testing.T, uri string) func() {
}
}

func testAccStepConfig(t *testing.T, uri string) logicaltest.TestStep {
func testAccStepConfig(t *testing.T, uri string, passwordPolicy string) logicaltest.TestStep {
username := os.Getenv(envRabbitMQUsername)
if len(username) == 0 {
username = "guest"
Expand All @@ -175,17 +202,18 @@ func testAccStepConfig(t *testing.T, uri string) logicaltest.TestStep {
Operation: logical.UpdateOperation,
Path: "config/connection",
Data: map[string]interface{}{
"connection_uri": uri,
"username": username,
"password": password,
"connection_uri": uri,
"username": username,
"password": password,
"password_policy": passwordPolicy,
},
}
}

func testAccStepRole(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "roles/web",
Path: fmt.Sprintf("roles/%s", roleName),
Data: map[string]interface{}{
"tags": testTags,
"vhosts": testVHosts,
Expand Down
14 changes: 14 additions & 0 deletions builtin/logical/rabbitmq/passwords.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package rabbitmq

import (
"context"

"github.com/hashicorp/vault/sdk/helper/base62"
)

func (b *backend) generatePassword(ctx context.Context, policyName string) (password string, err error) {
if policyName != "" {
return b.System().GeneratePasswordFromPolicy(ctx, policyName)
}
return base62.Random(36)
}
55 changes: 47 additions & 8 deletions builtin/logical/rabbitmq/path_config_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
rabbithole "github.com/michaelklishin/rabbit-hole"
)

const (
storageKey = "config/connection"
)

func pathConfigConnection(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/connection",
Expand All @@ -30,6 +34,10 @@ func pathConfigConnection(b *backend) *framework.Path {
Default: true,
Description: `If set, connection_uri is verified by actually connecting to the RabbitMQ management API`,
},
"password_policy": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the password policy to use to generate passwords for dynamic credentials.",
},
},

Callbacks: map[logical.Operation]framework.OperationFunc{
Expand Down Expand Up @@ -57,6 +65,8 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
return logical.ErrorResponse("missing password"), nil
}

passwordPolicy := data.Get("password_policy").(string)

// Don't check the connection_url if verification is disabled
verifyConnection := data.Get("verify_connection").(bool)
if verifyConnection {
Expand All @@ -73,15 +83,14 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
}

// Store it
entry, err := logical.StorageEntryJSON("config/connection", connectionConfig{
URI: uri,
Username: username,
Password: password,
})
if err != nil {
return nil, err
config := connectionConfig{
URI: uri,
Username: username,
Password: password,
PasswordPolicy: passwordPolicy,
}
if err := req.Storage.Put(ctx, entry); err != nil {
err := writeConfig(ctx, req.Storage, config)
if err != nil {
return nil, err
}

Expand All @@ -91,6 +100,33 @@ func (b *backend) pathConnectionUpdate(ctx context.Context, req *logical.Request
return nil, nil
}

func readConfig(ctx context.Context, storage logical.Storage) (connectionConfig, error) {
entry, err := storage.Get(ctx, storageKey)
if err != nil {
return connectionConfig{}, err
}
if entry == nil {
return connectionConfig{}, nil
}

var connConfig connectionConfig
if err := entry.DecodeJSON(&connConfig); err != nil {
return connectionConfig{}, err
}
return connConfig, nil
}

func writeConfig(ctx context.Context, storage logical.Storage, config connectionConfig) error {
entry, err := logical.StorageEntryJSON(storageKey, config)
if err != nil {
return err
}
if err := storage.Put(ctx, entry); err != nil {
return err
}
return nil
}

// connectionConfig contains the information required to make a connection to a RabbitMQ node
type connectionConfig struct {
// URI of the RabbitMQ server
Expand All @@ -101,6 +137,9 @@ type connectionConfig struct {

// Password for the Username
Password string `json:"password"`

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

const pathConfigConnectionHelpSyn = `
Expand Down
7 changes: 6 additions & 1 deletion builtin/logical/rabbitmq/path_role_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr
}
username := fmt.Sprintf("%s-%s", req.DisplayName, uuidVal)

password, err := uuid.GenerateUUID()
config, err := readConfig(ctx, req.Storage)
if err != nil {
return nil, fmt.Errorf("unable to read configuration: %w", err)
}

password, err := b.generatePassword(ctx, config.PasswordPolicy)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/helper/random/string_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var (
AlphaNumericFullSymbolRuneset = []rune(AlphaNumericFullSymbolCharset)

// DefaultStringGenerator has reasonable default rules for generating strings
DefaultStringGenerator = StringGenerator{
DefaultStringGenerator = &StringGenerator{
Length: 20,
Rules: []Rule{
CharsetRule{
Expand Down
Loading

0 comments on commit 1b7523a

Please sign in to comment.