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:
parent
022ccc2657
commit
5483eba5fc
|
@ -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 = `
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
`
|
`
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
secret/rabbitmq: Add ability to customize dynamic usernames
|
||||||
|
```
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue