Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
227 changes: 227 additions & 0 deletions builtin/logical/database/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,233 @@ func TestBackend_basic(t *testing.T) {
assertEvent(t, "database/role-delete", "plugin-role-test", "roles/plugin-role-test")
}

func TestBackend_basicWithAtRole(t *testing.T) {
cluster, sys := getClusterPostgresDB(t)
defer cluster.Cleanup()

config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
eventSender := logical.NewMockEventSender()
config.EventsSender = eventSender

b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
defer b.Cleanup(context.Background())

cleanup, connURL := postgreshelper.PrepareTestContainer(t)
t.Cleanup(cleanup)

// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"allowed_roles": []string{"plugin-role-@test"},
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

// Create a role
data = map[string]interface{}{
"db_name": "plugin-test",
"creation_statements": testRole,
"max_ttl": "10m",
}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/plugin-role-@test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Get creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/plugin-role-@test",
Storage: config.StorageView,
Data: data,
}
credsResp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (credsResp != nil && credsResp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, credsResp)
}

// Update the role with no max ttl
data = map[string]interface{}{
"db_name": "plugin-test",
"creation_statements": testRole,
"default_ttl": "5m",
"max_ttl": 0,
}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/plugin-role-@test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Get creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/plugin-role-@test",
Storage: config.StorageView,
Data: data,
}
credsResp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (credsResp != nil && credsResp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, credsResp)
}
// Test for #3812
if credsResp.Secret.TTL != 5*time.Minute {
t.Fatalf("unexpected TTL of %d", credsResp.Secret.TTL)
}
// Update the role with a max ttl
data = map[string]interface{}{
"db_name": "plugin-test",
"creation_statements": testRole,
"default_ttl": "5m",
"max_ttl": "10m",
}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/plugin-role-@test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

// Get creds and revoke when the role stays in existence
{
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/plugin-role-@test",
Storage: config.StorageView,
Data: data,
}
credsResp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (credsResp != nil && credsResp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, credsResp)
}
// Test for #3812
if credsResp.Secret.TTL != 5*time.Minute {
t.Fatalf("unexpected TTL of %d", credsResp.Secret.TTL)
}
if !testCredsExist(t, credsResp.Data, connURL) {
t.Fatalf("Creds should exist")
}

// Revoke creds
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Operation: logical.RevokeOperation,
Storage: config.StorageView,
Secret: &logical.Secret{
InternalData: map[string]interface{}{
"secret_type": "creds",
"username": credsResp.Data["username"],
"role": "plugin-role-@test",
},
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

if testCredsExist(t, credsResp.Data, connURL) {
t.Fatalf("Creds should not exist")
}
}

// Get creds and revoke using embedded revocation data
{
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/plugin-role-@test",
Storage: config.StorageView,
Data: data,
}
credsResp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (credsResp != nil && credsResp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, credsResp)
}
if !testCredsExist(t, credsResp.Data, connURL) {
t.Fatalf("Creds should exist")
}

// Delete role, forcing us to rely on embedded data
req = &logical.Request{
Operation: logical.DeleteOperation,
Path: "roles/plugin-role-@test",
Storage: config.StorageView,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

// Revoke creds
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Operation: logical.RevokeOperation,
Storage: config.StorageView,
Secret: &logical.Secret{
InternalData: map[string]interface{}{
"secret_type": "creds",
"username": credsResp.Data["username"],
"role": "plugin-role-@test",
"db_name": "plugin-test",
"revocation_statements": nil,
},
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

if testCredsExist(t, credsResp.Data, connURL) {
t.Fatalf("Creds should not exist")
}
}
assert.Equal(t, 9, len(eventSender.Events))

assertEvent := func(t *testing.T, typ, name, path string) {
t.Helper()
assert.Equal(t, typ, string(eventSender.Events[0].Type))
assert.Equal(t, name, eventSender.Events[0].Event.Metadata.AsMap()["name"])
assert.Equal(t, path, eventSender.Events[0].Event.Metadata.AsMap()["path"])
eventSender.Events = slices.Delete(eventSender.Events, 0, 1)
}

assertEvent(t, "database/config-write", "plugin-test", "config/plugin-test")
for i := 0; i < 3; i++ {
assertEvent(t, "database/role-update", "plugin-role-@test", "roles/plugin-role-@test")
assertEvent(t, "database/creds-create", "plugin-role-@test", "creds/plugin-role-@test")
}
assertEvent(t, "database/creds-create", "plugin-role-@test", "creds/plugin-role-@test")
assertEvent(t, "database/role-delete", "plugin-role-@test", "roles/plugin-role-@test")
}

