diff --git a/builtin/logical/database/backend.go b/builtin/logical/database/backend.go index dc599be8d8..18ecb2946e 100644 --- a/builtin/logical/database/backend.go +++ b/builtin/logical/database/backend.go @@ -108,6 +108,7 @@ func Backend(conf *logical.BackendConfig) *databaseBackend { "config/*", "static-role/*", }, + AllowSnapshotRead: []string{"static-roles/*", "static-roles", "static-creds/*"}, }, Paths: framework.PathAppend( []*framework.Path{ diff --git a/builtin/logical/database/path_roles.go b/builtin/logical/database/path_roles.go index 2d45f7c782..dfe83dd3d6 100644 --- a/builtin/logical/database/path_roles.go +++ b/builtin/logical/database/path_roles.go @@ -92,10 +92,11 @@ func pathRoles(b *databaseBackend) []*framework.Path { Fields: fieldsForType(databaseStaticRolePath), ExistenceCheck: b.pathStaticRoleExistenceCheck, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathStaticRoleRead, - logical.CreateOperation: b.pathStaticRoleCreateUpdate, - logical.UpdateOperation: b.pathStaticRoleCreateUpdate, - logical.DeleteOperation: b.pathStaticRoleDelete, + logical.ReadOperation: b.pathStaticRoleRead, + logical.CreateOperation: b.pathStaticRoleCreateUpdate, + logical.UpdateOperation: b.pathStaticRoleCreateUpdate, + logical.DeleteOperation: b.pathStaticRoleDelete, + logical.RecoverOperation: b.pathStaticRoleRecover, }, HelpSynopsis: pathStaticRoleHelpSyn, @@ -546,6 +547,45 @@ func (b *databaseBackend) pathRoleCreateUpdate(ctx context.Context, req *logical return nil, nil } +func (b *databaseBackend) pathStaticRoleRecover(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + exists, err := b.pathStaticRoleExistenceCheck(ctx, req, data) + if err != nil { + return nil, err + } + + if exists { + return logical.ErrorResponse("cannot recover a static role that already exists"), nil + } + + snapStorage, err := logical.NewSnapshotStorageView(req) + if err != nil { + return nil, fmt.Errorf("failed to create snapshot storage: %s", err) + } + + name := data.Get("name").(string) + if req.RecoverSourcePath != "" { + fd, err := b.RecoverSourcePathFieldData(req) + if err != nil { + return nil, fmt.Errorf("failed to parse the recover source path: %w", err) + } + name = fd.Get("name").(string) + } + + role, err := b.StaticRole(ctx, snapStorage, name) + if err != nil { + return nil, err + } + if role.StaticAccount.Password != "" { + data.Raw["password"] = role.StaticAccount.Password + } + req.Operation = logical.CreateOperation + defer func() { + req.Operation = logical.RecoverOperation + }() + return b.pathStaticRoleCreateUpdate(ctx, req, data) +} + +// ignore-nil-nil-function-check func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { response := &logical.Response{} name := data.Get("name").(string) diff --git a/builtin/logical/database/path_roles_test.go b/builtin/logical/database/path_roles_test.go index 8283b95939..f3cf01ae8d 100644 --- a/builtin/logical/database/path_roles_test.go +++ b/builtin/logical/database/path_roles_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/vault/helper/namespace" postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" + "github.com/hashicorp/vault/sdk/helper/testhelpers/snapshots" "github.com/hashicorp/vault/sdk/logical" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -1494,3 +1495,90 @@ ALTER USER "{{name}}" WITH PASSWORD '{{password}}'; const testRoleStaticUpdateRotation = ` ALTER USER "{{name}}" WITH PASSWORD '{{password}}';GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; ` + +// TestStaticRole_Recover verifies that a role that exists in a snapshot can be +// read, listed, and recovered +func TestStaticRole_Recover(t *testing.T) { + b, storage, mockDB := getBackend(t) + b2, snapStorage, mockDBSnap := getBackend(t) + ctx := context.Background() + defer b.Cleanup(ctx) + defer b2.Cleanup(ctx) + tc := snapshots.NewSnapshotTestCaseWithStorages(t, b, storage, snapStorage) + + configureDBMount(t, storage) + configureDBMount(t, snapStorage) + + createRole(t, b2, snapStorage, mockDBSnap, "hashicorp") + + tc.RunRead(t, "static-roles/hashicorp") + + tc.RunList(t, "static-roles") + + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, nil). + Once() + _, err := tc.DoRecover(t, "static-roles/hashicorp") + require.NoError(t, err) + readStaticCred(t, b, storage, mockDB, "hashicorp") + + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, nil). + Once() + _, err = tc.DoRecover(t, "static-roles/hashicorp-copy", snapshots.WithRecoverSourcePath("static-roles/hashicorp")) + require.NoError(t, err) + readStaticCred(t, b, storage, mockDB, "hashicorp-copy") +} + +// TestStaticRole_RecoverExists verifies that a static role cannot be updated +// via a recover operation, but can be copied to a new role +func TestStaticRole_RecoverExists(t *testing.T) { + b, storage, mockDB := getBackend(t) + b2, snapStorage, mockDBSnap := getBackend(t) + ctx := context.Background() + defer b.Cleanup(ctx) + defer b2.Cleanup(ctx) + tc := snapshots.NewSnapshotTestCaseWithStorages(t, b, storage, snapStorage) + + configureDBMount(t, storage) + configureDBMount(t, snapStorage) + + createRole(t, b2, snapStorage, mockDBSnap, "hashicorp") + createRole(t, b, storage, mockDB, "hashicorp") + + resp, err := tc.DoRecover(t, "static-roles/hashicorp") + require.NoError(t, err) + require.True(t, resp.IsError()) + require.ErrorContains(t, resp.Error(), "cannot recover a static role that already exists") + + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, nil). + Once() + resp, err = tc.DoRecover(t, "static-roles/hashicorp-copy", snapshots.WithRecoverSourcePath("static-roles/hashicorp")) + require.NoError(t, err) + require.False(t, resp.IsError()) + readStaticCred(t, b, storage, mockDB, "hashicorp-copy") +} + +// TestStaticCreds_Recover verifies that static credentials can be read from the +// snapshot without side effects, but they cannot be recovered +func TestStaticCreds_Recover(t *testing.T) { + b, storage, mockDB := getBackend(t) + b2, snapStorage, mockDBSnap := getBackend(t) + ctx := context.Background() + defer b.Cleanup(ctx) + defer b2.Cleanup(ctx) + tc := snapshots.NewSnapshotTestCaseWithStorages(t, b, storage, snapStorage) + + configureDBMount(t, storage) + configureDBMount(t, snapStorage) + + createRole(t, b2, snapStorage, mockDBSnap, "hashicorp") + createRole(t, b, storage, mockDB, "hashicorp") + + rotateRole(t, b, snapStorage, mockDB, "hashicorp") + tc.RunRead(t, "static-creds/hashicorp") + + _, err := tc.DoRecover(t, "static-creds/hashicorp") + require.Error(t, err) +} diff --git a/changelog/_8922.txt b/changelog/_8922.txt new file mode 100644 index 0000000000..a661633320 --- /dev/null +++ b/changelog/_8922.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/database (enterprise): Add support for reading, listing, and recovering static roles from a loaded snapshot. Also add support for reading static credentials from a loaded snapshot. +``` diff --git a/sdk/helper/testhelpers/snapshots/testcase.go b/sdk/helper/testhelpers/snapshots/testcase.go index de4b1fd0af..1b290dcce0 100644 --- a/sdk/helper/testhelpers/snapshots/testcase.go +++ b/sdk/helper/testhelpers/snapshots/testcase.go @@ -33,10 +33,19 @@ var _ logical.SnapshotStorageProvider = (*storageProvider)(nil) // when it receives snapshot operations, without having to do the end-to-end // setup of creating a raft cluster, taking a snapshot, and loading it. func NewSnapshotTestCase(t testing.TB, backend logical.Backend) *SnapshotTestCase { + return NewSnapshotTestCaseWithStorages(t, backend, &logical.InmemStorage{}, &logical.InmemStorage{}) +} + +// NewSnapshotTestCaseWithStorages is used to create a snapshot test case for a +// particular backend, using the provided storage instances. The test case is +// used to ensure that the backend behaves correctly when it receives snapshot +// operations, without having to do the end-to-end setup of creating a raft +// cluster, taking a snapshot, and loading it. +func NewSnapshotTestCaseWithStorages(t testing.TB, backend logical.Backend, regularStorage, snapshotStorage logical.Storage) *SnapshotTestCase { s := &SnapshotTestCase{ backend: backend, - regularStorage: &logical.InmemStorage{}, - snapshotStorage: &logical.InmemStorage{}, + regularStorage: regularStorage, + snapshotStorage: snapshotStorage, } s.storageRouter = logical.NewSnapshotStorageRouter(s.regularStorage, &storageProvider{s.snapshotStorage})