Skip to content

Commit fb71a62

Browse files
Backport of db: consider possibility of NextVaultRotation being unset on queue population (VAULT-35639) into release/1.19.x (#30430)
* backport of commit 294c304 * replace context --------- Co-authored-by: Ellie <[email protected]>
1 parent 6ea8432 commit fb71a62

File tree

5 files changed

+160
-0
lines changed

5 files changed

+160
-0
lines changed

builtin/logical/database/path_roles.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,14 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
763763
Key: name,
764764
}
765765
case logical.UpdateOperation:
766+
// if lastVaultRotation is zero, the role had `skip_import_rotation` set
767+
if lastVaultRotation.IsZero() {
768+
lastVaultRotation = time.Now()
769+
}
770+
771+
// Ensure that NextVaultRotation is recalculated in case the rotation period changed
772+
role.StaticAccount.SetNextVaultRotation(lastVaultRotation)
773+
766774
// store updated Role
767775
err := b.StoreStaticRole(ctx, req.Storage, role)
768776
if err != nil {

builtin/logical/database/path_roles_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/hashicorp/vault/sdk/logical"
1919
"github.com/stretchr/testify/assert"
2020
"github.com/stretchr/testify/mock"
21+
"github.com/stretchr/testify/require"
2122
)
2223

2324
func TestBackend_Roles_CredentialTypes(t *testing.T) {
@@ -1352,6 +1353,57 @@ func TestIsInsideRotationWindow(t *testing.T) {
13521353
}
13531354
}
13541355

