Skip to content

Commit

Permalink
flatten check-out handler (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyrannosaurus-becks authored Oct 8, 2019
1 parent e3ac74b commit 7ebec06
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 309 deletions.
5 changes: 2 additions & 3 deletions plugin/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ func newBackend(client secretsClient) *backend {
roleCache: cache.New(roleCacheExpiration, roleCacheCleanup),
credCache: cache.New(credCacheExpiration, credCacheCleanup),
rotateRootLock: new(int32),
checkOutHandler: &PasswordHandler{ // TODO the object model may change here but we do need to place something realistic here for testing
checkOutHandler: &checkOutHandler{
client: client,
child: &StorageHandler{},
},
checkOutLocks: locksutil.CreateLocks(),
}
Expand Down Expand Up @@ -75,7 +74,7 @@ type backend struct {
credLock sync.Mutex
rotateRootLock *int32

checkOutHandler CheckOutHandler
checkOutHandler *checkOutHandler
// checkOutLocks are used for avoiding races
// when working with sets through the check-out system.
checkOutLocks []*locksutil.LockEntry
Expand Down
195 changes: 195 additions & 0 deletions plugin/checkout_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package plugin

import (
"context"
"errors"
"time"

"github.com/hashicorp/vault-plugin-secrets-ad/plugin/util"
"github.com/hashicorp/vault/sdk/logical"
)

const (
checkoutStoragePrefix = "checkout/"
passwordStoragePrefix = "password/"
)

var (
// errCheckedOut is returned when a check-out request is received
// for a service account that's already checked out.
errCheckedOut = errors.New("checked out")

// errNotFound is used when a requested item doesn't exist.
errNotFound = errors.New("not found")
)

// CheckOut provides information for a service account that is currently
// checked out.
type CheckOut struct {
IsAvailable bool `json:"is_available"`
BorrowerEntityID string `json:"borrower_entity_id"`
BorrowerClientToken string `json:"borrower_client_token"`
Due time.Time `json:"due"`
}

// checkOutHandler manages checkouts. It's not thread-safe and expects the caller to handle locking because
// locking may span multiple calls.
type checkOutHandler struct {
client secretsClient
}

// CheckOut attempts to check out a service account. If the account is unavailable, it returns
// errCheckedOut. If the service account isn't managed by this plugin, it returns
// errNotFound.
func (h *checkOutHandler) CheckOut(ctx context.Context, storage logical.Storage, serviceAccountName string, checkOut *CheckOut) error {
if ctx == nil {
return errors.New("ctx must be provided")
}
if storage == nil {
return errors.New("storage must be provided")
}
if serviceAccountName == "" {
return errors.New("service account name must be provided")
}
if checkOut == nil {
return errors.New("check-out must be provided")
}

// Check if the service account is currently checked out.
currentEntry, err := storage.Get(ctx, checkoutStoragePrefix+serviceAccountName)
if err != nil {
return err
}
if currentEntry == nil {
return errNotFound
}
currentCheckOut := &CheckOut{}
if err := currentEntry.DecodeJSON(currentCheckOut); err != nil {
return err
}
if !currentCheckOut.IsAvailable {
return errCheckedOut
}

// Since it's not, store the new check-out.
entry, err := logical.StorageEntryJSON(checkoutStoragePrefix+serviceAccountName, checkOut)
if err != nil {
return err
}
return storage.Put(ctx, entry)
}

// CheckIn attempts to check in a service account. If an error occurs, the account remains checked out
// and can either be retried by the caller, or eventually may be checked in if it has a ttl
// that ends.
func (h *checkOutHandler) CheckIn(ctx context.Context, storage logical.Storage, serviceAccountName string) error {
if ctx == nil {
return errors.New("ctx must be provided")
}
if storage == nil {
return errors.New("storage must be provided")
}
if serviceAccountName == "" {
return errors.New("service account name must be provided")
}

// On check-ins, a new AD password is generated, updated in AD, and stored.
engineConf, err := readConfig(ctx, storage)
if err != nil {
return err
}
if engineConf == nil {
return errors.New("the config is currently unset")
}
newPassword, err := util.GeneratePassword(engineConf.PasswordConf.Formatter, engineConf.PasswordConf.Length)
if err != nil {
return err
}
if err := h.client.UpdatePassword(engineConf.ADConf, serviceAccountName, newPassword); err != nil {
return err
}
pwdEntry, err := logical.StorageEntryJSON(passwordStoragePrefix+serviceAccountName, newPassword)
if err != nil {
return err
}
if err := storage.Put(ctx, pwdEntry); err != nil {
return err
}

// That ends the password-handling leg of our journey, now let's deal with the stored check-out itself.
// Store a check-out status indicating it's available.
checkOut := &CheckOut{
IsAvailable: true,
}
entry, err := logical.StorageEntryJSON(checkoutStoragePrefix+serviceAccountName, checkOut)
if err != nil {
return err
}
return storage.Put(ctx, entry)
}

// LoadCheckOut returns either:
// - A *CheckOut and nil error if the serviceAccountName is currently managed by this engine.
// - A nil *Checkout and errNotFound if the serviceAccountName is not currently managed by this engine.
func (h *checkOutHandler) LoadCheckOut(ctx context.Context, storage logical.Storage, serviceAccountName string) (*CheckOut, error) {
if ctx == nil {
return nil, errors.New("ctx must be provided")
}
if storage == nil {
return nil, errors.New("storage must be provided")
}
if serviceAccountName == "" {
return nil, errors.New("service account name must be provided")
}

entry, err := storage.Get(ctx, checkoutStoragePrefix+serviceAccountName)
if err != nil {
return nil, err
}
if entry == nil {
return nil, errNotFound
}
checkOut := &CheckOut{}
if err := entry.DecodeJSON(checkOut); err != nil {
return nil, err
}
return checkOut, nil
}

// Delete cleans up anything we were tracking from the service account that we will no longer need.
func (h *checkOutHandler) Delete(ctx context.Context, storage logical.Storage, serviceAccountName string) error {
if ctx == nil {
return errors.New("ctx must be provided")
}
if storage == nil {
return errors.New("storage must be provided")
}
if serviceAccountName == "" {
return errors.New("service account name must be provided")
}

if err := storage.Delete(ctx, passwordStoragePrefix+serviceAccountName); err != nil {
return err
}
return storage.Delete(ctx, checkoutStoragePrefix+serviceAccountName)
}

// retrievePassword is a utility function for grabbing a service account's password from storage.
// retrievePassword will return:
// - "password", nil if it was successfully able to retrieve the password.
// - errNotFound if there's no password presently.
// - Some other err if it was unable to complete successfully.
func retrievePassword(ctx context.Context, storage logical.Storage, serviceAccountName string) (string, error) {
entry, err := storage.Get(ctx, passwordStoragePrefix+serviceAccountName)
if err != nil {
return "", err
}
if entry == nil {
return "", errNotFound
}
password := ""
if err := entry.DecodeJSON(&password); err != nil {
return "", err
}
return password, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ func setup() (context.Context, logical.Storage, string, *CheckOut) {
return ctx, storage, serviceAccountName, checkOut
}

func Test_StorageHandler(t *testing.T) {
func TestCheckOutHandlerStorageLayer(t *testing.T) {
ctx, storage, serviceAccountName, testCheckOut := setup()

storageHandler := &StorageHandler{}
storageHandler := &checkOutHandler{
client: &fakeSecretsClient{},
}

// Service accounts must initially be checked in to the library
if err := storageHandler.CheckIn(ctx, storage, serviceAccountName); err != nil {
Expand All @@ -48,7 +50,7 @@ func Test_StorageHandler(t *testing.T) {
}

// We should have the testCheckOut in storage now.
storedCheckOut, err := storageHandler.Status(ctx, storage, serviceAccountName)
storedCheckOut, err := storageHandler.LoadCheckOut(ctx, storage, serviceAccountName)
if err != nil {
t.Fatal(err)
}
Expand All @@ -63,8 +65,8 @@ func Test_StorageHandler(t *testing.T) {
// get a CurrentlyCheckedOutErr.
if err := storageHandler.CheckOut(ctx, storage, serviceAccountName, testCheckOut); err == nil {
t.Fatal("expected err but received none")
} else if err != ErrCheckedOut {
t.Fatalf("expected ErrCheckedOut, but received %s", err)
} else if err != errCheckedOut {
t.Fatalf("expected errCheckedOut, but received %s", err)
}

// If we try to check something in, it should succeed.
Expand All @@ -73,7 +75,7 @@ func Test_StorageHandler(t *testing.T) {
}

// We should no longer have the testCheckOut in storage.
storedCheckOut, err = storageHandler.Status(ctx, storage, serviceAccountName)
storedCheckOut, err = storageHandler.LoadCheckOut(ctx, storage, serviceAccountName)
if err != nil {
t.Fatal(err)
}
Expand All @@ -92,37 +94,16 @@ func Test_StorageHandler(t *testing.T) {
}
}

func TestValidateInputs(t *testing.T) {
ctx, storage, serviceAccountName, checkOut := setup()

// Failure cases.
if err := validateInputs(nil, storage, serviceAccountName, checkOut, true); err == nil {
t.Fatal("expected err because ctx isn't provided")
}
if err := validateInputs(ctx, nil, serviceAccountName, checkOut, true); err == nil {
t.Fatal("expected err because storage isn't provided")
}
if err := validateInputs(ctx, storage, "", checkOut, true); err == nil {
t.Fatal("expected err because serviceAccountName isn't provided")
}
if err := validateInputs(ctx, storage, serviceAccountName, nil, true); err == nil {
t.Fatal("expected err because checkOut isn't provided")
}
// Success cases.
if err := validateInputs(ctx, storage, serviceAccountName, checkOut, true); err != nil {
t.Fatal(err)
}
if err := validateInputs(ctx, storage, serviceAccountName, nil, false); err != nil {
t.Fatal(err)
}
}

func TestPasswordHandlerInterfaceFulfillment(t *testing.T) {
ctx, storage, serviceAccountName, checkOut := setup()

passwordHandler := &PasswordHandler{
passwordHandler := &checkOutHandler{
client: &fakeSecretsClient{},
child: &fakeCheckOutHandler{},
}

// We must always start managing a service account by checking it in.
if err := passwordHandler.CheckIn(ctx, storage, serviceAccountName); err != nil {
t.Fatal(err)
}

// There should be no error during check-out.
Expand All @@ -131,9 +112,9 @@ func TestPasswordHandlerInterfaceFulfillment(t *testing.T) {
}

// The password should get rotated successfully during check-in.
_, err := retrievePassword(ctx, storage, serviceAccountName)
if err != ErrNotFound {
t.Fatal("expected ErrNotFound")
origPassword, err := retrievePassword(ctx, storage, serviceAccountName)
if err != nil {
t.Fatal(err)
}
if err := passwordHandler.CheckIn(ctx, storage, serviceAccountName); err != nil {
t.Fatal(err)
Expand All @@ -142,7 +123,7 @@ func TestPasswordHandlerInterfaceFulfillment(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if currPassword == "" {
if currPassword == "" || currPassword == origPassword {
t.Fatal("expected password, but received none")
}

Expand All @@ -152,36 +133,15 @@ func TestPasswordHandlerInterfaceFulfillment(t *testing.T) {
}

currPassword, err = retrievePassword(ctx, storage, serviceAccountName)
if err != ErrNotFound {
t.Fatal("expected ErrNotFound")
if err != errNotFound {
t.Fatal("expected errNotFound")
}
checkOut, err = passwordHandler.Status(ctx, storage, serviceAccountName)
if err != nil {
t.Fatal(err)

checkOut, err = passwordHandler.LoadCheckOut(ctx, storage, serviceAccountName)
if err != errNotFound {
t.Fatal("expected err not found")
}
if checkOut != nil {
t.Fatal("expected checkOut to be nil")
}
}

type fakeCheckOutHandler struct{}

func (f *fakeCheckOutHandler) CheckOut(ctx context.Context, storage logical.Storage, serviceAccountName string, checkOut *CheckOut) error {
return nil
}

func (f *fakeCheckOutHandler) Renew(ctx context.Context, storage logical.Storage, serviceAccountName string, updatedCheckOut *CheckOut) error {
return nil
}

func (f *fakeCheckOutHandler) CheckIn(ctx context.Context, storage logical.Storage, serviceAccountName string) error {
return nil
}

func (f *fakeCheckOutHandler) Delete(ctx context.Context, storage logical.Storage, serviceAccountName string) error {
return nil
}

func (f *fakeCheckOutHandler) Status(ctx context.Context, storage logical.Storage, serviceAccountName string) (*CheckOut, error) {
return nil, nil
}
Loading

0 comments on commit 7ebec06

Please sign in to comment.