From 35bd5cd92fa8a640adf2acc577dd21588d415c25 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Tue, 7 Jun 2016 08:37:04 -0400 Subject: [PATCH] Updated comments and made some refactoring --- builtin/credential/appgroup/backend.go | 36 ++-- builtin/credential/appgroup/path_app.go | 93 ++++++----- builtin/credential/appgroup/path_app_test.go | 2 - builtin/credential/appgroup/path_group.go | 87 +++++----- builtin/credential/appgroup/path_login.go | 24 +-- .../credential/appgroup/path_supergroup.go | 43 ++++- builtin/credential/appgroup/secret_id.go | 156 ++++++++++++------ 7 files changed, 261 insertions(+), 180 deletions(-) diff --git a/builtin/credential/appgroup/backend.go b/builtin/credential/appgroup/backend.go index a3399791d4fa9..8e06c92693c1f 100644 --- a/builtin/credential/appgroup/backend.go +++ b/builtin/credential/appgroup/backend.go @@ -12,24 +12,35 @@ import ( type backend struct { *framework.Backend + + // The salt value to be used by the information to be accessed only + // by this backend. salt *salt.Salt // Guard to clean-up the expired SecretID entries tidySecretIDCASGuard uint32 - // Lock to make changes to registered Apps + // Lock to make changes to registered Apps. This is a low-traffic + // operation. So, using a single lock would suffice. appLock *sync.RWMutex - // Lock to make changes to registered Groups + // Lock to make changes to registered Groups. This is a low-traffic + // operation. So, using a single lock would suffice. groupLock *sync.RWMutex - // Lock to make changes to "supergroup" mode storage entries + // Lock to make changes to storage entries belonging to "supergroup" superGroupLock *sync.RWMutex - // Map of locks to make changes to the SecretIDs generated + // Map of locks to make changes to the storage entries of SelectorIDs + // generated. This will be initiated to a predefined number of locks + // when the backend is created, and will be indexed based on the salted + // selector IDs. selectorIDLocksMap map[string]*sync.RWMutex - // Map of locks to make changes to the SecretIDs generated + // Map of locks to make changes to the storage entries of SecretIDs + // generated. This will be initiated to a predefined number of locks + // when the backend is created, and will be indexed based on the hashed + // secret IDs. secretIDLocksMap map[string]*sync.RWMutex } @@ -71,16 +82,19 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { secretIDLocksMap: map[string]*sync.RWMutex{}, } + // Create a predefined number (256) of locks. This will avoid a superfluous number + // of locks directly proportional to the number of selectorID/secretIDs. These locks + // can be accessed by indexing based on the first 2 characters of the selectorID or + // the secretID respectively. Since these are randomly generated, uniformity of access + // is guaranteed. for i := int64(0); i < 256; i++ { b.selectorIDLocksMap[fmt.Sprintf("%2x", strconv.FormatInt(i, 16))] = &sync.RWMutex{} b.secretIDLocksMap[fmt.Sprintf("%2x", strconv.FormatInt(i, 16))] = &sync.RWMutex{} } - b.secretIDLocksMap["custom"] = &sync.RWMutex{} - // Ideally, "custom" entry is not required for selectorIDLocksMap since - // selectorID is always generated internally and is a UUID. But having - // one is safe. The getter method for lock will never be nil if it can - // always fallback on the "custom" lock. + // Have an extra lock, in case the indexing does not result in a lock, this can be used. + // These locks can be used for listing purposes as well. + b.secretIDLocksMap["custom"] = &sync.RWMutex{} b.selectorIDLocksMap["custom"] = &sync.RWMutex{} // Attach the paths and secrets that are to be handled by the backend @@ -109,7 +123,7 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { // periodicFunc of the backend will be invoked once a minute by the RollbackManager. // AppGroup backend utilizes this function to delete expired SecretID entries. -// This could mean that the SecretID may live in the backend upto 1min after its +// This could mean that the SecretID may live in the backend upto 1 min after its // expiration. The deletion of SecretIDs are not security sensitive and it is okay // to delay the removal of SecretIDs by a minute. func (b *backend) periodicFunc(req *logical.Request) error { diff --git a/builtin/credential/appgroup/path_app.go b/builtin/credential/appgroup/path_app.go index 6f0fffad638aa..68bd385ee1472 100644 --- a/builtin/credential/appgroup/path_app.go +++ b/builtin/credential/appgroup/path_app.go @@ -14,7 +14,8 @@ import ( // appStorageEntry stores all the options that are set on an App type appStorageEntry struct { - // UUID that uniquely represents this App + // UUID that uniquely represents this App. This serves as a credential to perform + // login using this App. SelectorID string `json:"selector_id" structs:"selector_id" mapstructure:"selector_id"` // UUID that serves as the HMAC key for the hashing the 'secret_id's of the App @@ -35,24 +36,25 @@ type appStorageEntry struct { // Duration after which an issued token should not be allowed to be renewed TokenMaxTTL time.Duration `json:"token_max_ttl" structs:"token_max_ttl" mapstructure:"token_max_ttl"` - // A constraint to require 'secret_id' credential during login + // A constraint, if set, requires 'secret_id' credential to be presented during login BindSecretID bool `json:"bind_secret_id" structs:"bind_secret_id" mapstructure:"bind_secret_id"` } // appPaths creates all the paths that are used to register and manage an App. // // Paths returned: -// app/ -// app/ -// app//policies -// app//num-uses -// app//secret-id-ttl -// app//token-ttl -// app//token-max-ttl -// app//bind-secret-id -// app//selector-id -// app//secret-id -// app//custom-secret-id +// app/ - For listing all the registered Apps +// app/ - For registering an App +// app//policies - For updating the param +// app//secret-id-num-uses - For updating the param +// app//secret-id-ttl - For updating the param +// app//token-ttl - For updating the param +// app//token-max-ttl - For updating the param +// app//bind-secret-id - For updating the param +// app//selector-id - For fetching the selector_id of an App +// app//secret-id - For issuing a secret_id against an App, also to list the hashed secret_ids +// app//secret-id/ - For reading the properties of, or deleting a secret_id +// app//custom-secret-id - For assigning a custom secret ID against an App func appPaths(b *backend) []*framework.Path { return []*framework.Path{ &framework.Path{ @@ -73,7 +75,7 @@ func appPaths(b *backend) []*framework.Path { "bind_secret_id": &framework.FieldSchema{ Type: framework.TypeBool, Default: true, - Description: "Impose secret_id to be presented during login using this App. Defaults to 'true'.", + Description: "Impose secret_id to be presented when logging in using this App. Defaults to 'true'.", }, "policies": &framework.FieldSchema{ Type: framework.TypeString, @@ -136,7 +138,7 @@ func appPaths(b *backend) []*framework.Path { }, "bind_secret_id": &framework.FieldSchema{ Type: framework.TypeBool, - Description: "Impose secret_id to be presented during login using this App.", + Description: "Impose secret_id to be presented when logging in using this App.", }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -297,7 +299,7 @@ func appPaths(b *backend) []*framework.Path { } } -// pathAppExistenceCheck returns if the app with the given name exists or not. +// pathAppExistenceCheck returns whether the app with the given name exists or not. func (b *backend) pathAppExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { app, err := b.appEntry(req.Storage, data.Get("app_name").(string)) if err != nil { @@ -307,8 +309,7 @@ func (b *backend) pathAppExistenceCheck(req *logical.Request, data *framework.Fi } // pathAppList is used to list all the Apps registered with the backend. -func (b *backend) pathAppList( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathAppList(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.appLock.RLock() defer b.appLock.RUnlock() @@ -319,7 +320,9 @@ func (b *backend) pathAppList( return logical.ListResponse(apps), nil } -// pathAppSecretIDList is used to list all the Apps registered with the backend. +// pathAppSecretIDList is used to list all the 'secret_id's issued against the App. +// The 'secret_id's will not be returned in plaintext. Instead the HMAC-ed 'secret_id's +// will be returned. func (b *backend) pathAppSecretIDList(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { appName := data.Get("app_name").(string) if appName == "" { @@ -363,6 +366,7 @@ func (b *backend) setAppEntry(s logical.Storage, appName string, app *appStorage return err } + // Create a selector ID reverse mapping entry for the App return b.setSelectorIDEntry(s, app.SelectorID, &selectorIDStorageEntry{ Type: selectorTypeApp, Name: appName, @@ -404,6 +408,7 @@ func (b *backend) pathAppCreateUpdate(req *logical.Request, data *framework.Fiel if err != nil { return nil, err } + // Create a new entry object if this is a CreateOperation if app == nil { selectorID, err := uuid.GenerateUUID() @@ -459,8 +464,6 @@ func (b *backend) pathAppCreateUpdate(req *logical.Request, data *framework.Fiel app.TokenMaxTTL = time.Second * time.Duration(data.Get("token_max_ttl").(int)) } - resp := &logical.Response{} - // Check that the TokenMaxTTL value provided is less than the TokenMaxTTL. // Sanitizing the TTL and MaxTTL is not required now and can be performed // at credential issue time. @@ -468,7 +471,9 @@ func (b *backend) pathAppCreateUpdate(req *logical.Request, data *framework.Fiel return logical.ErrorResponse("token_ttl should not be greater than token_max_ttl"), nil } + var resp *logical.Response if app.TokenMaxTTL > b.System().MaxLeaseTTL() { + resp = &logical.Response{} resp.AddWarning("token_max_ttl is greater than the backend mount's maximum TTL value; issued tokens' max TTL value will be truncated") } @@ -520,7 +525,7 @@ func (b *backend) pathAppDelete(req *logical.Request, data *framework.FieldData) b.appLock.Lock() defer b.appLock.Unlock() - // When the app is getting deleted, remove all the secrets issued as part of the app. + // Just before the app is deleted, remove all the secrets issued as part of the app. if err = b.flushSelectorSecrets(req.Storage, app.SelectorID); err != nil { return nil, fmt.Errorf("failed to invalidate the secrets belonging to app %s", appName) } @@ -532,28 +537,6 @@ func (b *backend) pathAppDelete(req *logical.Request, data *framework.FieldData) return nil, nil } -func (b *backend) pathAppBindSecretIDUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - appName := data.Get("app_name").(string) - if appName == "" { - return logical.ErrorResponse("missing app_name"), nil - } - - app, err := b.appEntry(req.Storage, strings.ToLower(appName)) - if err != nil { - return nil, err - } - if app == nil { - return nil, nil - } - - if bindSecretIDRaw, ok := data.GetOk("bind_secret_id"); ok { - app.BindSecretID = bindSecretIDRaw.(bool) - return nil, b.setAppEntry(req.Storage, appName, app) - } else { - return logical.ErrorResponse("missing bind_secret_id"), nil - } -} - func (b *backend) pathAppSecretIDHMACRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { appName := data.Get("app_name").(string) if appName == "" { @@ -621,6 +604,28 @@ func (b *backend) pathAppSecretIDHMACDelete(req *logical.Request, data *framewor return nil, req.Storage.Delete(entryIndex) } +func (b *backend) pathAppBindSecretIDUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + appName := data.Get("app_name").(string) + if appName == "" { + return logical.ErrorResponse("missing app_name"), nil + } + + app, err := b.appEntry(req.Storage, strings.ToLower(appName)) + if err != nil { + return nil, err + } + if app == nil { + return nil, nil + } + + if bindSecretIDRaw, ok := data.GetOk("bind_secret_id"); ok { + app.BindSecretID = bindSecretIDRaw.(bool) + return nil, b.setAppEntry(req.Storage, appName, app) + } else { + return logical.ErrorResponse("missing bind_secret_id"), nil + } +} + func (b *backend) pathAppBindSecretIDRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { appName := data.Get("app_name").(string) if appName == "" { diff --git a/builtin/credential/appgroup/path_app_test.go b/builtin/credential/appgroup/path_app_test.go index bf4c969110c46..97f8124438564 100644 --- a/builtin/credential/appgroup/path_app_test.go +++ b/builtin/credential/appgroup/path_app_test.go @@ -1,7 +1,6 @@ package appgroup import ( - "log" "reflect" "testing" "time" @@ -91,7 +90,6 @@ func TestBackend_app_secret_id_read_delete(t *testing.T) { t.Fatalf("err:%v resp:%#v", err, resp) } hmacSecretID := resp.Data["keys"].([]string)[0] - log.Printf("hmacSecretID: %s\n", hmacSecretID) hmacReq := &logical.Request{ Operation: logical.ReadOperation, diff --git a/builtin/credential/appgroup/path_group.go b/builtin/credential/appgroup/path_group.go index 8b129b21a9f54..686d672e2065d 100644 --- a/builtin/credential/appgroup/path_group.go +++ b/builtin/credential/appgroup/path_group.go @@ -48,17 +48,18 @@ type groupStorageEntry struct { // groupPaths creates all the paths that are used to register and manage an Group. // // Paths returned: -// group/ -// group/ -// group//policies -// group//bind-secret-id -// group//num-uses -// group//secret_id-ttl -// group//token-ttl -// group//token-max-ttl -// group//selector-id -// group//secret-id -// group//custom-secret-id +// group/ - For listing all the registered Groups +// group/ - For registering a Group +// group//additional-policies - For updating the param +// group//bind-secret-id - For updating the param +// group//secret-id-num-uses - For updating the param +// group//secret-id-ttl - For updating the param +// group//token-ttl - For updating the param +// group//token-max-ttl - For updating the param +// group//selector-id - For fetching the selector_id of a Group. +// group//secret-id - For issuing secret_id against a Group and to list the hashed secret_ids +// group//secret-id/ - For reading the properties of, or deleting a secret_id +// group//custom-secret-id - For assigning a custom secret ID against a Group func groupPaths(b *backend) []*framework.Path { return []*framework.Path{ &framework.Path{ @@ -84,7 +85,7 @@ func groupPaths(b *backend) []*framework.Path { "bind_secret_id": &framework.FieldSchema{ Type: framework.TypeBool, Default: true, - Description: "Impose secret_id to be presented during login using this Group. Defaults to 'true'.", + Description: "Impose secret_id to be presented when logging in using this Group. Defaults to 'true'.", }, "additional_policies": &framework.FieldSchema{ Type: framework.TypeString, @@ -346,8 +347,7 @@ func (b *backend) pathGroupExistenceCheck(req *logical.Request, data *framework. } // pathGroupList is used to list all the Groups registered with the backend. -func (b *backend) pathGroupList( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathGroupList(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.groupLock.RLock() defer b.groupLock.RUnlock() @@ -359,9 +359,7 @@ func (b *backend) pathGroupList( } // pathGroupSecretIDList is used to list all the 'secret_id's issued on the group. -func (b *backend) pathGroupSecretIDList( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - +func (b *backend) pathGroupSecretIDList(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { groupName := data.Get("group_name").(string) if groupName == "" { return logical.ErrorResponse("missing group_name"), nil @@ -403,6 +401,7 @@ func (b *backend) setGroupEntry(s logical.Storage, groupName string, group *grou return err } + // Create a selector ID reverse mapping entry for the Group return b.setSelectorIDEntry(s, group.SelectorID, &selectorIDStorageEntry{ Type: selectorTypeGroup, Name: groupName, @@ -508,8 +507,14 @@ func (b *backend) pathGroupCreateUpdate(req *logical.Request, data *framework.Fi return logical.ErrorResponse("token_ttl should not be greater than token_max_ttl"), nil } + var resp *logical.Response + if group.TokenMaxTTL > b.System().MaxLeaseTTL() { + resp = &logical.Response{} + resp.AddWarning("token_max_ttl is greater than the backend mount's maximum TTL value; issued tokens' max TTL value will be truncated") + } + // Store the entry. - return nil, b.setGroupEntry(req.Storage, groupName, group) + return resp, b.setGroupEntry(req.Storage, groupName, group) } // pathGroupRead grabs a read lock and reads the options set on the Group from the storage @@ -556,7 +561,7 @@ func (b *backend) pathGroupDelete(req *logical.Request, data *framework.FieldDat b.groupLock.Lock() defer b.groupLock.Unlock() - // When the group is getting deleted, remove all the secrets issued as part of the group. + // Just before the group is deleted, remove all the secrets issued as part of the group. if err = b.flushSelectorSecrets(req.Storage, group.SelectorID); err != nil { return nil, fmt.Errorf("failed to invalidate the secrets belonging to group %s", groupName) } @@ -628,28 +633,6 @@ func (b *backend) pathGroupAppsDelete(req *logical.Request, data *framework.Fiel return nil, b.setGroupEntry(req.Storage, groupName, group) } -func (b *backend) pathGroupBindSecretIDUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - groupName := data.Get("group_name").(string) - if groupName == "" { - return logical.ErrorResponse("missing group_name"), nil - } - - group, err := b.groupEntry(req.Storage, strings.ToLower(groupName)) - if err != nil { - return nil, err - } - if group == nil { - return nil, nil - } - - if bindSecretIDRaw, ok := data.GetOk("bind_secret_id"); ok { - group.BindSecretID = bindSecretIDRaw.(bool) - return nil, b.setGroupEntry(req.Storage, groupName, group) - } else { - return logical.ErrorResponse("missing bind_secret_id"), nil - } -} - func (b *backend) pathGroupSecretIDHMACRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { groupName := data.Get("group_name").(string) if groupName == "" { @@ -717,6 +700,28 @@ func (b *backend) pathGroupSecretIDHMACDelete(req *logical.Request, data *framew return nil, req.Storage.Delete(entryIndex) } +func (b *backend) pathGroupBindSecretIDUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + groupName := data.Get("group_name").(string) + if groupName == "" { + return logical.ErrorResponse("missing group_name"), nil + } + + group, err := b.groupEntry(req.Storage, strings.ToLower(groupName)) + if err != nil { + return nil, err + } + if group == nil { + return nil, nil + } + + if bindSecretIDRaw, ok := data.GetOk("bind_secret_id"); ok { + group.BindSecretID = bindSecretIDRaw.(bool) + return nil, b.setGroupEntry(req.Storage, groupName, group) + } else { + return logical.ErrorResponse("missing bind_secret_id"), nil + } +} + func (b *backend) pathGroupBindSecretIDRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { groupName := data.Get("group_name").(string) if groupName == "" { diff --git a/builtin/credential/appgroup/path_login.go b/builtin/credential/appgroup/path_login.go index 9ef2ba6c8dc65..8f91db8bd9f68 100644 --- a/builtin/credential/appgroup/path_login.go +++ b/builtin/credential/appgroup/path_login.go @@ -2,7 +2,6 @@ package appgroup import ( "fmt" - "strings" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -31,27 +30,8 @@ func pathLogin(b *backend) *framework.Path { } func (b *backend) pathLoginUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - secretID := strings.TrimSpace(data.Get("secret_id").(string)) - if secretID == "" { - return logical.ErrorResponse("missing secret_id"), nil - } - - // Selector can optionally be prepended to the SecretID with a `;` delimiter - selectorID := strings.TrimSpace(data.Get("selector_id").(string)) - if selectorID == "" { - selectorFields := strings.SplitN(secretID, ";", 2) - if len(selectorFields) != 2 || selectorFields[0] == "" { - return logical.ErrorResponse("missing selector_id"), nil - } else if selectorFields[1] == "" { - return logical.ErrorResponse("missing secret_id"), nil - } else { - selectorID = selectorFields[0] - secretID = selectorFields[1] - } - } - - validateResp, err := b.validateCredentials(req.Storage, selectorID, secretID) - if err != nil { + validateResp, err := b.validateCredentials(req.Storage, data) + if err != nil || validateResp == nil { return logical.ErrorResponse(fmt.Sprintf("failed to validate secret ID: %s", err)), nil } diff --git a/builtin/credential/appgroup/path_supergroup.go b/builtin/credential/appgroup/path_supergroup.go index 8023298ef1ec3..51373d41e955f 100644 --- a/builtin/credential/appgroup/path_supergroup.go +++ b/builtin/credential/appgroup/path_supergroup.go @@ -158,6 +158,9 @@ addition to those, a set of policies can be assigned using this. } } +// setSuperGroupEntry creates a storage entry for the options set on a supergroup. +// During login, this storage entry is referred to determine the capabilities to +// be allowed as part of authentication and authorization. func (b *backend) setSuperGroupEntry(s logical.Storage, superGroupName string, superGroup *superGroupStorageEntry) error { b.superGroupLock.Lock() defer b.superGroupLock.Unlock() @@ -173,12 +176,14 @@ func (b *backend) setSuperGroupEntry(s logical.Storage, superGroupName string, s return err } + // Create a selector ID reverse mapping entry for the supergroup return b.setSelectorIDEntry(s, superGroup.SelectorID, &selectorIDStorageEntry{ Type: selectorTypeSuperGroup, Name: superGroupName, }) } +// deleteSuperGroupEntry deletes the storage associated with the supergroup. func (b *backend) deleteSuperGroupEntry(s logical.Storage, superGroupName string) error { if superGroupName == "" { return fmt.Errorf("missing superGroupName") @@ -189,6 +194,8 @@ func (b *backend) deleteSuperGroupEntry(s logical.Storage, superGroupName string return s.Delete("supergroup/" + strings.ToLower(superGroupName)) } +// superGroupEntry is used to read the storage entry containing options that are set +// for the supergroup. func (b *backend) superGroupEntry(s logical.Storage, superGroupName string) (*superGroupStorageEntry, error) { if superGroupName == "" { return nil, fmt.Errorf("missing superGroupName") @@ -210,6 +217,7 @@ func (b *backend) superGroupEntry(s logical.Storage, superGroupName string) (*su return &result, nil } +// Path to issue a 'secret_id' on the supergroup func (b *backend) pathSuperGroupSecretIDUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { secretID, err := uuid.GenerateUUID() if err != nil { @@ -218,18 +226,25 @@ func (b *backend) pathSuperGroupSecretIDUpdate(req *logical.Request, data *frame return b.handleSuperGroupSecretIDCommon(req, data, secretID) } +// Path to assign a custom 'secret_id' on the supergroup func (b *backend) pathSuperGroupCustomSecretIDUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { return b.handleSuperGroupSecretIDCommon(req, data, data.Get("secret_id").(string)) } +// A common procedure to register a 'secret_id' against a supergroup. Currently this +// only supports 'bind_secret_id'. func (b *backend) handleSuperGroupSecretIDCommon(req *logical.Request, data *framework.FieldData, secretID string) (*logical.Response, error) { + if secretID == "" { + return logical.ErrorResponse("missing secret_id"), nil + } + selectorID, err := uuid.GenerateUUID() if err != nil { return nil, fmt.Errorf("failed to create selector_id: %s\n", err) } hmacKey, err := uuid.GenerateUUID() if err != nil { - return nil, fmt.Errorf("failed to create selector_id: %s\n", err) + return nil, fmt.Errorf("failed to create hmac_key: %s\n", err) } superGroup := &superGroupStorageEntry{ SelectorID: selectorID, @@ -256,10 +271,20 @@ func (b *backend) handleSuperGroupSecretIDCommon(req *logical.Request, data *fra return logical.ErrorResponse("token_ttl should not be greater than token_max_ttl"), nil } - if secretID == "" { - return logical.ErrorResponse("missing secret_id"), nil + var resp *logical.Response + if supergroup.TokenMaxTTL > b.System().MaxLeaseTTL() { + resp = &logical.Response{} + resp.AddWarning("token_max_ttl is greater than the backend mount's maximum TTL value; issued tokens' max TTL value will be truncated") } + // Only bind_secret_id is supported now. Check for it. + if !superGroup.BindSecretID { + return logical.ErrorResponse("bind_secret_id is not set on the app"), nil + } + + // Since there is no pre-registration of supergroups, there should be a predictable + // way to refer to the options set on the supergroup, during login time. Setting the + // name of the supergroup to be the salted hash of secret_id itself. superGroupName := b.salt.SaltID(secretID) // Store the entry. @@ -279,12 +304,12 @@ func (b *backend) handleSuperGroupSecretIDCommon(req *logical.Request, data *fra return nil, fmt.Errorf("failed to store secret ID: %s", err) } - return &logical.Response{ - Data: map[string]interface{}{ - "secret_id": secretID, - "selector_id": superGroup.SelectorID, - }, - }, nil + resp.Data = map[string]interface{}{ + "secret_id": secretID, + "selector_id": superGroup.SelectorID, + } + + return resp, nil } const pathSuperGroupCustomSecretIDHelpSys = `Assign a SecretID of choice against any combination of diff --git a/builtin/credential/appgroup/secret_id.go b/builtin/credential/appgroup/secret_id.go index 1fcfbeebee3b9..e1f4bcc74011a 100644 --- a/builtin/credential/appgroup/secret_id.go +++ b/builtin/credential/appgroup/secret_id.go @@ -5,12 +5,14 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "strings" "sync" "time" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" ) const ( @@ -45,15 +47,16 @@ type secretIDStorageEntry struct { // required to be stored as metadata in the response, and/or the information required to // create the client token. type validationResponse struct { - SelectorID string `json:"selector_id" structs:"selector_id" mapstructure:"selector_id"` - HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"` - TokenTTL time.Duration `json:"token_ttl" structs:"token_ttl" mapstructure:"token_ttl"` - TokenMaxTTL time.Duration `json:"token_max_ttl" structs:"token_max_ttl" mapstructure:"token_max_ttl"` - Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` + SelectorID string `json:"selector_id" structs:"selector_id" mapstructure:"selector_id"` + HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"` + TokenTTL time.Duration `json:"token_ttl" structs:"token_ttl" mapstructure:"token_ttl"` + TokenMaxTTL time.Duration `json:"token_max_ttl" structs:"token_max_ttl" mapstructure:"token_max_ttl"` + Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` + BindSecretID bool `json:"bind_secret_id" structs:"bind_secret_id" mapstructure:"bind_secret_id"` } -// selectorStorageEntry represents the reverse mapping of the selector to -// the respective app or group that the selectorID belongs to. +// selectorStorageEntry represents the reverse mapping from the selector +// to the respective app or group that the selectorID belongs to. type selectorIDStorageEntry struct { // Type of selector: "app", "group", "supergroup" Type string `json:"type" structs:"type" mapstructure:"type"` @@ -62,19 +65,8 @@ type selectorIDStorageEntry struct { Name string `json:"name" structs:"name" mapstructure:"name"` } -func (b *backend) selectorIDLock(selectorID string) *sync.RWMutex { - var lock *sync.RWMutex - var ok bool - if len(selectorID) >= 2 { - lock, ok = b.selectorIDLocksMap[selectorID[0:2]] - } - if !ok || lock == nil { - // Fall back for custom secret IDs - lock = b.selectorIDLocksMap["custom"] - } - return lock -} - +// setSelectorIDEntry creates a storage entry that maps the selector ID +// to an App or a Group. func (b *backend) setSelectorIDEntry(s logical.Storage, selectorID string, selectorEntry *selectorIDStorageEntry) error { lock := b.selectorIDLock(selectorID) lock.Lock() @@ -90,6 +82,8 @@ func (b *backend) setSelectorIDEntry(s logical.Storage, selectorID string, selec return nil } +// selectorIDEntry is used to read the storage entry that maps the selector +// ID to an App or a Group. func (b *backend) selectorIDEntry(s logical.Storage, selectorID string) (*selectorIDStorageEntry, error) { if selectorID == "" { return nil, fmt.Errorf("missing selectorID") @@ -112,36 +106,61 @@ func (b *backend) selectorIDEntry(s logical.Storage, selectorID string) (*select return &result, nil } -// Identifies the supplied selector and validates it, checks if the supplied secret ID -// has a corresponding entry in the backend and udpates the use count if needed. -func (b *backend) validateCredentials(s logical.Storage, selectorID, secretID string) (*validationResponse, error) { +// Identifies the supplied selector ID and validates it, checks if the supplied +// secret ID has a corresponding entry in the backend and udpates the use count +// if needed. +func (b *backend) validateCredentials(s logical.Storage, data *framework.FieldData) (*validationResponse, error) { + // SelectorID must be supplied during every login + selectorID := strings.TrimSpace(data.Get("selector_id").(string)) if selectorID == "" { return nil, fmt.Errorf("missing selector_id") } - if secretID == "" { - return nil, fmt.Errorf("missing secret_id") - } + // First validate the selector ID. Based on which type the selector + // belongs to, the validationResp will contain all the information + // required to continue the credential verification. Based on which + // binds are set, continue the verification process. validationResp, err := b.validateSelectorID(s, selectorID) if err != nil { return nil, err } - - // Check if the secret ID supplied is valid. If use limit was specified - // on the secret ID, it will be decremented in this call. - valid, err := b.secretIDEntryValid(s, selectorID, secretID, validationResp.HMACKey) - if err != nil { - return nil, err + if validationResp == nil { + return nil, fmt.Errorf("failed to validate selector_id") } - if !valid { - return nil, fmt.Errorf("invalid secret_id: %s\n", secretID) + + // If 'bind_secret_id' was set on app/group/supergroup, look for the + // field 'secret_id' to be specified and validate it. + if validationResp.BindSecretID { + secretID := strings.TrimSpace(data.Get("secret_id").(string)) + if secretID == "" { + return nil, fmt.Errorf("missing secret_id") + } + + // Check if the secret ID supplied is valid. If use limit was specified + // on the secret ID, it will be decremented in this call. + valid, err := b.validateBindSecretID(s, selectorID, secretID, validationResp.HMACKey) + if err != nil { + return nil, err + } + if !valid { + return nil, fmt.Errorf("invalid secret_id: %s\n", secretID) + } + } else { + return nil, fmt.Errorf("failed to find the binding creteria; there should be at least one") } + // As and when more binds are supported, add additional verification process + return validationResp, nil } -// Check if there exists an entry in the name of selectorValue for the selectorType supplied. +// validateSelectorID checks if there is a storage entry that maps the selector ID +// to app/group/supergroup. If it finds a mapping, then a validation response is +// returned containing a generic set of information that is used to perform further +// verification of credentials supplied and the information required to authenticate +// the request. func (b *backend) validateSelectorID(s logical.Storage, selectorID string) (*validationResponse, error) { + // Look for the storage entry that maps the selectorID to app/group/supergroup. selector, err := b.selectorIDEntry(s, selectorID) if err != nil { return nil, err @@ -151,8 +170,10 @@ func (b *backend) validateSelectorID(s logical.Storage, selectorID string) (*val } resp := &validationResponse{} + switch selector.Type { case selectorTypeApp: + // Get the properties of App and populate the response app, err := b.appEntry(s, selector.Name) if err != nil { return nil, err @@ -165,7 +186,9 @@ func (b *backend) validateSelectorID(s logical.Storage, selectorID string) (*val resp.TokenMaxTTL = app.TokenMaxTTL resp.SelectorID = app.SelectorID resp.HMACKey = app.HMACKey + resp.BindSecretID = app.BindSecretID case selectorTypeGroup: + // Get the properties of Group and populate the response group, err := b.groupEntry(s, selector.Name) if err != nil { return nil, err @@ -187,7 +210,9 @@ func (b *backend) validateSelectorID(s logical.Storage, selectorID string) (*val resp.TokenMaxTTL = group.TokenMaxTTL resp.SelectorID = group.SelectorID resp.HMACKey = group.HMACKey + resp.BindSecretID = group.BindSecretID case selectorTypeSuperGroup: + // Get the properties of supergroup and populate the response superGroup, err := b.superGroupEntry(s, selector.Name) if err != nil { return nil, err @@ -195,6 +220,7 @@ func (b *backend) validateSelectorID(s logical.Storage, selectorID string) (*val if superGroup == nil { return nil, fmt.Errorf("supergroup credential referred by the secret ID does not exist") } + // Get the properties of each group within supergroup. for _, groupName := range superGroup.Groups { group, err := b.groupEntry(s, groupName) if err != nil { @@ -210,6 +236,7 @@ func (b *backend) validateSelectorID(s logical.Storage, selectorID string) (*val resp.Policies = append(resp.Policies, group.AdditionalPolicies...) } + // Get the properties of each app within supergroup. for _, appName := range superGroup.Apps { app, err := b.appEntry(s, appName) if err != nil { @@ -226,6 +253,7 @@ func (b *backend) validateSelectorID(s logical.Storage, selectorID string) (*val resp.TokenMaxTTL = superGroup.TokenMaxTTL resp.SelectorID = superGroup.SelectorID resp.HMACKey = superGroup.HMACKey + resp.BindSecretID = superGroup.BindSecretID default: return nil, fmt.Errorf("unknown selector type") } @@ -236,25 +264,30 @@ func (b *backend) validateSelectorID(s logical.Storage, selectorID string) (*val return nil, err } + // The policies in response will have duplicates. Clean it up, and add 'default' + // policy if not added already. resp.Policies = policyutil.SanitizePolicies(resp.Policies, true) return resp, nil } -// secretIDEntryValid is used to determine if the given secret ID is a valid one. +// validateBindSecretID is used to determine if the given secret ID is a valid one. // The SecretID is looked to be present only under the sub-view of the selector. // This ensures that the SecretIDs that are reused between selector types, the // correct one is referred to. If the SecretIDs are always generated by the // backend, then there will be no collision between the SecretIDs from different -// types. But, if same specific SecretIDs are assigned across different selector -// types, then it should be supported. -func (b *backend) secretIDEntryValid(s logical.Storage, selectorID, secretID, hmacKey string) (bool, error) { +// types. But, if 'custom_secret_id's are assigned across different selector types, +// then it should be supported. +func (b *backend) validateBindSecretID(s logical.Storage, selectorID, secretID, hmacKey string) (bool, error) { hashedSecretID, err := createHMAC(hmacKey, secretID) if err != nil { return false, fmt.Errorf("failed to create HMAC of secret_id: %s", err) } entryIndex := fmt.Sprintf("secret_id/%s/%s", b.salt.SaltID(selectorID), hashedSecretID) + // SecretID locks are always index based on hashedSecretIDs. This helps + // acquiring the locks when the secret IDs are listed. This allows grabbing + // the correct locks even if the secret IDs are not known in plaintext. lock := b.secretIDLock(hashedSecretID) lock.RLock() @@ -297,13 +330,13 @@ func (b *backend) secretIDEntryValid(s logical.Storage, selectorID, secretID, hm } // If there exists a single use left, delete the SecretID entry from - // the storage but do not fail the validation request. Delete the - // SecretIDs lock from the map of locks. Subsequest requests to use - // the same SecretID will fail. + // the storage but do not fail the validation request. Subsequest + // requests to use the same SecretID will fail. if result.SecretIDNumUses == 1 { if err := s.Delete(entryIndex); err != nil { return false, err } + // The storage entry for superGroup type is not created by any endpoints // and it is not cleaned up in any other way. When the SecretID belonging // to the superGroup storage entry is getting invalidated, the entry should @@ -315,7 +348,6 @@ func (b *backend) secretIDEntryValid(s logical.Storage, selectorID, secretID, hm if selector == nil { return false, fmt.Errorf("failed to find selector for selector_id:%s\n", selectorID) } - if selector.Type == selectorTypeSuperGroup { if err := b.deleteSuperGroupEntry(s, selector.Name); err != nil { return false, err @@ -335,6 +367,27 @@ func (b *backend) secretIDEntryValid(s logical.Storage, selectorID, secretID, hm return true, nil } +// selectorIDLock is used to get a lock from the pre-initialized map +// of locks. Map is indexed based on the first 2 characters of the +// selectorID, which is a random UUID. If the input is not hex encoded +// or if it is empty a "custom" lock will be returned. +func (b *backend) selectorIDLock(selectorID string) *sync.RWMutex { + var lock *sync.RWMutex + var ok bool + if len(selectorID) >= 2 { + lock, ok = b.selectorIDLocksMap[selectorID[0:2]] + } + if !ok || lock == nil { + // Fall back for custom secret IDs + lock = b.selectorIDLocksMap["custom"] + } + return lock +} + +// secretIDLock is used to get a lock from the pre-initialized map +// of locks. Map is indexed based on the first 2 characters of the +// hashed secretID. If the input is not hex encoded or if empty, a +// "custom" lock will be returned. func (b *backend) secretIDLock(hashedSecretID string) *sync.RWMutex { var lock *sync.RWMutex var ok bool @@ -348,7 +401,8 @@ func (b *backend) secretIDLock(hashedSecretID string) *sync.RWMutex { return lock } -// Creates HMAC using a per-role key. +// Creates a SHA256 HMAC of the given 'value' using the given 'key' +// and returns a hex encoded string. func createHMAC(key, value string) (string, error) { if key == "" { return "", fmt.Errorf("invalid HMAC key") @@ -359,10 +413,6 @@ func createHMAC(key, value string) (string, error) { } // registerSecretIDEntry creates a new storage entry for the given SecretID. -// Successful creation of the storage entry results in the creation of a -// lock in the map of locks maintained at the backend. The index into the -// map is the SecretID itself. During login, if the SecretID supplied is not -// having a corresponding lock in the map, the login attempt fails. func (b *backend) registerSecretIDEntry(s logical.Storage, selectorID, secretID, hmacKey string, secretEntry *secretIDStorageEntry) error { hashedSecretID, err := createHMAC(hmacKey, secretID) if err != nil { @@ -426,9 +476,9 @@ func (b *backend) registerSecretIDEntry(s logical.Storage, selectorID, secretID, } // Iterates through all the Apps, fetches the polices from each App -// and returns a union of all the policies combined together. -// An error is thrown if any App in the list of Apps supplied -// is non-existent at the backend. +// and returns a union of all the policies combined together. An error +// is thrown if any App in the list of Apps supplied is non-existent +// with the backend. func (b *backend) fetchPolicies(s logical.Storage, apps []string) ([]string, error) { var policies []string for _, appName := range apps { @@ -445,7 +495,10 @@ func (b *backend) fetchPolicies(s logical.Storage, apps []string) ([]string, err return strutil.RemoveDuplicates(policies), nil } +// flushSelectorSecrets deletes all the 'secret_id's that belong to the given +// selector ID. func (b *backend) flushSelectorSecrets(s logical.Storage, selectorID string) error { + // Acquire the custom lock to perform listing of 'secret_id's lock := b.secretIDLock("") lock.RLock() hashedSecretIDs, err := s.List(fmt.Sprintf("secret_id/%s/", b.salt.SaltID(selectorID))) @@ -454,6 +507,7 @@ func (b *backend) flushSelectorSecrets(s logical.Storage, selectorID string) err } lock.RUnlock() for _, hashedSecretID := range hashedSecretIDs { + // Acquire the lock belonging to the secret ID lock = b.secretIDLock(hashedSecretID) lock.Lock() entryIndex := fmt.Sprintf("secret_id/%s/%s", b.salt.SaltID(selectorID), hashedSecretID)