update ad secrets plugin for check-out feature (#7617)

This commit is contained in:
Becca Petrin 2019-10-14 11:17:05 -07:00 committed by GitHub
parent dbdf65e5bc
commit e8432f1ebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 980 additions and 10 deletions

2
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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) {

View File

@ -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
}

View File

@ -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.
`
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

2
vendor/modules.txt vendored
View File

@ -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