// singletonDBFactory allows us to reach into the internals of a databaseBackend
// even when it's been created by a call to the sys mount. The factory method
// satisfies the logical.Factory type, and lazily creates the databaseBackend
Expand Down
4 changes: 2 additions & 2 deletions builtin/logical/database/path_creds_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
func pathCredsCreate(b *databaseBackend) []*framework.Path {
return []*framework.Path{
{
Pattern: "creds/" + framework.GenericNameRegex("name"),
Pattern: "creds/" + framework.GenericNameWithAtRegex("name"),

DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixDatabase,
Expand All @@ -41,7 +41,7 @@ func pathCredsCreate(b *databaseBackend) []*framework.Path {
HelpDescription: pathCredsCreateReadHelpDesc,
},
{
Pattern: "static-creds/" + framework.GenericNameRegex("name"),
Pattern: "static-creds/" + framework.GenericNameWithAtRegex("name"),

DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixDatabase,
Expand Down
6 changes: 3 additions & 3 deletions builtin/logical/database/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func pathListRoles(b *databaseBackend) []*framework.Path {
func pathRoles(b *databaseBackend) []*framework.Path {
return []*framework.Path{
{
Pattern: "roles/" + framework.GenericNameRegex("name"),
Pattern: "roles/" + framework.GenericNameWithAtRegex("name"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixDatabase,
OperationSuffix: "role",
Expand All @@ -84,7 +84,7 @@ func pathRoles(b *databaseBackend) []*framework.Path {
},

{
Pattern: "static-roles/" + framework.GenericNameRegex("name"),
Pattern: "static-roles/" + framework.GenericNameWithAtRegex("name"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixDatabase,
OperationSuffix: "static-role",
Expand Down Expand Up @@ -1189,7 +1189,7 @@ and "}}" to be replaced.

* "password" - The random password generated for the DB user. Populated if the
static role's credential_type is 'password'.

* "public_key" - The public key generated for the DB user. Populated if the
static role's credential_type is 'rsa_private_key'.

Expand Down
44 changes: 44 additions & 0 deletions builtin/logical/database/path_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,50 @@ func TestBackend_StaticRole_Role_name_check(t *testing.T) {
if resp == nil || !resp.IsError() {
t.Fatalf("expected error, got none")
}

// create a role with a @ in name expect success
data = map[string]interface{}{
"name": "plugin-role-@test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"default_ttl": "5m",
"max_ttl": "10m",
}

req = &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/plugin-role-@test",
Storage: config.StorageView,
Data: data,
}

resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

// create a static role with a @ in name expect success
data = map[string]interface{}{
"name": "plugin-role-@test-2",
"db_name": "plugin-test",
"rotation_statements": testRoleStaticUpdate,
"username": dbUser,
"rotation_period": "1h",
}

req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-@test-2",
Storage: config.StorageView,
Data: data,
}

resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
}

// TestStaticRole_NewCredentialGeneration verifies that new
Expand Down
4 changes: 2 additions & 2 deletions builtin/logical/database/path_rotate_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func pathRotateRootCredentials(b *databaseBackend) []*framework.Path {
HelpDescription: pathRotateCredentialsUpdateHelpDesc,
},
{
Pattern: "rotate-role/" + framework.GenericNameRegex("name"),
Pattern: "rotate-role/" + framework.GenericNameWithAtRegex("name"),

DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixDatabase,
Expand Down Expand Up @@ -361,7 +361,7 @@ Request to rotate the root credentials for a certain database connection.
`

const pathRotateCredentialsUpdateHelpDesc = `
This path attempts to rotate the root credentials for the given database.
This path attempts to rotate the root credentials for the given database.
`

const pathRotateRoleCredentialsUpdateHelpSyn = `
Expand Down
23 changes: 23 additions & 0 deletions builtin/logical/database/rotation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,29 @@ func TestBackend_StaticRole_Rotation_basic(t *testing.T) {
},
waitTime: 20 * time.Second,
},
"basic with @ path and rotation_period": {
account: map[string]interface{}{
"username": dbUser,
"rotation_period": "5400s",
},
path: "plugin-role-@test-1",
expected: map[string]interface{}{
"username": dbUser,
"rotation_period": float64(5400),
},
},
"@ path and rotation_schedule is set and expires": {
account: map[string]interface{}{
"username": dbUser,
"rotation_schedule": "*/10 * * * * *",
},
path: "plugin-role-@test-@2",
expected: map[string]interface{}{
"username": dbUser,
"rotation_schedule": "*/10 * * * * *",
},
waitTime: 20 * time.Second,
},
}

for name, tc := range testCases {
Expand Down