acl: refactor the authmethod.Validator interface (#7760)
This is a collection of refactors that make upcoming PRs easier to digest. The main change is the introduction of the authmethod.Identity struct. In the one and only current auth method (type=kubernetes) all of the trusted identity attributes are both selectable and projectable, so they were just passed around as a map[string]string. When namespaces were added, this was slightly changed so that the enterprise metadata can also come back from the login operation, so login now returned two fields. Now with some upcoming auth methods it won't be true that all identity attributes will be both selectable and projectable, so rather than update the login function to return 3 pieces of data it seemed worth it to wrap those fields up and give them a proper name.
This commit is contained in:
parent
1697971a8f
commit
3ac5a841ec
|
@ -42,7 +42,7 @@ func (s *Server) loadAuthMethodValidator(idx uint64, method *structs.ACLAuthMeth
|
|||
// A list of role links and service identities are returned.
|
||||
func (s *Server) evaluateRoleBindings(
|
||||
validator authmethod.Validator,
|
||||
verifiedFields map[string]string,
|
||||
verifiedIdentity *authmethod.Identity,
|
||||
methodMeta *structs.EnterpriseMeta,
|
||||
targetMeta *structs.EnterpriseMeta,
|
||||
) ([]*structs.ACLServiceIdentity, []structs.ACLTokenRoleLink, error) {
|
||||
|
@ -54,13 +54,10 @@ func (s *Server) evaluateRoleBindings(
|
|||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Convert the fields into something suitable for go-bexpr.
|
||||
selectableVars := validator.MakeFieldMapSelectable(verifiedFields)
|
||||
|
||||
// Find all binding rules that match the provided fields.
|
||||
var matchingRules []*structs.ACLBindingRule
|
||||
for _, rule := range rules {
|
||||
if doesBindingRuleMatch(rule, selectableVars) {
|
||||
if doesSelectorMatch(rule.Selector, verifiedIdentity.SelectableFields) {
|
||||
matchingRules = append(matchingRules, rule)
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +71,7 @@ func (s *Server) evaluateRoleBindings(
|
|||
serviceIdentities []*structs.ACLServiceIdentity
|
||||
)
|
||||
for _, rule := range matchingRules {
|
||||
bindName, valid, err := computeBindingRuleBindName(rule.BindType, rule.BindName, verifiedFields)
|
||||
bindName, valid, err := computeBindingRuleBindName(rule.BindType, rule.BindName, verifiedIdentity.ProjectedVars)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot compute %q bind name for bind target: %v", rule.BindType, err)
|
||||
} else if !valid {
|
||||
|
@ -107,14 +104,13 @@ func (s *Server) evaluateRoleBindings(
|
|||
return serviceIdentities, roleLinks, nil
|
||||
}
|
||||
|
||||
// doesBindingRuleMatch checks that a single binding rule matches the provided
|
||||
// vars.
|
||||
func doesBindingRuleMatch(rule *structs.ACLBindingRule, selectableVars interface{}) bool {
|
||||
if rule.Selector == "" {
|
||||
// doesSelectorMatch checks that a single selector matches the provided vars.
|
||||
func doesSelectorMatch(selector string, selectableVars interface{}) bool {
|
||||
if selector == "" {
|
||||
return true // catch-all
|
||||
}
|
||||
|
||||
eval, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, selectableVars)
|
||||
eval, err := bexpr.CreateEvaluatorForType(selector, nil, selectableVars)
|
||||
if err != nil {
|
||||
return false // fails to match if selector is invalid
|
||||
}
|
||||
|
|
|
@ -3,11 +3,10 @@ package consul
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDoesBindingRuleMatch(t *testing.T) {
|
||||
func TestDoesSelectorMatch(t *testing.T) {
|
||||
type matchable struct {
|
||||
A string `bexpr:"a"`
|
||||
C string `bexpr:"c"`
|
||||
|
@ -40,8 +39,7 @@ func TestDoesBindingRuleMatch(t *testing.T) {
|
|||
"", &matchable{A: "b"}, true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rule := structs.ACLBindingRule{Selector: test.selector}
|
||||
ok := doesBindingRuleMatch(&rule, test.details)
|
||||
ok := doesSelectorMatch(test.selector, test.details)
|
||||
require.Equal(t, test.ok, ok)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -685,13 +686,13 @@ func validateBindingRuleBindName(bindType, bindName string, availableFields []st
|
|||
}
|
||||
|
||||
// computeBindingRuleBindName processes the HIL for the provided bind type+name
|
||||
// using the verified fields.
|
||||
// using the projected variables.
|
||||
//
|
||||
// - If the HIL is invalid ("", false, AN_ERROR) is returned.
|
||||
// - If the computed name is not valid for the type ("INVALID_NAME", false, nil) is returned.
|
||||
// - If the computed name is valid for the type ("VALID_NAME", true, nil) is returned.
|
||||
func computeBindingRuleBindName(bindType, bindName string, verifiedFields map[string]string) (string, bool, error) {
|
||||
bindName, err := InterpolateHIL(bindName, verifiedFields)
|
||||
func computeBindingRuleBindName(bindType, bindName string, projectedVars map[string]string) (string, bool, error) {
|
||||
bindName, err := InterpolateHIL(bindName, projectedVars, true)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
@ -1870,10 +1871,11 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
|
|||
return err
|
||||
}
|
||||
|
||||
// Create a blank placeholder identity for use in validation below.
|
||||
blankID := validator.NewIdentity()
|
||||
|
||||
if rule.Selector != "" {
|
||||
selectableVars := validator.MakeFieldMapSelectable(map[string]string{})
|
||||
_, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, selectableVars)
|
||||
if err != nil {
|
||||
if _, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, blankID.SelectableFields); err != nil {
|
||||
return fmt.Errorf("invalid Binding Rule: Selector is invalid: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -1893,7 +1895,7 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
|
|||
return fmt.Errorf("Invalid Binding Rule: unknown BindType %q", rule.BindType)
|
||||
}
|
||||
|
||||
if valid, err := validateBindingRuleBindName(rule.BindType, rule.BindName, validator.AvailableFields()); err != nil {
|
||||
if valid, err := validateBindingRuleBindName(rule.BindType, rule.BindName, blankID.ProjectedVarNames()); err != nil {
|
||||
return fmt.Errorf("Invalid Binding Rule: invalid BindName: %v", err)
|
||||
} else if !valid {
|
||||
return fmt.Errorf("Invalid Binding Rule: invalid BindName")
|
||||
|
@ -1909,7 +1911,7 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
|
|||
}
|
||||
|
||||
if respErr, ok := resp.(error); ok {
|
||||
return respErr
|
||||
return fmt.Errorf("Failed to apply binding rule upsert request: %v", respErr)
|
||||
}
|
||||
|
||||
if _, rule, err := a.srv.fsm.State().ACLBindingRuleGetByID(nil, rule.ID, &rule.EnterpriseMeta); err == nil && rule != nil {
|
||||
|
@ -2283,16 +2285,16 @@ func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLToken) erro
|
|||
}
|
||||
|
||||
// 2. Send args.Data.BearerToken to method validator and get back a fields map
|
||||
verifiedFields, desiredMeta, err := validator.ValidateLogin(auth.BearerToken)
|
||||
verifiedIdentity, err := validator.ValidateLogin(context.Background(), auth.BearerToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This always will return a valid pointer
|
||||
targetMeta := method.TargetEnterpriseMeta(desiredMeta)
|
||||
targetMeta := method.TargetEnterpriseMeta(verifiedIdentity.EnterpriseMeta)
|
||||
|
||||
// 3. send map through role bindings
|
||||
serviceIdentities, roleLinks, err := a.srv.evaluateRoleBindings(validator, verifiedFields, &auth.EnterpriseMeta, targetMeta)
|
||||
serviceIdentities, roleLinks, err := a.srv.evaluateRoleBindings(validator, verifiedIdentity, &auth.EnterpriseMeta, targetMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package authmethod
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
@ -31,6 +32,9 @@ type Validator interface {
|
|||
// Name returns the name of the auth method backing this validator.
|
||||
Name() string
|
||||
|
||||
// NewIdentity creates a blank identity populated with empty values.
|
||||
NewIdentity() *Identity
|
||||
|
||||
// ValidateLogin takes raw user-provided auth method metadata and ensures
|
||||
// it is sane, provably correct, and currently valid. Relevant identifying
|
||||
// data is extracted and returned for immediate use by the role binding
|
||||
|
@ -42,16 +46,32 @@ type Validator interface {
|
|||
// Returns auth method specific metadata suitable for the Role Binding
|
||||
// process as well as the desired enterprise meta for the token to be
|
||||
// created.
|
||||
ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error)
|
||||
ValidateLogin(ctx context.Context, loginToken string) (*Identity, error)
|
||||
|
||||
// AvailableFields returns a slice of all fields that are returned as a
|
||||
// result of ValidateLogin. These are valid fields for use in any
|
||||
// BindingRule tied to this auth method.
|
||||
AvailableFields() []string
|
||||
// Stop should be called to cease any background activity and free up
|
||||
// resources.
|
||||
Stop()
|
||||
}
|
||||
|
||||
// MakeFieldMapSelectable converts a field map as returned by ValidateLogin
|
||||
// into a structure suitable for selection with a binding rule.
|
||||
MakeFieldMapSelectable(fieldMap map[string]string) interface{}
|
||||
type Identity struct {
|
||||
// SelectableFields is the format of this Identity suitable for selection
|
||||
// with a binding rule.
|
||||
SelectableFields interface{}
|
||||
|
||||
// ProjectedVars is the format of this Identity suitable for interpolation
|
||||
// in a bind name within a binding rule.
|
||||
ProjectedVars map[string]string
|
||||
|
||||
*structs.EnterpriseMeta
|
||||
}
|
||||
|
||||
// ProjectedVarNames returns just the keyspace of the ProjectedVars map.
|
||||
func (i *Identity) ProjectedVarNames() []string {
|
||||
v := make([]string, 0, len(i.ProjectedVars))
|
||||
for k, _ := range i.ProjectedVars {
|
||||
v = append(v, k)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -116,6 +136,7 @@ func (c *authMethodCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, val
|
|||
if prev.ModifyIndex >= idx {
|
||||
return prev.Validator
|
||||
}
|
||||
prev.Validator.Stop()
|
||||
}
|
||||
|
||||
c.entries[method.Name] = &authMethodValidatorEntry{
|
||||
|
@ -126,6 +147,9 @@ func (c *authMethodCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, val
|
|||
}
|
||||
|
||||
func (c *authMethodCache) Purge() {
|
||||
for _, entry := range c.entries {
|
||||
entry.Validator.Stop()
|
||||
}
|
||||
c.entries = make(map[string]*authMethodValidatorEntry)
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,6 @@ func (c *syncCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, validator
|
|||
|
||||
func (c *syncCache) Purge() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.cache.Purge()
|
||||
c.lock.Unlock()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package kubeauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
@ -21,7 +22,7 @@ import (
|
|||
|
||||
func init() {
|
||||
// register this as an available auth method type
|
||||
authmethod.Register("kubernetes", func(_ hclog.Logger, method *structs.ACLAuthMethod) (authmethod.Validator, error) {
|
||||
authmethod.Register("kubernetes", func(logger hclog.Logger, method *structs.ACLAuthMethod) (authmethod.Validator, error) {
|
||||
v, err := NewValidator(method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -119,9 +120,11 @@ func NewValidator(method *structs.ACLAuthMethod) (*Validator, error) {
|
|||
|
||||
func (v *Validator) Name() string { return v.name }
|
||||
|
||||
func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error) {
|
||||
func (v *Validator) Stop() {}
|
||||
|
||||
func (v *Validator) ValidateLogin(ctx context.Context, loginToken string) (*authmethod.Identity, error) {
|
||||
if _, err := jwt.ParseSigned(loginToken); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse and validate JWT: %v", err)
|
||||
return nil, fmt.Errorf("failed to parse and validate JWT: %v", err)
|
||||
}
|
||||
|
||||
// Check TokenReview for the bulk of the work.
|
||||
|
@ -132,24 +135,24 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *struct
|
|||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
} else if trResp.Status.Error != "" {
|
||||
return nil, nil, fmt.Errorf("lookup failed: %s", trResp.Status.Error)
|
||||
return nil, fmt.Errorf("lookup failed: %s", trResp.Status.Error)
|
||||
}
|
||||
|
||||
if !trResp.Status.Authenticated {
|
||||
return nil, nil, errors.New("lookup failed: service account jwt not valid")
|
||||
return nil, errors.New("lookup failed: service account jwt not valid")
|
||||
}
|
||||
|
||||
// The username is of format: system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT)
|
||||
parts := strings.Split(trResp.Status.User.Username, ":")
|
||||
if len(parts) != 4 {
|
||||
return nil, nil, errors.New("lookup failed: unexpected username format")
|
||||
return nil, errors.New("lookup failed: unexpected username format")
|
||||
}
|
||||
|
||||
// Validate the user that comes back from token review is a service account
|
||||
if parts[0] != "system" || parts[1] != "serviceaccount" {
|
||||
return nil, nil, errors.New("lookup failed: username returned is not a service account")
|
||||
return nil, errors.New("lookup failed: username returned is not a service account")
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -161,7 +164,7 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *struct
|
|||
// Check to see if there is an override name on the ServiceAccount object.
|
||||
sa, err := v.saGetter.ServiceAccounts(saNamespace).Get(saName, client_metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("annotation lookup failed: %v", err)
|
||||
return nil, fmt.Errorf("annotation lookup failed: %v", err)
|
||||
}
|
||||
|
||||
annotations := sa.GetObjectMeta().GetAnnotations()
|
||||
|
@ -175,25 +178,37 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *struct
|
|||
serviceAccountUIDField: saUID,
|
||||
}
|
||||
|
||||
return fields, v.k8sEntMetaFromFields(fields), nil
|
||||
id := v.NewIdentity()
|
||||
id.SelectableFields = &k8sFieldDetails{
|
||||
ServiceAccount: k8sFieldDetailsServiceAccount{
|
||||
Namespace: fields[serviceAccountNamespaceField],
|
||||
Name: fields[serviceAccountNameField],
|
||||
UID: fields[serviceAccountUIDField],
|
||||
},
|
||||
}
|
||||
for k, val := range fields {
|
||||
id.ProjectedVars[k] = val
|
||||
}
|
||||
id.EnterpriseMeta = v.k8sEntMetaFromFields(fields)
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (p *Validator) AvailableFields() []string {
|
||||
return []string{
|
||||
func (v *Validator) NewIdentity() *authmethod.Identity {
|
||||
id := &authmethod.Identity{
|
||||
SelectableFields: &k8sFieldDetails{},
|
||||
ProjectedVars: map[string]string{},
|
||||
}
|
||||
for _, f := range availableFields {
|
||||
id.ProjectedVars[f] = ""
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
var availableFields = []string{
|
||||
serviceAccountNamespaceField,
|
||||
serviceAccountNameField,
|
||||
serviceAccountUIDField,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) MakeFieldMapSelectable(fieldMap map[string]string) interface{} {
|
||||
return &k8sFieldDetails{
|
||||
ServiceAccount: k8sFieldDetailsServiceAccount{
|
||||
Namespace: fieldMap[serviceAccountNamespaceField],
|
||||
Name: fieldMap[serviceAccountNameField],
|
||||
UID: fieldMap[serviceAccountUIDField],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type k8sFieldDetails struct {
|
||||
|
|
|
@ -7,5 +7,5 @@ import "github.com/hashicorp/consul/agent/structs"
|
|||
type enterpriseConfig struct{}
|
||||
|
||||
func (v *Validator) k8sEntMetaFromFields(fields map[string]string) *structs.EnterpriseMeta {
|
||||
return structs.DefaultEnterpriseMeta()
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package kubeauth
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
|
@ -74,6 +75,35 @@ func TestStructs_ACLAuthMethod_Kubernetes_MsgpackEncodeDecode(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNewIdentity(t *testing.T) {
|
||||
testSrv := StartTestAPIServer(t)
|
||||
defer testSrv.Stop()
|
||||
|
||||
method := &structs.ACLAuthMethod{
|
||||
Name: "test-k8s",
|
||||
Description: "k8s test",
|
||||
Type: "kubernetes",
|
||||
Config: map[string]interface{}{
|
||||
"Host": testSrv.Addr(),
|
||||
"CACert": testSrv.CACert(),
|
||||
"ServiceAccountJWT": goodJWT_A,
|
||||
},
|
||||
}
|
||||
validator, err := NewValidator(method)
|
||||
require.NoError(t, err)
|
||||
|
||||
id := validator.NewIdentity()
|
||||
authmethod.RequireIdentityMatch(t, id, map[string]string{
|
||||
"serviceaccount.namespace": "",
|
||||
"serviceaccount.name": "",
|
||||
"serviceaccount.uid": "",
|
||||
},
|
||||
`serviceaccount.namespace == ""`,
|
||||
`serviceaccount.name == ""`,
|
||||
`serviceaccount.uid == ""`,
|
||||
)
|
||||
}
|
||||
|
||||
func TestValidateLogin(t *testing.T) {
|
||||
testSrv := StartTestAPIServer(t)
|
||||
defer testSrv.Stop()
|
||||
|
@ -101,18 +131,23 @@ func TestValidateLogin(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
t.Run("invalid bearer token", func(t *testing.T) {
|
||||
_, _, err := validator.ValidateLogin("invalid")
|
||||
_, err := validator.ValidateLogin(context.Background(), "invalid")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("valid bearer token", func(t *testing.T) {
|
||||
fields, _, err := validator.ValidateLogin(goodJWT_B)
|
||||
id, err := validator.ValidateLogin(context.Background(), goodJWT_B)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
|
||||
authmethod.RequireIdentityMatch(t, id, map[string]string{
|
||||
"serviceaccount.namespace": "default",
|
||||
"serviceaccount.name": "demo",
|
||||
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
|
||||
}, fields)
|
||||
},
|
||||
`serviceaccount.namespace == default`,
|
||||
`serviceaccount.name == "demo"`,
|
||||
`serviceaccount.uid == "76091af4-4b56-11e9-ac4b-708b11801cbe"`,
|
||||
)
|
||||
})
|
||||
|
||||
// annotate the account
|
||||
|
@ -125,13 +160,18 @@ func TestValidateLogin(t *testing.T) {
|
|||
)
|
||||
|
||||
t.Run("valid bearer token with annotation", func(t *testing.T) {
|
||||
fields, _, err := validator.ValidateLogin(goodJWT_B)
|
||||
id, err := validator.ValidateLogin(context.Background(), goodJWT_B)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
|
||||
authmethod.RequireIdentityMatch(t, id, map[string]string{
|
||||
"serviceaccount.namespace": "default",
|
||||
"serviceaccount.name": "alternate-name",
|
||||
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
|
||||
}, fields)
|
||||
},
|
||||
`serviceaccount.namespace == default`,
|
||||
`serviceaccount.name == "alternate-name"`,
|
||||
`serviceaccount.uid == "76091af4-4b56-11e9-ac4b-708b11801cbe"`,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package testauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
|
@ -115,6 +116,8 @@ type Validator struct {
|
|||
|
||||
func (v *Validator) Name() string { return v.name }
|
||||
|
||||
func (v *Validator) Stop() {}
|
||||
|
||||
// ValidateLogin takes raw user-provided auth method metadata and ensures it is
|
||||
// sane, provably correct, and currently valid. Relevant identifying data is
|
||||
// extracted and returned for immediate use by the role binding process.
|
||||
|
@ -123,16 +126,40 @@ func (v *Validator) Name() string { return v.name }
|
|||
// to extend the life of the underlying token.
|
||||
//
|
||||
// Returns auth method specific metadata suitable for the Role Binding process.
|
||||
func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error) {
|
||||
func (v *Validator) ValidateLogin(ctx context.Context, loginToken string) (*authmethod.Identity, error) {
|
||||
fields, valid := GetSessionToken(v.config.SessionID, loginToken)
|
||||
if !valid {
|
||||
return nil, nil, acl.ErrNotFound
|
||||
return nil, acl.ErrNotFound
|
||||
}
|
||||
|
||||
return fields, v.testAuthEntMetaFromFields(fields), nil
|
||||
id := v.NewIdentity()
|
||||
id.SelectableFields = &selectableVars{
|
||||
ServiceAccount: selectableServiceAccount{
|
||||
Namespace: fields[serviceAccountNamespaceField],
|
||||
Name: fields[serviceAccountNameField],
|
||||
UID: fields[serviceAccountUIDField],
|
||||
},
|
||||
}
|
||||
for k, val := range fields {
|
||||
id.ProjectedVars[k] = val
|
||||
}
|
||||
id.EnterpriseMeta = v.testAuthEntMetaFromFields(fields)
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (v *Validator) AvailableFields() []string { return availableFields }
|
||||
func (v *Validator) NewIdentity() *authmethod.Identity {
|
||||
id := &authmethod.Identity{
|
||||
SelectableFields: &selectableVars{},
|
||||
ProjectedVars: map[string]string{},
|
||||
}
|
||||
|
||||
for _, f := range availableFields {
|
||||
id.ProjectedVars[f] = ""
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
const (
|
||||
serviceAccountNamespaceField = "serviceaccount.namespace"
|
||||
|
@ -146,18 +173,6 @@ var availableFields = []string{
|
|||
serviceAccountUIDField,
|
||||
}
|
||||
|
||||
// MakeFieldMapSelectable converts a field map as returned by ValidateLogin
|
||||
// into a structure suitable for selection with a binding rule.
|
||||
func (v *Validator) MakeFieldMapSelectable(fieldMap map[string]string) interface{} {
|
||||
return &selectableVars{
|
||||
ServiceAccount: selectableServiceAccount{
|
||||
Namespace: fieldMap[serviceAccountNamespaceField],
|
||||
Name: fieldMap[serviceAccountNameField],
|
||||
UID: fieldMap[serviceAccountUIDField],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type selectableVars struct {
|
||||
ServiceAccount selectableServiceAccount `bexpr:"serviceaccount"`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package authmethod
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/mitchellh/go-testing-interface"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// RequireIdentityMatch tests to see if the given Identity matches the provided
|
||||
// projected vars and filters for testing purpose.
|
||||
func RequireIdentityMatch(t testing.T, id *Identity, projectedVars map[string]string, filters ...string) {
|
||||
t.Helper()
|
||||
|
||||
gotNames := id.ProjectedVarNames()
|
||||
|
||||
require.Equal(t, projectedVars, id.ProjectedVars)
|
||||
|
||||
expectNames := make([]string, 0, len(projectedVars))
|
||||
for k, _ := range projectedVars {
|
||||
expectNames = append(expectNames, k)
|
||||
}
|
||||
sort.Strings(expectNames)
|
||||
sort.Strings(gotNames)
|
||||
|
||||
require.Equal(t, expectNames, gotNames)
|
||||
require.Nil(t, id.EnterpriseMeta)
|
||||
|
||||
for _, filter := range filters {
|
||||
eval, err := bexpr.CreateEvaluatorForType(filter, nil, id.SelectableFields)
|
||||
if err != nil {
|
||||
t.Fatalf("filter %q got err: %v", filter, err)
|
||||
}
|
||||
|
||||
result, err := eval.Evaluate(id.SelectableFields)
|
||||
if err != nil {
|
||||
t.Fatalf("filter %q got err: %v", filter, err)
|
||||
}
|
||||
|
||||
if !result {
|
||||
t.Fatalf("filter %q did not match", filter)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -443,7 +443,7 @@ func ServersGetACLMode(provider checkServersProvider, leaderAddr string, datacen
|
|||
|
||||
// InterpolateHIL processes the string as if it were HIL and interpolates only
|
||||
// the provided string->string map as possible variables.
|
||||
func InterpolateHIL(s string, vars map[string]string) (string, error) {
|
||||
func InterpolateHIL(s string, vars map[string]string, lowercase bool) (string, error) {
|
||||
if strings.Index(s, "${") == -1 {
|
||||
// Skip going to the trouble of parsing something that has no HIL.
|
||||
return s, nil
|
||||
|
@ -456,6 +456,9 @@ func InterpolateHIL(s string, vars map[string]string) (string, error) {
|
|||
|
||||
vm := make(map[string]ast.Variable)
|
||||
for k, v := range vars {
|
||||
if lowercase {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
vm[k] = ast.Variable{
|
||||
Type: ast.TypeString,
|
||||
Value: v,
|
||||
|
|
|
@ -441,124 +441,139 @@ func TestServersInDCMeetMinimumVersion(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInterpolateHIL(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
for name, test := range map[string]struct {
|
||||
in string
|
||||
vars map[string]string
|
||||
exp string
|
||||
exp string // when lower=false
|
||||
expLower string // when lower=true
|
||||
ok bool
|
||||
}{
|
||||
// valid HIL
|
||||
{
|
||||
"empty",
|
||||
"empty": {
|
||||
"",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"no vars",
|
||||
"no vars": {
|
||||
"nothing",
|
||||
map[string]string{},
|
||||
"nothing",
|
||||
"nothing",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"just var",
|
||||
"just lowercase var": {
|
||||
"${item}",
|
||||
map[string]string{"item": "value"},
|
||||
"value",
|
||||
"value",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"var in middle",
|
||||
"just uppercase var": {
|
||||
"${item}",
|
||||
map[string]string{"item": "VaLuE"},
|
||||
"VaLuE",
|
||||
"value",
|
||||
true,
|
||||
},
|
||||
"lowercase var in middle": {
|
||||
"before ${item}after",
|
||||
map[string]string{"item": "value"},
|
||||
"before valueafter",
|
||||
"before valueafter",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"two vars",
|
||||
"uppercase var in middle": {
|
||||
"before ${item}after",
|
||||
map[string]string{"item": "VaLuE"},
|
||||
"before VaLuEafter",
|
||||
"before valueafter",
|
||||
true,
|
||||
},
|
||||
"two vars": {
|
||||
"before ${item}after ${more}",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"before valueafter xyz",
|
||||
"before valueafter xyz",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"missing map val",
|
||||
"missing map val": {
|
||||
"${item}",
|
||||
map[string]string{"item": ""},
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
// "weird" HIL, but not technically invalid
|
||||
{
|
||||
"just end",
|
||||
"just end": {
|
||||
"}",
|
||||
map[string]string{},
|
||||
"}",
|
||||
"}",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"var without start",
|
||||
"var without start": {
|
||||
" item }",
|
||||
map[string]string{"item": "value"},
|
||||
" item }",
|
||||
" item }",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"two vars missing second start",
|
||||
"two vars missing second start": {
|
||||
"before ${ item }after more }",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"before valueafter more }",
|
||||
"before valueafter more }",
|
||||
true,
|
||||
},
|
||||
// invalid HIL
|
||||
{
|
||||
"just start",
|
||||
"just start": {
|
||||
"${",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"backwards",
|
||||
"backwards": {
|
||||
"}${",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no varname",
|
||||
"no varname": {
|
||||
"${}",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"missing map key",
|
||||
"missing map key": {
|
||||
"${item}",
|
||||
map[string]string{},
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"var without end",
|
||||
"${ item ",
|
||||
map[string]string{"item": "value"},
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"two vars missing first end",
|
||||
"var without end": {
|
||||
"${ item ",
|
||||
map[string]string{"item": "value"},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"two vars missing first end": {
|
||||
"before ${ item after ${ more }",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
out, err := InterpolateHIL(test.in, test.vars)
|
||||
test := test
|
||||
t.Run(name+" lower=false", func(t *testing.T) {
|
||||
out, err := InterpolateHIL(test.in, test.vars, false)
|
||||
if test.ok {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.exp, out)
|
||||
|
@ -567,6 +582,16 @@ func TestInterpolateHIL(t *testing.T) {
|
|||
require.Equal(t, out, "")
|
||||
}
|
||||
})
|
||||
t.Run(name+" lower=true", func(t *testing.T) {
|
||||
out, err := InterpolateHIL(test.in, test.vars, true)
|
||||
if test.ok {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expLower, out)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, out, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue