Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port: Premature Rotation For autorotate #12563

Merged
merged 29 commits into from Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b90bbd2
port of ldap fix for early cred rotation
HridoyRoy Sep 15, 2021
b4fa49a
some more porting
HridoyRoy Sep 15, 2021
f603227
another couple lines to port
HridoyRoy Sep 15, 2021
ade88b4
final commits before report
HridoyRoy Sep 15, 2021
cd4b897
remove deadlock
HridoyRoy Sep 15, 2021
61882c1
needs testing
HridoyRoy Sep 15, 2021
73fc684
updates
HridoyRoy Sep 16, 2021
01eae5d
Sync with OpenLDAP PR
tomhjp Sep 16, 2021
6756417
Update the update error handling for items not found in the queue
tomhjp Sep 17, 2021
584c65c
WIP unit tests
tomhjp Sep 17, 2021
a5de7c3
throw error on role creation failure
HridoyRoy Sep 17, 2021
b1c8a06
do not swallow error on role creation
HridoyRoy Sep 17, 2021
271831e
Merge branch 'vault-2485' of github.com:hashicorp/vault into vault-2485
HridoyRoy Sep 17, 2021
a85bda7
comment out wip tests and add in a test for disallowed role
HridoyRoy Sep 17, 2021
b07aa53
Use newly generated password in WAL
HridoyRoy Sep 17, 2021
7649297
return err on popFromRotationQueueByKey error; cleanup on setStaticAc…
calvn Sep 20, 2021
747e0ee
test: fix TestPlugin_lifecycle
calvn Sep 20, 2021
9cd86cb
Uncomment and fix unit tests
tomhjp Sep 21, 2021
63c3d0c
Undo logging updates
tomhjp Sep 21, 2021
45af160
Add changelog
tomhjp Sep 21, 2021
0c01226
Remove ticker and background threads from WAL tests
tomhjp Sep 21, 2021
166fe7d
Keep pre-existing API behaviour of allowing update static role to act…
tomhjp Sep 21, 2021
5b88f40
Switch test back to update operation
tomhjp Sep 21, 2021
a6765ff
Revert my revert, and fix some test bugs
tomhjp Sep 21, 2021
52137e6
Fix TestBackend_StaticRole_LockRegression
tomhjp Sep 21, 2021
83e9387
clean up defer on TestPlugin_lifecycle
calvn Sep 21, 2021
7c29a76
unwrap reqs on cleanup
calvn Sep 21, 2021
dec64cd
setStaticAccount: don't hold a write lock
calvn Sep 21, 2021
9e0c962
TestStoredWALsCorrectlyProcessed: set replication state to unknown
calvn Sep 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 48 additions & 12 deletions builtin/logical/database/path_roles.go
Expand Up @@ -6,6 +6,7 @@ import (
"strings"
"time"

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/strutil"
v4 "github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/framework"
Expand Down Expand Up @@ -215,7 +216,28 @@ func (b *databaseBackend) pathStaticRoleDelete(ctx context.Context, req *logical
return nil, err
}

return nil, nil
walIDs, err := framework.ListWAL(ctx, req.Storage)
if err != nil {
return nil, err
}
var merr *multierror.Error
for _, walID := range walIDs {
wal, err := b.findStaticWAL(ctx, req.Storage, walID)
if err != nil {
merr = multierror.Append(merr, err)
continue
}
if wal != nil && name == wal.RoleName {
b.Logger().Debug("deleting WAL for deleted role", "WAL ID", walID, "role", name)
err = framework.DeleteWAL(ctx, req.Storage, walID)
if err != nil {
b.Logger().Debug("failed to delete WAL for deleted role", "WAL ID", walID, "error", err)
merr = multierror.Append(merr, err)
}
}
}

return nil, merr.ErrorOrNil()
}

func (b *databaseBackend) pathStaticRoleRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
Expand Down Expand Up @@ -482,19 +504,34 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l

