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

Backport 1.8.x: Port: Premature Rotation For autorotate (#12563) #12606

Merged
merged 1 commit into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

"github.com/hashicorp/go-multierror"
v4 "github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/locksutil"
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()
}
}

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
Original file line number Diff line number Diff line change
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 @@ -510,6 +526,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
Original file line number Diff line number Diff line change
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
Loading