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"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 = `
|
||||
|
|
|
@ -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"
|
||||
"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"),
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -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.
|
||||
`
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
||||
```json
|
||||
|
|
Loading…
Reference in New Issue