// Only call setStaticAccount if we're creating the role for the
// first time
var item *queue.Item
switch req.Operation {
case logical.CreateOperation:
// setStaticAccount calls Storage.Put and saves the role to storage
resp, err := b.setStaticAccount(ctx, req.Storage, &setStaticAccountInput{
RoleName: name,
Role: role,
CreateUser: createRole,
RoleName: name,
Role: role,
})
if err != nil {
if resp != nil && resp.WALID != "" {
b.Logger().Debug("deleting WAL for failed role creation", "WAL ID", resp.WALID, "role", name)
walDeleteErr := framework.DeleteWAL(ctx, req.Storage, resp.WALID)
if walDeleteErr != nil {
b.Logger().Debug("failed to delete WAL for failed role creation", "WAL ID", resp.WALID, "error", walDeleteErr)
var merr *multierror.Error
merr = multierror.Append(merr, err)
merr = multierror.Append(merr, fmt.Errorf("failed to clean up WAL from failed role creation: %w", walDeleteErr))
err = merr.ErrorOrNil()
}
}
tomhjp marked this conversation as resolved.
Show resolved Hide resolved

return nil, err
}
// guard against RotationTime not being set or zero-value
lvr = resp.RotationTime
item = &queue.Item{
Key: name,
}
case logical.UpdateOperation:
// store updated Role
entry, err := logical.StorageEntryJSON(databaseStaticRolePath+name, role)
Expand All @@ -504,17 +541,16 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}

// In case this is an update, remove any previous version of the item from
// the queue
b.popFromRotationQueueByKey(name)
item, err = b.popFromRotationQueueByKey(name)
if err != nil {
return nil, err
}
}

item.Priority = lvr.Add(role.StaticAccount.RotationPeriod).Unix()

// Add their rotation to the queue
if err := b.pushItem(&queue.Item{
Key: name,
Priority: lvr.Add(role.StaticAccount.RotationPeriod).Unix(),
}); err != nil {
if err := b.pushItem(item); err != nil {
return nil, err
}

Expand Down
151 changes: 148 additions & 3 deletions builtin/logical/database/path_roles_test.go
Expand Up @@ -3,13 +3,16 @@ package database
import (
"context"
"errors"
"strings"
"testing"
"time"

"github.com/go-test/deep"
"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/logical"
"github.com/stretchr/testify/mock"
)

var dataKeys = []string{"username", "password", "last_vault_rotation", "rotation_period"}
Expand Down Expand Up @@ -43,7 +46,7 @@ func TestBackend_StaticRole_Config(t *testing.T) {
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"allowed_roles": []string{"plugin-role-test"},
"name": "plugin-test",
}
req := &logical.Request{
Expand All @@ -61,6 +64,7 @@ func TestBackend_StaticRole_Config(t *testing.T) {
// ordering, so each case cleans up by deleting the role
testCases := map[string]struct {
account map[string]interface{}
path string
expected map[string]interface{}
err error
}{
Expand All @@ -69,6 +73,7 @@ func TestBackend_StaticRole_Config(t *testing.T) {
"username": dbUser,
"rotation_period": "5400s",
},
path: "plugin-role-test",
expected: map[string]interface{}{
"username": dbUser,
"rotation_period": float64(5400),
Expand All @@ -78,7 +83,16 @@ func TestBackend_StaticRole_Config(t *testing.T) {
account: map[string]interface{}{
"username": dbUser,
},
err: errors.New("rotation_period is required to create static accounts"),
path: "plugin-role-test",
err: errors.New("rotation_period is required to create static accounts"),
},
"disallowed role config": {
account: map[string]interface{}{
"username": dbUser,
"rotation_period": "5400s",
},
path: "disallowed-role",
err: errors.New("\"disallowed-role\" is not an allowed role"),
},
}

Expand All @@ -94,9 +108,11 @@ func TestBackend_StaticRole_Config(t *testing.T) {
data[k] = v
}

path := "static-roles/" + tc.path

req := &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test",
Path: path,
Storage: config.StorageView,
Data: data,
}
Expand Down Expand Up @@ -516,6 +532,135 @@ func TestBackend_StaticRole_Role_name_check(t *testing.T) {
}
}

func TestWALsStillTrackedAfterUpdate(t *testing.T) {
ctx := context.Background()
b, storage, mockDB := getBackend(t)
defer b.Cleanup(ctx)
configureDBMount(t, storage)

createRole(t, b, storage, mockDB, "hashicorp")

generateWALFromFailedRotation(t, b, storage, mockDB, "hashicorp")
requireWALs(t, storage, 1)

resp, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "static-roles/hashicorp",
Storage: storage,
Data: map[string]interface{}{
"username": "hashicorp",
"dn": "uid=hashicorp,ou=users,dc=hashicorp,dc=com",
"rotation_period": "600s",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatal(resp, err)
}
walIDs := requireWALs(t, storage, 1)

// Now when we trigger a manual rotate, it should use the WAL's new password
// which will tell us that the in-memory structure still kept track of the
// WAL in addition to it still being in storage.
wal, err := b.findStaticWAL(ctx, storage, walIDs[0])
if err != nil {
t.Fatal(err)
}
rotateRole(t, b, storage, mockDB, "hashicorp")
role, err := b.StaticRole(ctx, storage, "hashicorp")
if err != nil {
t.Fatal(err)
}
if role.StaticAccount.Password != wal.NewPassword {
t.Fatal()
}
requireWALs(t, storage, 0)
}

func TestWALsDeletedOnRoleCreationFailed(t *testing.T) {
ctx := context.Background()
b, storage, mockDB := getBackend(t)
defer b.Cleanup(ctx)
configureDBMount(t, storage)

for i := 0; i < 3; i++ {
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
Return(v5.UpdateUserResponse{}, errors.New("forced error")).
Once()
resp, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/hashicorp",
Storage: storage,
Data: map[string]interface{}{
"username": "hashicorp",
"db_name": "mockv5",
"rotation_period": "5s",
},
})
if err == nil {
t.Fatal("expected error from DB")
}
if !strings.Contains(err.Error(), "forced error") {
t.Fatal("expected forced error message", resp, err)
}
}

requireWALs(t, storage, 0)
}

