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.
|
// A list of role links and service identities are returned.
|
||||||
func (s *Server) evaluateRoleBindings(
|
func (s *Server) evaluateRoleBindings(
|
||||||
validator authmethod.Validator,
|
validator authmethod.Validator,
|
||||||
verifiedFields map[string]string,
|
verifiedIdentity *authmethod.Identity,
|
||||||
methodMeta *structs.EnterpriseMeta,
|
methodMeta *structs.EnterpriseMeta,
|
||||||
targetMeta *structs.EnterpriseMeta,
|
targetMeta *structs.EnterpriseMeta,
|
||||||
) ([]*structs.ACLServiceIdentity, []structs.ACLTokenRoleLink, error) {
|
) ([]*structs.ACLServiceIdentity, []structs.ACLTokenRoleLink, error) {
|
||||||
|
@ -54,13 +54,10 @@ func (s *Server) evaluateRoleBindings(
|
||||||
return nil, nil, nil
|
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.
|
// Find all binding rules that match the provided fields.
|
||||||
var matchingRules []*structs.ACLBindingRule
|
var matchingRules []*structs.ACLBindingRule
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if doesBindingRuleMatch(rule, selectableVars) {
|
if doesSelectorMatch(rule.Selector, verifiedIdentity.SelectableFields) {
|
||||||
matchingRules = append(matchingRules, rule)
|
matchingRules = append(matchingRules, rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +71,7 @@ func (s *Server) evaluateRoleBindings(
|
||||||
serviceIdentities []*structs.ACLServiceIdentity
|
serviceIdentities []*structs.ACLServiceIdentity
|
||||||
)
|
)
|
||||||
for _, rule := range matchingRules {
|
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 {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("cannot compute %q bind name for bind target: %v", rule.BindType, err)
|
return nil, nil, fmt.Errorf("cannot compute %q bind name for bind target: %v", rule.BindType, err)
|
||||||
} else if !valid {
|
} else if !valid {
|
||||||
|
@ -107,14 +104,13 @@ func (s *Server) evaluateRoleBindings(
|
||||||
return serviceIdentities, roleLinks, nil
|
return serviceIdentities, roleLinks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// doesBindingRuleMatch checks that a single binding rule matches the provided
|
// doesSelectorMatch checks that a single selector matches the provided vars.
|
||||||
// vars.
|
func doesSelectorMatch(selector string, selectableVars interface{}) bool {
|
||||||
func doesBindingRuleMatch(rule *structs.ACLBindingRule, selectableVars interface{}) bool {
|
if selector == "" {
|
||||||
if rule.Selector == "" {
|
|
||||||
return true // catch-all
|
return true // catch-all
|
||||||
}
|
}
|
||||||
|
|
||||||
eval, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, selectableVars)
|
eval, err := bexpr.CreateEvaluatorForType(selector, nil, selectableVars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false // fails to match if selector is invalid
|
return false // fails to match if selector is invalid
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,10 @@ package consul
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDoesBindingRuleMatch(t *testing.T) {
|
func TestDoesSelectorMatch(t *testing.T) {
|
||||||
type matchable struct {
|
type matchable struct {
|
||||||
A string `bexpr:"a"`
|
A string `bexpr:"a"`
|
||||||
C string `bexpr:"c"`
|
C string `bexpr:"c"`
|
||||||
|
@ -40,8 +39,7 @@ func TestDoesBindingRuleMatch(t *testing.T) {
|
||||||
"", &matchable{A: "b"}, true},
|
"", &matchable{A: "b"}, true},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
rule := structs.ACLBindingRule{Selector: test.selector}
|
ok := doesSelectorMatch(test.selector, test.details)
|
||||||
ok := doesBindingRuleMatch(&rule, test.details)
|
|
||||||
require.Equal(t, test.ok, ok)
|
require.Equal(t, test.ok, ok)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package consul
|
package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -685,13 +686,13 @@ func validateBindingRuleBindName(bindType, bindName string, availableFields []st
|
||||||
}
|
}
|
||||||
|
|
||||||
// computeBindingRuleBindName processes the HIL for the provided bind type+name
|
// 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 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 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.
|
// - 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) {
|
func computeBindingRuleBindName(bindType, bindName string, projectedVars map[string]string) (string, bool, error) {
|
||||||
bindName, err := InterpolateHIL(bindName, verifiedFields)
|
bindName, err := InterpolateHIL(bindName, projectedVars, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
|
@ -1870,10 +1871,11 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a blank placeholder identity for use in validation below.
|
||||||
|
blankID := validator.NewIdentity()
|
||||||
|
|
||||||
if rule.Selector != "" {
|
if rule.Selector != "" {
|
||||||
selectableVars := validator.MakeFieldMapSelectable(map[string]string{})
|
if _, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, blankID.SelectableFields); err != nil {
|
||||||
_, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, selectableVars)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid Binding Rule: Selector is invalid: %v", err)
|
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)
|
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)
|
return fmt.Errorf("Invalid Binding Rule: invalid BindName: %v", err)
|
||||||
} else if !valid {
|
} else if !valid {
|
||||||
return fmt.Errorf("Invalid Binding Rule: invalid BindName")
|
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 {
|
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 {
|
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
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// This always will return a valid pointer
|
// This always will return a valid pointer
|
||||||
targetMeta := method.TargetEnterpriseMeta(desiredMeta)
|
targetMeta := method.TargetEnterpriseMeta(verifiedIdentity.EnterpriseMeta)
|
||||||
|
|
||||||
// 3. send map through role bindings
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package authmethod
|
package authmethod
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -31,6 +32,9 @@ type Validator interface {
|
||||||
// Name returns the name of the auth method backing this validator.
|
// Name returns the name of the auth method backing this validator.
|
||||||
Name() string
|
Name() string
|
||||||
|
|
||||||
|
// NewIdentity creates a blank identity populated with empty values.
|
||||||
|
NewIdentity() *Identity
|
||||||
|
|
||||||
// ValidateLogin takes raw user-provided auth method metadata and ensures
|
// ValidateLogin takes raw user-provided auth method metadata and ensures
|
||||||
// it is sane, provably correct, and currently valid. Relevant identifying
|
// it is sane, provably correct, and currently valid. Relevant identifying
|
||||||
// data is extracted and returned for immediate use by the role binding
|
// 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
|
// Returns auth method specific metadata suitable for the Role Binding
|
||||||
// process as well as the desired enterprise meta for the token to be
|
// process as well as the desired enterprise meta for the token to be
|
||||||
// created.
|
// 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
|
// Stop should be called to cease any background activity and free up
|
||||||
// result of ValidateLogin. These are valid fields for use in any
|
// resources.
|
||||||
// BindingRule tied to this auth method.
|
Stop()
|
||||||
AvailableFields() []string
|
}
|
||||||
|
|
||||||
// MakeFieldMapSelectable converts a field map as returned by ValidateLogin
|
type Identity struct {
|
||||||
// into a structure suitable for selection with a binding rule.
|
// SelectableFields is the format of this Identity suitable for selection
|
||||||
MakeFieldMapSelectable(fieldMap map[string]string) interface{}
|
// 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 (
|
var (
|
||||||
|
@ -116,6 +136,7 @@ func (c *authMethodCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, val
|
||||||
if prev.ModifyIndex >= idx {
|
if prev.ModifyIndex >= idx {
|
||||||
return prev.Validator
|
return prev.Validator
|
||||||
}
|
}
|
||||||
|
prev.Validator.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.entries[method.Name] = &authMethodValidatorEntry{
|
c.entries[method.Name] = &authMethodValidatorEntry{
|
||||||
|
@ -126,6 +147,9 @@ func (c *authMethodCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, val
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *authMethodCache) Purge() {
|
func (c *authMethodCache) Purge() {
|
||||||
|
for _, entry := range c.entries {
|
||||||
|
entry.Validator.Stop()
|
||||||
|
}
|
||||||
c.entries = make(map[string]*authMethodValidatorEntry)
|
c.entries = make(map[string]*authMethodValidatorEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,6 @@ func (c *syncCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, validator
|
||||||
|
|
||||||
func (c *syncCache) Purge() {
|
func (c *syncCache) Purge() {
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
c.cache.Purge()
|
c.cache.Purge()
|
||||||
c.lock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package kubeauth
|
package kubeauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -21,7 +22,7 @@ import (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// register this as an available auth method type
|
// 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)
|
v, err := NewValidator(method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) 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 {
|
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.
|
// 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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
} else if trResp.Status.Error != "" {
|
} 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 {
|
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)
|
// The username is of format: system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT)
|
||||||
parts := strings.Split(trResp.Status.User.Username, ":")
|
parts := strings.Split(trResp.Status.User.Username, ":")
|
||||||
if len(parts) != 4 {
|
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
|
// Validate the user that comes back from token review is a service account
|
||||||
if parts[0] != "system" || parts[1] != "serviceaccount" {
|
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 (
|
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.
|
// Check to see if there is an override name on the ServiceAccount object.
|
||||||
sa, err := v.saGetter.ServiceAccounts(saNamespace).Get(saName, client_metav1.GetOptions{})
|
sa, err := v.saGetter.ServiceAccounts(saNamespace).Get(saName, client_metav1.GetOptions{})
|
||||||
if err != nil {
|
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()
|
annotations := sa.GetObjectMeta().GetAnnotations()
|
||||||
|
@ -175,25 +178,37 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *struct
|
||||||
serviceAccountUIDField: saUID,
|
serviceAccountUIDField: saUID,
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields, v.k8sEntMetaFromFields(fields), nil
|
id := v.NewIdentity()
|
||||||
}
|
id.SelectableFields = &k8sFieldDetails{
|
||||||
|
|
||||||
func (p *Validator) AvailableFields() []string {
|
|
||||||
return []string{
|
|
||||||
serviceAccountNamespaceField,
|
|
||||||
serviceAccountNameField,
|
|
||||||
serviceAccountUIDField,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Validator) MakeFieldMapSelectable(fieldMap map[string]string) interface{} {
|
|
||||||
return &k8sFieldDetails{
|
|
||||||
ServiceAccount: k8sFieldDetailsServiceAccount{
|
ServiceAccount: k8sFieldDetailsServiceAccount{
|
||||||
Namespace: fieldMap[serviceAccountNamespaceField],
|
Namespace: fields[serviceAccountNamespaceField],
|
||||||
Name: fieldMap[serviceAccountNameField],
|
Name: fields[serviceAccountNameField],
|
||||||
UID: fieldMap[serviceAccountUIDField],
|
UID: fields[serviceAccountUIDField],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
for k, val := range fields {
|
||||||
|
id.ProjectedVars[k] = val
|
||||||
|
}
|
||||||
|
id.EnterpriseMeta = v.k8sEntMetaFromFields(fields)
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
type k8sFieldDetails struct {
|
type k8sFieldDetails struct {
|
||||||
|
|
|
@ -7,5 +7,5 @@ import "github.com/hashicorp/consul/agent/structs"
|
||||||
type enterpriseConfig struct{}
|
type enterpriseConfig struct{}
|
||||||
|
|
||||||
func (v *Validator) k8sEntMetaFromFields(fields map[string]string) *structs.EnterpriseMeta {
|
func (v *Validator) k8sEntMetaFromFields(fields map[string]string) *structs.EnterpriseMeta {
|
||||||
return structs.DefaultEnterpriseMeta()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package kubeauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/connect"
|
"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) {
|
func TestValidateLogin(t *testing.T) {
|
||||||
testSrv := StartTestAPIServer(t)
|
testSrv := StartTestAPIServer(t)
|
||||||
defer testSrv.Stop()
|
defer testSrv.Stop()
|
||||||
|
@ -101,18 +131,23 @@ func TestValidateLogin(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("invalid bearer token", func(t *testing.T) {
|
t.Run("invalid bearer token", func(t *testing.T) {
|
||||||
_, _, err := validator.ValidateLogin("invalid")
|
_, err := validator.ValidateLogin(context.Background(), "invalid")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("valid bearer token", func(t *testing.T) {
|
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.NoError(t, err)
|
||||||
require.Equal(t, map[string]string{
|
|
||||||
|
authmethod.RequireIdentityMatch(t, id, map[string]string{
|
||||||
"serviceaccount.namespace": "default",
|
"serviceaccount.namespace": "default",
|
||||||
"serviceaccount.name": "demo",
|
"serviceaccount.name": "demo",
|
||||||
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
|
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
|
||||||
}, fields)
|
},
|
||||||
|
`serviceaccount.namespace == default`,
|
||||||
|
`serviceaccount.name == "demo"`,
|
||||||
|
`serviceaccount.uid == "76091af4-4b56-11e9-ac4b-708b11801cbe"`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// annotate the account
|
// annotate the account
|
||||||
|
@ -125,13 +160,18 @@ func TestValidateLogin(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Run("valid bearer token with annotation", func(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.NoError(t, err)
|
||||||
require.Equal(t, map[string]string{
|
|
||||||
|
authmethod.RequireIdentityMatch(t, id, map[string]string{
|
||||||
"serviceaccount.namespace": "default",
|
"serviceaccount.namespace": "default",
|
||||||
"serviceaccount.name": "alternate-name",
|
"serviceaccount.name": "alternate-name",
|
||||||
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
|
"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
|
package testauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
@ -115,6 +116,8 @@ type Validator struct {
|
||||||
|
|
||||||
func (v *Validator) Name() string { return v.name }
|
func (v *Validator) Name() string { return v.name }
|
||||||
|
|
||||||
|
func (v *Validator) Stop() {}
|
||||||
|
|
||||||
// ValidateLogin takes raw user-provided auth method metadata and ensures it is
|
// ValidateLogin takes raw user-provided auth method metadata and ensures it is
|
||||||
// sane, provably correct, and currently valid. Relevant identifying data is
|
// sane, provably correct, and currently valid. Relevant identifying data is
|
||||||
// extracted and returned for immediate use by the role binding process.
|
// 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.
|
// to extend the life of the underlying token.
|
||||||
//
|
//
|
||||||
// Returns auth method specific metadata suitable for the Role Binding process.
|
// 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)
|
fields, valid := GetSessionToken(v.config.SessionID, loginToken)
|
||||||
if !valid {
|
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 (
|
const (
|
||||||
serviceAccountNamespaceField = "serviceaccount.namespace"
|
serviceAccountNamespaceField = "serviceaccount.namespace"
|
||||||
|
@ -146,18 +173,6 @@ var availableFields = []string{
|
||||||
serviceAccountUIDField,
|
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 {
|
type selectableVars struct {
|
||||||
ServiceAccount selectableServiceAccount `bexpr:"serviceaccount"`
|
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
|
// InterpolateHIL processes the string as if it were HIL and interpolates only
|
||||||
// the provided string->string map as possible variables.
|
// 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 {
|
if strings.Index(s, "${") == -1 {
|
||||||
// Skip going to the trouble of parsing something that has no HIL.
|
// Skip going to the trouble of parsing something that has no HIL.
|
||||||
return s, nil
|
return s, nil
|
||||||
|
@ -456,6 +456,9 @@ func InterpolateHIL(s string, vars map[string]string) (string, error) {
|
||||||
|
|
||||||
vm := make(map[string]ast.Variable)
|
vm := make(map[string]ast.Variable)
|
||||||
for k, v := range vars {
|
for k, v := range vars {
|
||||||
|
if lowercase {
|
||||||
|
v = strings.ToLower(v)
|
||||||
|
}
|
||||||
vm[k] = ast.Variable{
|
vm[k] = ast.Variable{
|
||||||
Type: ast.TypeString,
|
Type: ast.TypeString,
|
||||||
Value: v,
|
Value: v,
|
||||||
|
|
|
@ -441,124 +441,139 @@ func TestServersInDCMeetMinimumVersion(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInterpolateHIL(t *testing.T) {
|
func TestInterpolateHIL(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for name, test := range map[string]struct {
|
||||||
name string
|
in string
|
||||||
in string
|
vars map[string]string
|
||||||
vars map[string]string
|
exp string // when lower=false
|
||||||
exp string
|
expLower string // when lower=true
|
||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
// valid HIL
|
// valid HIL
|
||||||
{
|
"empty": {
|
||||||
"empty",
|
|
||||||
"",
|
"",
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
"no vars": {
|
||||||
"no vars",
|
|
||||||
"nothing",
|
"nothing",
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"nothing",
|
"nothing",
|
||||||
|
"nothing",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
"just lowercase var": {
|
||||||
"just var",
|
|
||||||
"${item}",
|
"${item}",
|
||||||
map[string]string{"item": "value"},
|
map[string]string{"item": "value"},
|
||||||
"value",
|
"value",
|
||||||
|
"value",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
"just uppercase var": {
|
||||||
"var in middle",
|
"${item}",
|
||||||
|
map[string]string{"item": "VaLuE"},
|
||||||
|
"VaLuE",
|
||||||
|
"value",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
"lowercase var in middle": {
|
||||||
"before ${item}after",
|
"before ${item}after",
|
||||||
map[string]string{"item": "value"},
|
map[string]string{"item": "value"},
|
||||||
"before valueafter",
|
"before valueafter",
|
||||||
|
"before valueafter",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
"uppercase var in middle": {
|
||||||
"two vars",
|
"before ${item}after",
|
||||||
|
map[string]string{"item": "VaLuE"},
|
||||||
|
"before VaLuEafter",
|
||||||
|
"before valueafter",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
"two vars": {
|
||||||
"before ${item}after ${more}",
|
"before ${item}after ${more}",
|
||||||
map[string]string{"item": "value", "more": "xyz"},
|
map[string]string{"item": "value", "more": "xyz"},
|
||||||
"before valueafter xyz",
|
"before valueafter xyz",
|
||||||
|
"before valueafter xyz",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
"missing map val": {
|
||||||
"missing map val",
|
|
||||||
"${item}",
|
"${item}",
|
||||||
map[string]string{"item": ""},
|
map[string]string{"item": ""},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
// "weird" HIL, but not technically invalid
|
// "weird" HIL, but not technically invalid
|
||||||
{
|
"just end": {
|
||||||
"just end",
|
|
||||||
"}",
|
"}",
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"}",
|
"}",
|
||||||
|
"}",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
"var without start": {
|
||||||
"var without start",
|
|
||||||
" item }",
|
" item }",
|
||||||
map[string]string{"item": "value"},
|
map[string]string{"item": "value"},
|
||||||
" item }",
|
" item }",
|
||||||
|
" item }",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
"two vars missing second start": {
|
||||||
"two vars missing second start",
|
|
||||||
"before ${ item }after more }",
|
"before ${ item }after more }",
|
||||||
map[string]string{"item": "value", "more": "xyz"},
|
map[string]string{"item": "value", "more": "xyz"},
|
||||||
"before valueafter more }",
|
"before valueafter more }",
|
||||||
|
"before valueafter more }",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
// invalid HIL
|
// invalid HIL
|
||||||
{
|
"just start": {
|
||||||
"just start",
|
|
||||||
"${",
|
"${",
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
"backwards": {
|
||||||
"backwards",
|
|
||||||
"}${",
|
"}${",
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
"no varname": {
|
||||||
"no varname",
|
|
||||||
"${}",
|
"${}",
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
"missing map key": {
|
||||||
"missing map key",
|
|
||||||
"${item}",
|
"${item}",
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"var without end",
|
|
||||||
"${ item ",
|
|
||||||
map[string]string{"item": "value"},
|
|
||||||
"",
|
"",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
"var without end": {
|
||||||
"two vars missing first end",
|
"${ item ",
|
||||||
|
map[string]string{"item": "value"},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
"two vars missing first end": {
|
||||||
"before ${ item after ${ more }",
|
"before ${ item after ${ more }",
|
||||||
map[string]string{"item": "value", "more": "xyz"},
|
map[string]string{"item": "value", "more": "xyz"},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
test := test
|
||||||
out, err := InterpolateHIL(test.in, test.vars)
|
t.Run(name+" lower=false", func(t *testing.T) {
|
||||||
|
out, err := InterpolateHIL(test.in, test.vars, false)
|
||||||
if test.ok {
|
if test.ok {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.exp, out)
|
require.Equal(t, test.exp, out)
|
||||||
|
@ -567,6 +582,16 @@ func TestInterpolateHIL(t *testing.T) {
|
||||||
require.Equal(t, out, "")
|
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