diff --git a/go.mod b/go.mod index 342fd30991..09efb4b9b3 100644 --- a/go.mod +++ b/go.mod @@ -75,7 +75,7 @@ require ( github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.2-0.20190826163451-8461c66275a9 github.com/hashicorp/vault-plugin-auth-oci v0.0.0-20190904175623-97c0c0187c5c github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190814210117-e079e01fbb93 - github.com/hashicorp/vault-plugin-secrets-ad v0.5.3-0.20190814210122-0f2fd536b250 + github.com/hashicorp/vault-plugin-secrets-ad v0.6.0 github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.2-0.20190814210129-4d18bec92f56 github.com/hashicorp/vault-plugin-secrets-azure v0.5.2-0.20190814210135-54b8afbc42ae github.com/hashicorp/vault-plugin-secrets-gcp v0.5.3-0.20190814210141-d2086ff79b04 diff --git a/go.sum b/go.sum index 90d8af8148..b00b2a74ac 100644 --- a/go.sum +++ b/go.sum @@ -362,6 +362,8 @@ github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190814210117-e github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190814210117-e079e01fbb93/go.mod h1:N9XpfMXjeLHBgUd8iy4avOC4mCSqUC7B/R8AtCYhcfE= github.com/hashicorp/vault-plugin-secrets-ad v0.5.3-0.20190814210122-0f2fd536b250 h1:+mm2cM5msg/USImbvnMS2yzCMBYMCO3CrvsATWGtHtY= github.com/hashicorp/vault-plugin-secrets-ad v0.5.3-0.20190814210122-0f2fd536b250/go.mod h1:F8hKHqcB7stN2OhnqE3emwFYtKO0IDNxMBbPs2n8vr0= +github.com/hashicorp/vault-plugin-secrets-ad v0.6.0 h1:N0AtdV3w6VCtU7rZiTbPxsxhluJXrzpYH9B1pLZhG6g= +github.com/hashicorp/vault-plugin-secrets-ad v0.6.0/go.mod h1:qm2QDW9KNY+pFoxBEYGYvcHnVjdiOr3tXeO9DMeo3mI= github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.2-0.20190814210129-4d18bec92f56 h1:PGE26//x1eiAbZ1ExffhKa4y9xgDKLd9BHDZRkOzbEY= github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.2-0.20190814210129-4d18bec92f56/go.mod h1:hJ42zFd3bHyE8O2liBUG+VPY0JxdMrj51TOwVGViUIU= github.com/hashicorp/vault-plugin-secrets-azure v0.5.2-0.20190814210135-54b8afbc42ae h1:LtRJy7H/9ftjHGo5SMLG8/7DI7CYL1Zur9jBJTyzXg8= diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/backend.go b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/backend.go index 979b6aaab6..6174e159f6 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/backend.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/backend.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/vault-plugin-secrets-ad/plugin/client" "github.com/hashicorp/vault-plugin-secrets-ad/plugin/util" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/locksutil" "github.com/hashicorp/vault/sdk/logical" "github.com/patrickmn/go-cache" ) func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { backend := newBackend(util.NewSecretsClient(conf.Logger)) - backend.Setup(ctx, conf) + if err := backend.Setup(ctx, conf); err != nil { + return nil, err + } return backend, nil } @@ -24,6 +27,10 @@ func newBackend(client secretsClient) *backend { roleCache: cache.New(roleCacheExpiration, roleCacheCleanup), credCache: cache.New(credCacheExpiration, credCacheCleanup), rotateRootLock: new(int32), + checkOutHandler: &checkOutHandler{ + client: client, + }, + checkOutLocks: locksutil.CreateLocks(), } adBackend.Backend = &framework.Backend{ Help: backendHelp, @@ -33,6 +40,14 @@ func newBackend(client secretsClient) *backend { adBackend.pathListRoles(), adBackend.pathCreds(), adBackend.pathRotateCredentials(), + + // The following paths are for AD credential checkout. + adBackend.pathSetCheckIn(), + adBackend.pathSetManageCheckIn(), + adBackend.pathSetCheckOut(), + adBackend.pathSetStatus(), + adBackend.pathSets(), + adBackend.pathListSets(), }, PathsSpecial: &logical.Paths{ SealWrapStorage: []string{ @@ -42,12 +57,15 @@ func newBackend(client secretsClient) *backend { }, Invalidate: adBackend.Invalidate, BackendType: logical.TypeLogical, + Secrets: []*framework.Secret{ + adBackend.secretAccessKeys(), + }, } return adBackend } type backend struct { - logical.Backend + *framework.Backend client secretsClient @@ -55,6 +73,11 @@ type backend struct { credCache *cache.Cache credLock sync.Mutex rotateRootLock *int32 + + checkOutHandler *checkOutHandler + // checkOutLocks are used for avoiding races + // when working with sets through the check-out system. + checkOutLocks []*locksutil.LockEntry } func (b *backend) Invalidate(ctx context.Context, key string) { diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/checkout_handler.go b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/checkout_handler.go new file mode 100644 index 0000000000..5b4c729aa1 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/checkout_handler.go @@ -0,0 +1,192 @@ +package plugin + +import ( + "context" + "errors" + "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"` +} + +// 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 +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_checkout_sets.go b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_checkout_sets.go new file mode 100644 index 0000000000..f819038946 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_checkout_sets.go @@ -0,0 +1,376 @@ +package plugin + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/locksutil" + "github.com/hashicorp/vault/sdk/helper/strutil" + "github.com/hashicorp/vault/sdk/logical" +) + +const libraryPrefix = "library/" + +type librarySet struct { + ServiceAccountNames []string `json:"service_account_names"` + TTL time.Duration `json:"ttl"` + MaxTTL time.Duration `json:"max_ttl"` + DisableCheckInEnforcement bool `json:"disable_check_in_enforcement"` +} + +// Validates ensures that a set meets our code assumptions that TTLs are set in +// a way that makes sense, and that there's at least one service account. +func (l *librarySet) Validate() error { + if len(l.ServiceAccountNames) < 1 { + return fmt.Errorf(`at least one service account must be configured`) + } + if l.MaxTTL > 0 { + if l.MaxTTL < l.TTL { + return fmt.Errorf(`max_ttl (%d seconds) may not be less than ttl (%d seconds)`, l.MaxTTL, l.TTL) + } + } + return nil +} + +func (b *backend) pathListSets() *framework.Path { + return &framework.Path{ + Pattern: libraryPrefix + "?$", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: b.setListOperation, + }, + }, + HelpSynopsis: pathListSetsHelpSyn, + HelpDescription: pathListSetsHelpDesc, + } +} + +func (b *backend) setListOperation(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + keys, err := req.Storage.List(ctx, libraryPrefix) + if err != nil { + return nil, err + } + return logical.ListResponse(keys), nil +} + +func (b *backend) pathSets() *framework.Path { + return &framework.Path{ + Pattern: libraryPrefix + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeLowerCaseString, + Description: "Name of the set.", + Required: true, + }, + "service_account_names": { + Type: framework.TypeCommaStringSlice, + Description: "The username/logon name for the service accounts with which this set will be associated.", + }, + "ttl": { + Type: framework.TypeDurationSecond, + Description: "In seconds, the amount of time a check-out should last. Defaults to 24 hours.", + Default: 24 * 60 * 60, // 24 hours + }, + "max_ttl": { + Type: framework.TypeDurationSecond, + Description: "In seconds, the max amount of time a check-out's renewals should last. Defaults to 24 hours.", + Default: 24 * 60 * 60, // 24 hours + }, + "disable_check_in_enforcement": { + Type: framework.TypeBool, + Description: "Disable the default behavior of requiring that check-ins are performed by the entity that checked them out.", + Default: false, + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.CreateOperation: &framework.PathOperation{ + Callback: b.operationSetCreate, + Summary: "Create a library set.", + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.operationSetUpdate, + Summary: "Update a library set.", + }, + logical.ReadOperation: &framework.PathOperation{ + Callback: b.operationSetRead, + Summary: "Read a library set.", + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: b.operationSetDelete, + Summary: "Delete a library set.", + }, + }, + ExistenceCheck: b.operationSetExistenceCheck, + HelpSynopsis: setHelpSynopsis, + HelpDescription: setHelpDescription, + } +} + +func (b *backend) operationSetExistenceCheck(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (bool, error) { + set, err := readSet(ctx, req.Storage, fieldData.Get("name").(string)) + if err != nil { + return false, err + } + return set != nil, nil +} + +func (b *backend) operationSetCreate(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := fieldData.Get("name").(string) + + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.Lock() + defer lock.Unlock() + + serviceAccountNames := fieldData.Get("service_account_names").([]string) + ttl := time.Duration(fieldData.Get("ttl").(int)) * time.Second + maxTTL := time.Duration(fieldData.Get("max_ttl").(int)) * time.Second + disableCheckInEnforcement := fieldData.Get("disable_check_in_enforcement").(bool) + + if len(serviceAccountNames) == 0 { + return logical.ErrorResponse(`"service_account_names" must be provided`), nil + } + + // Ensure these service accounts aren't already managed by another check-out set. + for _, serviceAccountName := range serviceAccountNames { + if _, err := b.checkOutHandler.LoadCheckOut(ctx, req.Storage, serviceAccountName); err != nil { + if err == errNotFound { + // This is what we want to see. + continue + } + return nil, err + } + return logical.ErrorResponse(fmt.Sprintf("%q is already managed by another set", serviceAccountName)), nil + } + + set := &librarySet{ + ServiceAccountNames: serviceAccountNames, + TTL: ttl, + MaxTTL: maxTTL, + DisableCheckInEnforcement: disableCheckInEnforcement, + } + if err := set.Validate(); err != nil { + return logical.ErrorResponse(err.Error()), nil + } + for _, serviceAccountName := range serviceAccountNames { + if err := b.checkOutHandler.CheckIn(ctx, req.Storage, serviceAccountName); err != nil { + return nil, err + } + } + if err := storeSet(ctx, req.Storage, setName, set); err != nil { + return nil, err + } + return nil, nil +} + +func (b *backend) operationSetUpdate(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := fieldData.Get("name").(string) + + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.Lock() + defer lock.Unlock() + + newServiceAccountNamesRaw, newServiceAccountNamesSent := fieldData.GetOk("service_account_names") + var newServiceAccountNames []string + if newServiceAccountNamesSent { + newServiceAccountNames = newServiceAccountNamesRaw.([]string) + } + + ttlRaw, ttlSent := fieldData.GetOk("ttl") + if !ttlSent { + ttlRaw = fieldData.Schema["ttl"].Default + } + ttl := time.Duration(ttlRaw.(int)) * time.Second + + maxTTLRaw, maxTTLSent := fieldData.GetOk("max_ttl") + if !maxTTLSent { + maxTTLRaw = fieldData.Schema["max_ttl"].Default + } + maxTTL := time.Duration(maxTTLRaw.(int)) * time.Second + + disableCheckInEnforcementRaw, enforcementSent := fieldData.GetOk("disable_check_in_enforcement") + if !enforcementSent { + disableCheckInEnforcementRaw = false + } + disableCheckInEnforcement := disableCheckInEnforcementRaw.(bool) + + set, err := readSet(ctx, req.Storage, setName) + if err != nil { + return nil, err + } + if set == nil { + return logical.ErrorResponse(fmt.Sprintf(`%q doesn't exist`, setName)), nil + } + + var beingAdded []string + var beingDeleted []string + if newServiceAccountNamesSent { + + // For new service accounts we receive, before we check them in, ensure they're not in another set. + beingAdded = strutil.Difference(newServiceAccountNames, set.ServiceAccountNames, true) + for _, newServiceAccountName := range beingAdded { + if _, err := b.checkOutHandler.LoadCheckOut(ctx, req.Storage, newServiceAccountName); err != nil { + if err == errNotFound { + // Great, this validates that it's not in use in another set. + continue + } + return nil, err + } + return logical.ErrorResponse(fmt.Sprintf("%q is already managed by another set", newServiceAccountName)), nil + } + + // For service accounts we won't be handling anymore, before we delete them, ensure they're not checked out. + beingDeleted = strutil.Difference(set.ServiceAccountNames, newServiceAccountNames, true) + for _, prevServiceAccountName := range beingDeleted { + checkOut, err := b.checkOutHandler.LoadCheckOut(ctx, req.Storage, prevServiceAccountName) + if err != nil { + if err == errNotFound { + // Nothing else to do here. + continue + } + return nil, err + } + if !checkOut.IsAvailable { + return logical.ErrorResponse(fmt.Sprintf(`"%s" can't be deleted because it is currently checked out'`, prevServiceAccountName)), nil + } + } + set.ServiceAccountNames = newServiceAccountNames + } + + if ttlSent { + set.TTL = ttl + } + if maxTTLSent { + set.MaxTTL = maxTTL + } + if enforcementSent { + set.DisableCheckInEnforcement = disableCheckInEnforcement + } + if err := set.Validate(); err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + // Now that we know we can take all these actions, let's take them. + for _, newServiceAccountName := range beingAdded { + if err := b.checkOutHandler.CheckIn(ctx, req.Storage, newServiceAccountName); err != nil { + return nil, err + } + } + for _, prevServiceAccountName := range beingDeleted { + if err := b.checkOutHandler.Delete(ctx, req.Storage, prevServiceAccountName); err != nil { + return nil, err + } + } + if err := storeSet(ctx, req.Storage, setName, set); err != nil { + return nil, err + } + return nil, nil +} + +func (b *backend) operationSetRead(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := fieldData.Get("name").(string) + + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.RLock() + defer lock.RUnlock() + + set, err := readSet(ctx, req.Storage, setName) + if err != nil { + return nil, err + } + if set == nil { + return nil, nil + } + return &logical.Response{ + Data: map[string]interface{}{ + "service_account_names": set.ServiceAccountNames, + "ttl": int64(set.TTL.Seconds()), + "max_ttl": int64(set.MaxTTL.Seconds()), + "disable_check_in_enforcement": set.DisableCheckInEnforcement, + }, + }, nil +} + +func (b *backend) operationSetDelete(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := fieldData.Get("name").(string) + + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.Lock() + defer lock.Unlock() + + set, err := readSet(ctx, req.Storage, setName) + if err != nil { + return nil, err + } + if set == nil { + return nil, nil + } + // We need to remove all the items we'd stored for these service accounts. + for _, serviceAccountName := range set.ServiceAccountNames { + checkOut, err := b.checkOutHandler.LoadCheckOut(ctx, req.Storage, serviceAccountName) + if err != nil { + if err == errNotFound { + // Nothing else to do here. + continue + } + return nil, err + } + if !checkOut.IsAvailable { + return logical.ErrorResponse(fmt.Sprintf(`"%s" can't be deleted because it is currently checked out'`, serviceAccountName)), nil + } + } + for _, serviceAccountName := range set.ServiceAccountNames { + if err := b.checkOutHandler.Delete(ctx, req.Storage, serviceAccountName); err != nil { + return nil, err + } + } + if err := req.Storage.Delete(ctx, libraryPrefix+setName); err != nil { + return nil, err + } + return nil, nil +} + +// readSet is a helper method for reading a set from storage by name. +// It's intended to be used anywhere in the plugin. It may return nil, nil if +// a librarySet doesn't currently exist for a given setName. +func readSet(ctx context.Context, storage logical.Storage, setName string) (*librarySet, error) { + entry, err := storage.Get(ctx, libraryPrefix+setName) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + set := &librarySet{} + if err := entry.DecodeJSON(set); err != nil { + return nil, err + } + return set, nil +} + +// storeSet stores a librarySet. +func storeSet(ctx context.Context, storage logical.Storage, setName string, set *librarySet) error { + entry, err := logical.StorageEntryJSON(libraryPrefix+setName, set) + if err != nil { + return err + } + return storage.Put(ctx, entry) +} + +const ( + setHelpSynopsis = ` +Manage sets to build a library of service accounts that can be checked out. +` + setHelpDescription = ` +This endpoint allows you to read, write, and delete individual sets that are used for checking out service accounts. +Deleting a set can only be performed if all of its service accounts are currently checked in. +` + pathListSetsHelpSyn = ` +List the name of each set currently stored. +` + pathListSetsHelpDesc = ` +To learn which service accounts are being managed by Vault, list the set names using +this endpoint. Then read any individual set by name to learn more. +` +) diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_checkouts.go b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_checkouts.go new file mode 100644 index 0000000000..8d5478f841 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_checkouts.go @@ -0,0 +1,377 @@ +package plugin + +import ( + "context" + "fmt" + "time" + + metrics "github.com/armon/go-metrics" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/locksutil" + "github.com/hashicorp/vault/sdk/logical" +) + +const secretAccessKeyType = "creds" + +func (b *backend) pathSetCheckOut() *framework.Path { + return &framework.Path{ + Pattern: libraryPrefix + framework.GenericNameRegex("name") + "/check-out$", + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeLowerCaseString, + Description: "Name of the set", + Required: true, + }, + "ttl": { + Type: framework.TypeDurationSecond, + Description: "The length of time before the check-out will expire, in seconds.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.operationSetCheckOut, + Summary: "Check a service account out from the library.", + }, + }, + HelpSynopsis: `Check a service account out from the library.`, + } +} + +func (b *backend) operationSetCheckOut(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := fieldData.Get("name").(string) + + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.Lock() + defer lock.Unlock() + + ttlPeriodRaw, ttlPeriodSent := fieldData.GetOk("ttl") + if !ttlPeriodSent { + ttlPeriodRaw = 0 + } + requestedTTL := time.Duration(ttlPeriodRaw.(int)) * time.Second + + set, err := readSet(ctx, req.Storage, setName) + if err != nil { + return nil, err + } + if set == nil { + return logical.ErrorResponse(fmt.Sprintf(`%q doesn't exist`, setName)), nil + } + + // Prepare the check-out we'd like to execute. + ttl := set.TTL + if ttlPeriodSent { + switch { + case set.TTL <= 0 && requestedTTL > 0: + // The set's TTL is infinite and the caller requested a finite TTL. + ttl = requestedTTL + case set.TTL > 0 && requestedTTL < set.TTL: + // The set's TTL isn't infinite and the caller requested a shorter TTL. + ttl = requestedTTL + } + } + newCheckOut := &CheckOut{ + IsAvailable: false, + BorrowerEntityID: req.EntityID, + BorrowerClientToken: req.ClientToken, + } + + // Check out the first service account available. + for _, serviceAccountName := range set.ServiceAccountNames { + if err := b.checkOutHandler.CheckOut(ctx, req.Storage, serviceAccountName, newCheckOut); err != nil { + if err == errCheckedOut { + continue + } + return nil, err + } + password, err := retrievePassword(ctx, req.Storage, serviceAccountName) + if err != nil { + return nil, err + } + respData := map[string]interface{}{ + "service_account_name": serviceAccountName, + "password": password, + } + internalData := map[string]interface{}{ + "service_account_name": serviceAccountName, + "set_name": setName, + } + resp := b.Backend.Secret(secretAccessKeyType).Response(respData, internalData) + resp.Secret.Renewable = true + resp.Secret.TTL = ttl + resp.Secret.MaxTTL = set.MaxTTL + return resp, nil + } + + // If we arrived here, it's because we never had a hit for a service account that was available. + // In case of customer issues, we need to make this easy to see and diagnose. + b.Logger().Debug(fmt.Sprintf(`%q had no check-outs available`, setName)) + metrics.IncrCounter([]string{"active directory", "check-out", "unavailable", setName}, 1) + + return logical.RespondWithStatusCode(&logical.Response{ + Warnings: []string{"No service accounts available for check-out."}, + }, req, 400) +} + +func (b *backend) secretAccessKeys() *framework.Secret { + return &framework.Secret{ + Type: secretAccessKeyType, + Fields: map[string]*framework.FieldSchema{ + "service_account_name": { + Type: framework.TypeString, + Description: "Service account name", + }, + "password": { + Type: framework.TypeString, + Description: "Password", + }, + }, + Renew: b.renewCheckOut, + Revoke: b.endCheckOut, + } +} + +func (b *backend) renewCheckOut(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := req.Secret.InternalData["set_name"].(string) + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.RLock() + defer lock.RUnlock() + + set, err := readSet(ctx, req.Storage, setName) + if err != nil { + return nil, err + } + if set == nil { + return logical.ErrorResponse(fmt.Sprintf(`%q doesn't exist`, setName)), nil + } + + serviceAccountName := req.Secret.InternalData["service_account_name"].(string) + checkOut, err := b.checkOutHandler.LoadCheckOut(ctx, req.Storage, serviceAccountName) + if err != nil { + return nil, err + } + if checkOut.IsAvailable { + // It's possible that this renewal could be attempted after a check-in occurred either by this entity or by + // another user with access to the "manage check-ins" endpoint that forcibly checked it back in. + return logical.ErrorResponse(fmt.Sprintf("%s is already checked in, please call check-out to regain it", serviceAccountName)), nil + } + resp := &logical.Response{Secret: req.Secret} + resp.Secret.TTL = set.TTL + resp.Secret.MaxTTL = set.MaxTTL + return resp, nil +} + +func (b *backend) endCheckOut(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := req.Secret.InternalData["set_name"].(string) + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.Lock() + defer lock.Unlock() + + serviceAccountName := req.Secret.InternalData["service_account_name"].(string) + if err := b.checkOutHandler.CheckIn(ctx, req.Storage, serviceAccountName); err != nil { + return nil, err + } + return nil, nil +} + +func (b *backend) pathSetCheckIn() *framework.Path { + return &framework.Path{ + Pattern: libraryPrefix + framework.GenericNameRegex("name") + "/check-in$", + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeLowerCaseString, + Description: "Name of the set.", + Required: true, + }, + "service_account_names": { + Type: framework.TypeCommaStringSlice, + Description: "The username/logon name for the service accounts to check in.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.operationCheckIn(false), + Summary: "Check service accounts in to the library.", + }, + }, + HelpSynopsis: `Check service accounts in to the library.`, + } +} + +func (b *backend) pathSetManageCheckIn() *framework.Path { + return &framework.Path{ + Pattern: libraryPrefix + "manage/" + framework.GenericNameRegex("name") + "/check-in$", + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeLowerCaseString, + Description: "Name of the set.", + Required: true, + }, + "service_account_names": { + Type: framework.TypeCommaStringSlice, + Description: "The username/logon name for the service accounts to check in.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.operationCheckIn(true), + Summary: "Check service accounts in to the library.", + }, + }, + HelpSynopsis: `Force checking service accounts in to the library.`, + } +} + +func (b *backend) operationCheckIn(overrideCheckInEnforcement bool) framework.OperationFunc { + return func(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := fieldData.Get("name").(string) + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.Lock() + defer lock.Unlock() + + serviceAccountNamesRaw, serviceAccountNamesSent := fieldData.GetOk("service_account_names") + var serviceAccountNames []string + if serviceAccountNamesSent { + serviceAccountNames = serviceAccountNamesRaw.([]string) + } + + set, err := readSet(ctx, req.Storage, setName) + if err != nil { + return nil, err + } + if set == nil { + return logical.ErrorResponse(fmt.Sprintf(`%q doesn't exist`, setName)), nil + } + + // If check-in enforcement is overridden or disabled at the set level, we should consider it disabled. + disableCheckInEnforcement := overrideCheckInEnforcement || set.DisableCheckInEnforcement + + // Track the service accounts we check in so we can include it in our response. + toCheckIn := make([]string, 0) + + // Build and validate a list of service account names that we will be checking in. + if len(serviceAccountNames) == 0 { + // It's okay if the caller doesn't tell us which service accounts they + // want to check in as long as they only have one checked out. + // We'll assume that's the one they want to check in. + for _, setServiceAccount := range set.ServiceAccountNames { + checkOut, err := b.checkOutHandler.LoadCheckOut(ctx, req.Storage, setServiceAccount) + if err != nil { + return nil, err + } + if checkOut.IsAvailable { + continue + } + if !disableCheckInEnforcement && !checkinAuthorized(req, checkOut) { + continue + } + toCheckIn = append(toCheckIn, setServiceAccount) + } + if len(toCheckIn) > 1 { + return logical.ErrorResponse(`when multiple service accounts are checked out, the "service_account_names" to check in must be provided`), nil + } + } else { + for _, serviceAccountName := range serviceAccountNames { + checkOut, err := b.checkOutHandler.LoadCheckOut(ctx, req.Storage, serviceAccountName) + if err != nil { + return nil, err + } + // First guard that they should be able to do anything at all. + if !checkOut.IsAvailable && !disableCheckInEnforcement && !checkinAuthorized(req, checkOut) { + return logical.ErrorResponse("%q can't be checked in because it wasn't checked out by the caller", serviceAccountName), nil + } + if checkOut.IsAvailable { + continue + } + toCheckIn = append(toCheckIn, serviceAccountName) + } + } + for _, serviceAccountName := range toCheckIn { + if err := b.checkOutHandler.CheckIn(ctx, req.Storage, serviceAccountName); err != nil { + return nil, err + } + } + return &logical.Response{ + Data: map[string]interface{}{ + "check_ins": toCheckIn, + }, + }, nil + } +} + +func (b *backend) pathSetStatus() *framework.Path { + return &framework.Path{ + Pattern: libraryPrefix + framework.GenericNameRegex("name") + "/status$", + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeLowerCaseString, + Description: "Name of the set.", + Required: true, + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.operationSetStatus, + Summary: "Check the status of the service accounts in a library set.", + }, + }, + HelpSynopsis: `Check the status of the service accounts in a library.`, + } +} + +func (b *backend) operationSetStatus(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + setName := fieldData.Get("name").(string) + lock := locksutil.LockForKey(b.checkOutLocks, setName) + lock.RLock() + defer lock.RUnlock() + + set, err := readSet(ctx, req.Storage, setName) + if err != nil { + return nil, err + } + if set == nil { + return logical.ErrorResponse(fmt.Sprintf(`%q doesn't exist`, setName)), nil + } + respData := make(map[string]interface{}) + + for _, serviceAccountName := range set.ServiceAccountNames { + checkOut, err := b.checkOutHandler.LoadCheckOut(ctx, req.Storage, serviceAccountName) + if err != nil { + return nil, err + } + + status := map[string]interface{}{ + "available": checkOut.IsAvailable, + } + if checkOut.IsAvailable { + // We only omit all other fields if the checkout is currently available, + // because they're only relevant to accounts that aren't checked out. + respData[serviceAccountName] = status + continue + } + if checkOut.BorrowerClientToken != "" { + status["borrower_client_token"] = checkOut.BorrowerClientToken + } + if checkOut.BorrowerEntityID != "" { + status["borrower_entity_id"] = checkOut.BorrowerEntityID + } + respData[serviceAccountName] = status + } + return &logical.Response{ + Data: respData, + }, nil +} + +func checkinAuthorized(req *logical.Request, checkOut *CheckOut) bool { + if checkOut.BorrowerEntityID != "" && req.EntityID != "" { + if checkOut.BorrowerEntityID == req.EntityID { + return true + } + } + if checkOut.BorrowerClientToken != "" && req.ClientToken != "" { + if checkOut.BorrowerClientToken == req.ClientToken { + return true + } + } + return false +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_config.go b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_config.go index a79a688114..a5345a8900 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_config.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_config.go @@ -24,7 +24,7 @@ const ( defaultTLSVersion = "tls12" ) -func (b *backend) readConfig(ctx context.Context, storage logical.Storage) (*configuration, error) { +func readConfig(ctx context.Context, storage logical.Storage) (*configuration, error) { entry, err := storage.Get(ctx, configStorageKey) if err != nil { return nil, err @@ -145,7 +145,7 @@ func (b *backend) configUpdateOperation(ctx context.Context, req *logical.Reques } func (b *backend) configReadOperation(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { - config, err := b.readConfig(ctx, req.Storage) + config, err := readConfig(ctx, req.Storage) if err != nil { return nil, err } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_creds.go b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_creds.go index bbb2abe870..c2a192b9ac 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_creds.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_creds.go @@ -59,7 +59,7 @@ func (b *backend) pathCreds() *framework.Path { func (b *backend) credReadOperation(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { cred := make(map[string]interface{}) - engineConf, err := b.readConfig(ctx, req.Storage) + engineConf, err := readConfig(ctx, req.Storage) if err != nil { return nil, err } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_roles.go b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_roles.go index 594888fc96..2f92f0bf18 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_roles.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_roles.go @@ -89,7 +89,7 @@ func (b *backend) readRole(ctx context.Context, storage logical.Storage, roleNam } // Always check when ActiveDirectory shows the password as last set on the fly. - engineConf, err := b.readConfig(ctx, storage) + engineConf, err := readConfig(ctx, storage) if err != nil { return nil, err } @@ -125,7 +125,7 @@ func (b *backend) roleUpdateOperation(ctx context.Context, req *logical.Request, // Get everything we need to construct the role. roleName := fieldData.Get("name").(string) - engineConf, err := b.readConfig(ctx, req.Storage) + engineConf, err := readConfig(ctx, req.Storage) if err != nil { return nil, err } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_rotate_root_creds.go b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_rotate_root_creds.go index 2853f62d40..a7b4448c03 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_rotate_root_creds.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-ad/plugin/path_rotate_root_creds.go @@ -25,7 +25,7 @@ func (b *backend) pathRotateCredentials() *framework.Path { } func (b *backend) pathRotateCredentialsUpdate(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { - engineConf, err := b.readConfig(ctx, req.Storage) + engineConf, err := readConfig(ctx, req.Storage) if err != nil { return nil, err } diff --git a/vendor/modules.txt b/vendor/modules.txt index 6711c249a5..aafff94748 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -361,7 +361,7 @@ github.com/hashicorp/vault-plugin-auth-kubernetes github.com/hashicorp/vault-plugin-auth-oci # github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190814210117-e079e01fbb93 github.com/hashicorp/vault-plugin-database-elasticsearch -# github.com/hashicorp/vault-plugin-secrets-ad v0.5.3-0.20190814210122-0f2fd536b250 +# github.com/hashicorp/vault-plugin-secrets-ad v0.6.0 github.com/hashicorp/vault-plugin-secrets-ad/plugin github.com/hashicorp/vault-plugin-secrets-ad/plugin/client github.com/hashicorp/vault-plugin-secrets-ad/plugin/util