1356+
// TestStaticRoleTTLAfterUpdate tests that a static roles
1357+
// TTL is properly updated after updating rotation period
1358+
// This addresses a bug in which NextVaultRotation was not
1359+
// set on update
1360+
func TestStaticRoleTTLAfterUpdate(t *testing.T) {
1361+
ctx := context.Background()
1362+
b, storage, mockDB := getBackend(t)
1363+
defer b.Cleanup(ctx)
1364+
configureDBMount(t, storage)
1365+
1366+
roleName := "hashicorp"
1367+
data := map[string]interface{}{
1368+
"username": "hashicorp",
1369+
"db_name": "mockv5",
1370+
"rotation_period": "10m",
1371+
}
1372+
1373+
createRoleWithData(t, b, storage, mockDB, roleName, data)
1374+
// read credential
1375+
resp := readStaticCred(t, b, storage, mockDB, roleName)
1376+
var initialTTL float64
1377+
if v, ok := resp.Data["ttl"]; !ok || v == nil {
1378+
require.FailNow(t, "initial ttl should be set")
1379+
} else {
1380+
initialTTL, ok = v.(float64)
1381+
if !ok {
1382+
require.FailNow(t, "expected ttl to be an integer")
1383+
}
1384+
}
1385+
1386+
updateStaticRoleWithData(t, b, storage, mockDB, roleName, map[string]interface{}{
1387+
"username": "hashicorp",
1388+
"db_name": "mockv5",
1389+
"rotation_period": "20m",
1390+
})
1391+
1392+
resp = readStaticCred(t, b, storage, mockDB, roleName)
1393+
var updatedTTL float64
1394+
if v, ok := resp.Data["ttl"]; !ok || v == nil {
1395+
require.FailNow(t, "expected ttl to be set after update")
1396+
} else {
1397+
updatedTTL, ok = v.(float64)
1398+
if !ok {
1399+
require.FailNow(t, "expected ttl to be a float64 after update")
1400+
}
1401+
}
1402+
1403+
require.Greaterf(t, updatedTTL, initialTTL, "expected ttl to be greater than %f, actual value: %f",
1404+
initialTTL, updatedTTL)
1405+
}
1406+
13551407
func createRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) {
13561408
t.Helper()
13571409
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
@@ -1404,6 +1456,31 @@ func readStaticCred(t *testing.T, b *databaseBackend, s logical.Storage, mockDB
14041456
return resp
14051457
}
14061458

1459+
func updateStaticRoleWithData(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string, d map[string]interface{}) {
1460+
t.Helper()
1461+
1462+
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
1463+
Return(v5.UpdateUserResponse{}, nil).
1464+
Once()
1465+
1466+
req := &logical.Request{
1467+
Operation: logical.UpdateOperation,
1468+
Path: "static-roles/" + roleName,
1469+
Storage: storage,
1470+
Data: d,
1471+
}
1472+
1473+
resp, err := b.HandleRequest(context.Background(), req)
1474+
assert.NoError(t, err, "unexpected error")
1475+
if resp != nil {
1476+
assert.NoError(t, resp.Error(), "unexpected error in response")
1477+
}
1478+
1479+
if t.Failed() {
1480+
require.FailNow(t, "failed to update static role: %s", roleName)
1481+
}
1482+
}
1483+
14071484
const testRoleStaticCreate = `
14081485
CREATE ROLE "{{name}}" WITH
14091486
LOGIN

builtin/logical/database/rotation.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ func (b *databaseBackend) populateQueue(ctx context.Context, s logical.Storage)
6464
continue
6565
}
6666

67+
// If an account's NextVaultRotation period is zero time (time.Time{}), it means that the
68+
// role was created before we added the `NextVaultRotation` field. In this
69+
// case, we need to calculate the next rotation time based on the
70+
// LastVaultRotation and the RotationPeriod. However, if the role was
71+
// created with skip_import_rotation set, we need to use the current time
72+
// instead of LastVaultRotation because LastVaultRotation is 0
73+
if role.StaticAccount.NextVaultRotation.IsZero() {
74+
log.Debug("NextVaultRotation unset (zero time). Role may predate field", roleName)
75+
if role.StaticAccount.LastVaultRotation.IsZero() {
76+
log.Debug("Setting NextVaultRotation based on current time", roleName)
77+
role.StaticAccount.SetNextVaultRotation(time.Now())
78+
} else {
79+
log.Debug("Setting NextVaultRotation based on LastVaultRotation", roleName)
80+
role.StaticAccount.SetNextVaultRotation(role.StaticAccount.LastVaultRotation)
81+
}
82+
83+
b.StoreStaticRole(ctx, s, role)
84+
}
85+
6786
item := queue.Item{
6887
Key: roleName,
6988
Priority: role.StaticAccount.NextRotationTime().Unix(),

builtin/logical/database/rotation_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,59 @@ func TestDeletesOlderWALsOnLoad(t *testing.T) {
16201620
requireWALs(t, storage, 1)
16211621
}
16221622

1623+
// TestStaticRoleNextVaultRotationOnRestart verifies that a static role created prior to Vault 1.15.0
1624+
// (when roles were created without NextVaultRotation set) is automatically assigned a valid
1625+
// NextVaultRotation when loaded from storage and the rotation queue is repopulated.
1626+
func TestStaticRoleNextVaultRotationOnRestart(t *testing.T) {
1627+
ctx := context.Background()
1628+
b, storage, mockDB := getBackend(t)
1629+
defer b.Cleanup(ctx)
1630+
configureDBMount(t, storage)
1631+
1632+
roleName := "hashicorp"
1633+
data := map[string]interface{}{
1634+
"username": "hashicorp",
1635+
"db_name": "mockv5",
1636+
"rotation_period": "10m",
1637+
}
1638+
1639+
createRoleWithData(t, b, storage, mockDB, roleName, data)
1640+
item, err := b.credRotationQueue.Pop()
1641+
require.NoError(t, err)
1642+
firstPriority := item.Priority
1643+
role, err := b.StaticRole(context.Background(), storage, roleName)
1644+
firstPassword := role.StaticAccount.Password
1645+
require.NoError(t, err)
1646+
1647+
// force NextVaultRotation to zero to simulate roles before 1.15.0
1648+
role.StaticAccount.NextVaultRotation = time.Time{}
1649+
entry, err := logical.StorageEntryJSON(databaseStaticRolePath+roleName, role)
1650+
require.NoError(t, err)
1651+
if err := storage.Put(context.Background(), entry); err != nil {
1652+
t.Fatal("failed to write role to storage", err)
1653+
}
1654+
1655+
// Confirm that NextVaultRotation is nil
1656+
role, err = b.StaticRole(ctx, storage, roleName)
1657+
require.NoError(t, err)
1658+
require.Equal(t, role.StaticAccount.NextVaultRotation, time.Time{})
1659+
1660+
// Repopulate queue to simulate restart
1661+
b.populateQueue(ctx, storage)
1662+
1663+
success := b.rotateCredential(ctx, storage)
1664+
require.False(t, success, "expected rotation to fail")
1665+
1666+
role, err = b.StaticRole(ctx, storage, roleName)
1667+
require.NoError(t, err)
1668+
require.Equal(t, role.StaticAccount.Password, firstPassword)
1669+
item, err = b.credRotationQueue.Pop()
1670+
require.NoError(t, err)
1671+
newPriority := item.Priority
1672+
require.Equal(t, role.StaticAccount.NextVaultRotation.Unix(), newPriority) // Confirm NextVaultRotation and priority are equal
1673+
require.Equal(t, newPriority, firstPriority) // confirm that priority has not changed
1674+
}
1675+
16231676
func generateWALFromFailedRotation(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) {
16241677
t.Helper()
16251678
mockDB.On("UpdateUser", mock.Anything, mock.Anything).

changelog/30320.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
database: Prevent static roles created in versions prior to 1.15.0 from rotating on backend restart.
3+
```

0 commit comments

Comments
 (0)