Skip to content

Commit eaf949c

Browse files
VAULT-37633: Database static role recover operations (hashicorp#8922) (hashicorp#8982)
* initial implementation * fix * tests * changelog * fix vet errors * pr comments Co-authored-by: miagilepner <[email protected]>
1 parent 3c459f7 commit eaf949c

File tree

5 files changed

+147
-6
lines changed

5 files changed

+147
-6
lines changed

builtin/logical/database/backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
108108
"config/*",
109109
"static-role/*",
110110
},
111+
AllowSnapshotRead: []string{"static-roles/*", "static-roles", "static-creds/*"},
111112
},
112113
Paths: framework.PathAppend(
113114
[]*framework.Path{

builtin/logical/database/path_roles.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,11 @@ func pathRoles(b *databaseBackend) []*framework.Path {
9292
Fields: fieldsForType(databaseStaticRolePath),
9393
ExistenceCheck: b.pathStaticRoleExistenceCheck,
9494
Callbacks: map[logical.Operation]framework.OperationFunc{
95-
logical.ReadOperation: b.pathStaticRoleRead,
96-
logical.CreateOperation: b.pathStaticRoleCreateUpdate,
97-
logical.UpdateOperation: b.pathStaticRoleCreateUpdate,
98-
logical.DeleteOperation: b.pathStaticRoleDelete,
95+
logical.ReadOperation: b.pathStaticRoleRead,
96+
logical.CreateOperation: b.pathStaticRoleCreateUpdate,
97+
logical.UpdateOperation: b.pathStaticRoleCreateUpdate,
98+
logical.DeleteOperation: b.pathStaticRoleDelete,
99+
logical.RecoverOperation: b.pathStaticRoleRecover,
99100
},
100101

101102
HelpSynopsis: pathStaticRoleHelpSyn,
@@ -546,6 +547,45 @@ func (b *databaseBackend) pathRoleCreateUpdate(ctx context.Context, req *logical
546547
return nil, nil
547548
}
548549

550+
func (b *databaseBackend) pathStaticRoleRecover(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
551+
exists, err := b.pathStaticRoleExistenceCheck(ctx, req, data)
552+
if err != nil {
553+
return nil, err
554+
}
555+
556+
if exists {
557+
return logical.ErrorResponse("cannot recover a static role that already exists"), nil
558+
}
559+
560+
snapStorage, err := logical.NewSnapshotStorageView(req)
561+
if err != nil {
562+
return nil, fmt.Errorf("failed to create snapshot storage: %s", err)
563+
}
564+
565+
name := data.Get("name").(string)
566+
if req.RecoverSourcePath != "" {
567+
fd, err := b.RecoverSourcePathFieldData(req)
568+
if err != nil {
569+
return nil, fmt.Errorf("failed to parse the recover source path: %w", err)
570+
}
571+
name = fd.Get("name").(string)
572+
}
573+
574+
role, err := b.StaticRole(ctx, snapStorage, name)
575+
if err != nil {
576+
return nil, err
577+
}
578+
if role.StaticAccount.Password != "" {
579+
data.Raw["password"] = role.StaticAccount.Password
580+
}
581+
req.Operation = logical.CreateOperation
582+
defer func() {
583+
req.Operation = logical.RecoverOperation
584+
}()
585+
return b.pathStaticRoleCreateUpdate(ctx, req, data)
586+
}
587+
588+
// ignore-nil-nil-function-check
549589
func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
550590
response := &logical.Response{}
551591
name := data.Get("name").(string)

builtin/logical/database/path_roles_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/hashicorp/vault/helper/namespace"
1616
postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql"
1717
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
18+
"github.com/hashicorp/vault/sdk/helper/testhelpers/snapshots"
1819
"github.com/hashicorp/vault/sdk/logical"
1920
"github.com/stretchr/testify/assert"
2021
"github.com/stretchr/testify/mock"
@@ -1494,3 +1495,90 @@ ALTER USER "{{name}}" WITH PASSWORD '{{password}}';
14941495
const testRoleStaticUpdateRotation = `
14951496
ALTER USER "{{name}}" WITH PASSWORD '{{password}}';GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
14961497
`
1498+
1499+
// TestStaticRole_Recover verifies that a role that exists in a snapshot can be
1500+
// read, listed, and recovered
1501+
func TestStaticRole_Recover(t *testing.T) {
1502+
b, storage, mockDB := getBackend(t)
1503+
b2, snapStorage, mockDBSnap := getBackend(t)
1504+
ctx := context.Background()
1505+
defer b.Cleanup(ctx)
1506+
defer b2.Cleanup(ctx)
1507+
tc := snapshots.NewSnapshotTestCaseWithStorages(t, b, storage, snapStorage)
1508+
1509+
configureDBMount(t, storage)
1510+
configureDBMount(t, snapStorage)
1511+
1512+
createRole(t, b2, snapStorage, mockDBSnap, "hashicorp")
1513+
1514+
tc.RunRead(t, "static-roles/hashicorp")
1515+
1516+
tc.RunList(t, "static-roles")
1517+
1518+
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
1519+
Return(v5.UpdateUserResponse{}, nil).
1520+
Once()
1521+
_, err := tc.DoRecover(t, "static-roles/hashicorp")
1522+
require.NoError(t, err)
1523+
readStaticCred(t, b, storage, mockDB, "hashicorp")
1524+
1525+
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
1526+
Return(v5.UpdateUserResponse{}, nil).
1527+
Once()
1528+
_, err = tc.DoRecover(t, "static-roles/hashicorp-copy", snapshots.WithRecoverSourcePath("static-roles/hashicorp"))
1529+
require.NoError(t, err)
1530+
readStaticCred(t, b, storage, mockDB, "hashicorp-copy")
1531+
}
1532+
1533+
// TestStaticRole_RecoverExists verifies that a static role cannot be updated
1534+
// via a recover operation, but can be copied to a new role
1535+
func TestStaticRole_RecoverExists(t *testing.T) {
1536+
b, storage, mockDB := getBackend(t)
1537+
b2, snapStorage, mockDBSnap := getBackend(t)
1538+
ctx := context.Background()
1539+
defer b.Cleanup(ctx)
1540+
defer b2.Cleanup(ctx)
1541+
tc := snapshots.NewSnapshotTestCaseWithStorages(t, b, storage, snapStorage)
1542+
1543+
configureDBMount(t, storage)
1544+
configureDBMount(t, snapStorage)
1545+
1546+
createRole(t, b2, snapStorage, mockDBSnap, "hashicorp")
1547+
createRole(t, b, storage, mockDB, "hashicorp")
1548+
1549+
resp, err := tc.DoRecover(t, "static-roles/hashicorp")
1550+
require.NoError(t, err)
1551+
require.True(t, resp.IsError())
1552+
require.ErrorContains(t, resp.Error(), "cannot recover a static role that already exists")
1553+
1554+
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
1555+
Return(v5.UpdateUserResponse{}, nil).
1556+
Once()
1557+
resp, err = tc.DoRecover(t, "static-roles/hashicorp-copy", snapshots.WithRecoverSourcePath("static-roles/hashicorp"))
1558+
require.NoError(t, err)
1559+
require.False(t, resp.IsError())
1560+
readStaticCred(t, b, storage, mockDB, "hashicorp-copy")
1561+
}
1562+
1563+
// TestStaticCreds_Recover verifies that static credentials can be read from the
1564+
// snapshot without side effects, but they cannot be recovered
1565+
func TestStaticCreds_Recover(t *testing.T) {
1566+
b, storage, mockDB := getBackend(t)
1567+
b2, snapStorage, mockDBSnap := getBackend(t)
1568+
ctx := context.Background()
1569+
defer b.Cleanup(ctx)
1570+
defer b2.Cleanup(ctx)
1571+
tc := snapshots.NewSnapshotTestCaseWithStorages(t, b, storage, snapStorage)
1572+
1573+
configureDBMount(t, storage)
1574+
configureDBMount(t, snapStorage)
1575+
1576+
createRole(t, b2, snapStorage, mockDBSnap, "hashicorp")
1577+
createRole(t, b, storage, mockDB, "hashicorp")
1578+
1579+
rotateRole(t, b, snapStorage, mockDB, "hashicorp")
1580+
tc.RunRead(t, "static-creds/hashicorp")
1581+
1582+
_, err := tc.DoRecover(t, "static-creds/hashicorp")
1583+
require.Error(t, err)
1584+
}

changelog/_8922.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
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.
3+
```

sdk/helper/testhelpers/snapshots/testcase.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,19 @@ var _ logical.SnapshotStorageProvider = (*storageProvider)(nil)
3333
// when it receives snapshot operations, without having to do the end-to-end
3434
// setup of creating a raft cluster, taking a snapshot, and loading it.
3535
func NewSnapshotTestCase(t testing.TB, backend logical.Backend) *SnapshotTestCase {
36+
return NewSnapshotTestCaseWithStorages(t, backend, &logical.InmemStorage{}, &logical.InmemStorage{})
37+
}
38+
39+
// NewSnapshotTestCaseWithStorages is used to create a snapshot test case for a
40+
// particular backend, using the provided storage instances. The test case is
41+
// used to ensure that the backend behaves correctly when it receives snapshot
42+
// operations, without having to do the end-to-end setup of creating a raft
43+
// cluster, taking a snapshot, and loading it.
44+
func NewSnapshotTestCaseWithStorages(t testing.TB, backend logical.Backend, regularStorage, snapshotStorage logical.Storage) *SnapshotTestCase {
3645
s := &SnapshotTestCase{
3746
backend: backend,
38-
regularStorage: &logical.InmemStorage{},
39-
snapshotStorage: &logical.InmemStorage{},
47+
regularStorage: regularStorage,
48+
snapshotStorage: snapshotStorage,
4049
}
4150

4251
s.storageRouter = logical.NewSnapshotStorageRouter(s.regularStorage, &storageProvider{s.snapshotStorage})

0 commit comments

Comments
 (0)