diff --git a/builtin/logical/database/backend_test.go b/builtin/logical/database/backend_test.go index 1cb10575fac..ad2417d6c35 100644 --- a/builtin/logical/database/backend_test.go +++ b/builtin/logical/database/backend_test.go @@ -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 diff --git a/builtin/logical/database/path_creds_create.go b/builtin/logical/database/path_creds_create.go index 43d215f7718..24d0d454f4d 100644 --- a/builtin/logical/database/path_creds_create.go +++ b/builtin/logical/database/path_creds_create.go @@ -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, @@ -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, diff --git a/builtin/logical/database/path_roles.go b/builtin/logical/database/path_roles.go index dd47eaabdda..abc69e388b5 100644 --- a/builtin/logical/database/path_roles.go +++ b/builtin/logical/database/path_roles.go @@ -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", @@ -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", @@ -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'. diff --git a/builtin/logical/database/path_roles_test.go b/builtin/logical/database/path_roles_test.go index 85cd4cd1763..9dd382d9577 100644 --- a/builtin/logical/database/path_roles_test.go +++ b/builtin/logical/database/path_roles_test.go @@ -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 diff --git a/builtin/logical/database/path_rotate_credentials.go b/builtin/logical/database/path_rotate_credentials.go index a11a045cbf3..8c2315c7f91 100644 --- a/builtin/logical/database/path_rotate_credentials.go +++ b/builtin/logical/database/path_rotate_credentials.go @@ -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, @@ -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 = ` diff --git a/builtin/logical/database/rotation_test.go b/builtin/logical/database/rotation_test.go index 251d8d14687..8f7c13ccf21 100644 --- a/builtin/logical/database/rotation_test.go +++ b/builtin/logical/database/rotation_test.go @@ -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 {