func TestWALsDeletedOnRoleDeletion(t *testing.T) {
ctx := context.Background()
b, storage, mockDB := getBackend(t)
defer b.Cleanup(ctx)
configureDBMount(t, storage)

// Create the roles
roleNames := []string{"hashicorp", "2"}
for _, roleName := range roleNames {
createRole(t, b, storage, mockDB, roleName)
}

// Fail to rotate the roles
for _, roleName := range roleNames {
generateWALFromFailedRotation(t, b, storage, mockDB, roleName)
}

// Should have 2 WALs hanging around
requireWALs(t, storage, 2)

// Delete one of the static roles
resp, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.DeleteOperation,
Path: "static-roles/hashicorp",
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatal(resp, err)
}

// 1 WAL should be cleared by the delete
requireWALs(t, storage, 1)
}

func createRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) {
t.Helper()
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
Return(v5.UpdateUserResponse{}, nil).
Once()
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/" + roleName,
Storage: storage,
Data: map[string]interface{}{
"username": roleName,
"db_name": "mockv5",
"rotation_period": "86400s",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatal(resp, err)
}
}

const testRoleStaticCreate = `
CREATE ROLE "{{name}}" WITH
LOGIN
Expand Down
17 changes: 14 additions & 3 deletions builtin/logical/database/path_rotate_credentials.go
Expand Up @@ -169,10 +169,14 @@ func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationF
}
}

resp, err := b.setStaticAccount(ctx, req.Storage, &setStaticAccountInput{
input := &setStaticAccountInput{
RoleName: name,
Role: role,
})
}
if walID, ok := item.Value.(string); ok {
input.WALID = walID
}
resp, err := b.setStaticAccount(ctx, req.Storage, input)
// if err is not nil, we need to attempt to update the priority and place
// this item back on the queue. The err should still be returned at the end
// of this method.
Expand All @@ -188,15 +192,22 @@ func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationF
}
} else {
item.Priority = resp.RotationTime.Add(role.StaticAccount.RotationPeriod).Unix()
// Clear any stored WAL ID as we must have successfully deleted our WAL to get here.
item.Value = ""
}

// Add their rotation to the queue
if err := b.pushItem(item); err != nil {
return nil, err
}

if err != nil {
return nil, fmt.Errorf("unable to finish rotating credentials; retries will "+
"continue in the background but it is also safe to retry manually: %w", err)
}

// return any err from the setStaticAccount call
return nil, err
return nil, nil
}
}

Expand Down