AuthMethod updates to support alternate namespace logins (#7029)

This commit is contained in:
Matt Keeler 2020-01-14 10:09:29 -05:00 committed by GitHub
parent cc8f580ddd
commit c8294b8595
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 190 additions and 52 deletions

View File

@ -358,6 +358,9 @@ func (s *HTTPServer) ACLTokenList(resp http.ResponseWriter, req *http.Request) (
args.Policy = req.URL.Query().Get("policy") args.Policy = req.URL.Query().Get("policy")
args.Role = req.URL.Query().Get("role") args.Role = req.URL.Query().Get("role")
args.AuthMethod = req.URL.Query().Get("authmethod") args.AuthMethod = req.URL.Query().Get("authmethod")
if err := parseACLAuthMethodEnterpriseMeta(req, &args.ACLAuthMethodEnterpriseMeta); err != nil {
return nil, err
}
var out structs.ACLTokenListResponse var out structs.ACLTokenListResponse
defer setMeta(resp, &out.QueryMeta) defer setMeta(resp, &out.QueryMeta)

View File

@ -1405,7 +1405,6 @@ func (s *HTTPServer) AgentHost(resp http.ResponseWriter, req *http.Request) (int
return nil, err return nil, err
} }
// TODO (namespaces) - pass through a real ent authz ctx
if rule != nil && rule.OperatorRead(nil) != acl.Allow { if rule != nil && rule.OperatorRead(nil) != acl.Allow {
return nil, acl.ErrPermissionDenied return nil, acl.ErrPermissionDenied
} }

View File

@ -43,10 +43,11 @@ func (s *Server) loadAuthMethodValidator(idx uint64, method *structs.ACLAuthMeth
func (s *Server) evaluateRoleBindings( func (s *Server) evaluateRoleBindings(
validator authmethod.Validator, validator authmethod.Validator,
verifiedFields map[string]string, verifiedFields map[string]string,
entMeta *structs.EnterpriseMeta, methodMeta *structs.EnterpriseMeta,
targetMeta *structs.EnterpriseMeta,
) ([]*structs.ACLServiceIdentity, []structs.ACLTokenRoleLink, error) { ) ([]*structs.ACLServiceIdentity, []structs.ACLTokenRoleLink, error) {
// Only fetch rules that are relevant for this method. // Only fetch rules that are relevant for this method.
_, rules, err := s.fsm.State().ACLBindingRuleList(nil, validator.Name(), entMeta) _, rules, err := s.fsm.State().ACLBindingRuleList(nil, validator.Name(), methodMeta)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} else if len(rules) == 0 { } else if len(rules) == 0 {
@ -87,7 +88,7 @@ func (s *Server) evaluateRoleBindings(
}) })
case structs.BindingRuleBindTypeRole: case structs.BindingRuleBindTypeRole:
_, role, err := s.fsm.State().ACLRoleGetByName(nil, bindName, &rule.EnterpriseMeta) _, role, err := s.fsm.State().ACLRoleGetByName(nil, bindName, targetMeta)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -828,9 +828,15 @@ func (a *ACL) TokenList(args *structs.ACLTokenListRequest, reply *structs.ACLTok
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
var methodMeta *structs.EnterpriseMeta
if args.AuthMethod != "" {
methodMeta = args.ACLAuthMethodEnterpriseMeta.ToEnterpriseMeta()
methodMeta.Merge(&args.EnterpriseMeta)
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta, return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, state *state.Store) error {
index, tokens, err := state.ACLTokenList(ws, args.IncludeLocal, args.IncludeGlobal, args.Policy, args.Role, args.AuthMethod, &args.EnterpriseMeta) index, tokens, err := state.ACLTokenList(ws, args.IncludeLocal, args.IncludeGlobal, args.Policy, args.Role, args.AuthMethod, methodMeta, &args.EnterpriseMeta)
if err != nil { if err != nil {
return err return err
} }
@ -2221,13 +2227,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, err := validator.ValidateLogin(auth.BearerToken) verifiedFields, desiredMeta, err := validator.ValidateLogin(auth.BearerToken)
if err != nil { if err != nil {
return err return err
} }
// This always will return a valid pointer
targetMeta := method.TargetEnterpriseMeta(desiredMeta)
// 3. send map through role bindings // 3. send map through role bindings
serviceIdentities, roleLinks, err := a.srv.evaluateRoleBindings(validator, verifiedFields, &auth.EnterpriseMeta) serviceIdentities, roleLinks, err := a.srv.evaluateRoleBindings(validator, verifiedFields, &auth.EnterpriseMeta, targetMeta)
if err != nil { if err != nil {
return err return err
} }
@ -2256,11 +2265,13 @@ func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLToken) erro
AuthMethod: auth.AuthMethod, AuthMethod: auth.AuthMethod,
ServiceIdentities: serviceIdentities, ServiceIdentities: serviceIdentities,
Roles: roleLinks, Roles: roleLinks,
EnterpriseMeta: auth.EnterpriseMeta, EnterpriseMeta: *targetMeta,
}, },
WriteRequest: args.WriteRequest, WriteRequest: args.WriteRequest,
} }
createReq.ACLToken.ACLAuthMethodEnterpriseMeta.FillWithEnterpriseMeta(&auth.EnterpriseMeta)
// 5. return token information like a TokenCreate would // 5. return token information like a TokenCreate would
err = a.tokenSetInternal(&createReq, reply, true) err = a.tokenSetInternal(&createReq, reply, true)

View File

@ -262,7 +262,7 @@ func (a *ACL) List(args *structs.DCSpecificRequest,
return a.srv.blockingQuery(&args.QueryOptions, return a.srv.blockingQuery(&args.QueryOptions,
&reply.QueryMeta, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, state *state.Store) error {
index, tokens, err := state.ACLTokenList(ws, false, true, "", "", "", nil) index, tokens, err := state.ACLTokenList(ws, false, true, "", "", "", nil, nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -138,7 +138,7 @@ func reconcileLegacyACLs(local, remote structs.ACLs, lastRemoteIndex uint64) str
// FetchLocalACLs returns the ACLs in the local state store. // FetchLocalACLs returns the ACLs in the local state store.
func (s *Server) fetchLocalLegacyACLs() (structs.ACLs, error) { func (s *Server) fetchLocalLegacyACLs() (structs.ACLs, error) {
_, local, err := s.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil) _, local, err := s.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -396,11 +396,11 @@ func TestACLReplication_LegacyTokens(t *testing.T) {
} }
checkSame := func() error { checkSame := func() error {
index, remote, err := s1.fsm.State().ACLTokenList(nil, true, true, "", "", "", nil) index, remote, err := s1.fsm.State().ACLTokenList(nil, true, true, "", "", "", nil, nil)
if err != nil { if err != nil {
return err return err
} }
_, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "", "", "", nil) _, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "", "", "", nil, nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -351,9 +351,9 @@ func TestACLReplication_Tokens(t *testing.T) {
checkSame := func(t *retry.R) { checkSame := func(t *retry.R) {
// only account for global tokens - local tokens shouldn't be replicated // only account for global tokens - local tokens shouldn't be replicated
index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil) index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
_, local, err := s2.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil) _, local, err := s2.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, local, len(remote)) require.Len(t, local, len(remote))
@ -451,7 +451,7 @@ func TestACLReplication_Tokens(t *testing.T) {
}) })
// verify dc2 local tokens didn't get blown away // verify dc2 local tokens didn't get blown away
_, local, err := s2.fsm.State().ACLTokenList(nil, true, false, "", "", "", nil) _, local, err := s2.fsm.State().ACLTokenList(nil, true, false, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, local, 50) require.Len(t, local, 50)
@ -787,10 +787,10 @@ func TestACLReplication_AllTypes(t *testing.T) {
checkSameTokens := func(t *retry.R) { checkSameTokens := func(t *retry.R) {
// only account for global tokens - local tokens shouldn't be replicated // only account for global tokens - local tokens shouldn't be replicated
index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil) index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
// Query for all of them, so that we can prove that no globals snuck in. // Query for all of them, so that we can prove that no globals snuck in.
_, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "", "", "", nil) _, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, remote, len(local)) require.Len(t, remote, len(local))

View File

@ -34,7 +34,7 @@ func (r *aclTokenReplicator) FetchRemote(srv *Server, lastRemoteIndex uint64) (i
func (r *aclTokenReplicator) FetchLocal(srv *Server) (int, uint64, error) { func (r *aclTokenReplicator) FetchLocal(srv *Server) (int, uint64, error) {
r.local = nil r.local = nil
idx, local, err := srv.fsm.State().ACLTokenList(nil, false, true, "", "", "", srv.replicationEnterpriseMeta()) idx, local, err := srv.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, srv.replicationEnterpriseMeta())
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }

View File

@ -39,8 +39,9 @@ type Validator interface {
// continue to extend the life of the underlying token. // continue to extend the life of the underlying token.
// //
// Returns auth method specific metadata suitable for the Role Binding // Returns auth method specific metadata suitable for the Role Binding
// process. // process as well as the desired enterprise meta for the token to be
ValidateLogin(loginToken string) (map[string]string, error) // created.
ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error)
// AvailableFields returns a slice of all fields that are returned as a // AvailableFields returns a slice of all fields that are returned as a
// result of ValidateLogin. These are valid fields for use in any // result of ValidateLogin. These are valid fields for use in any

View File

@ -50,6 +50,8 @@ type Config struct {
// other JWTs during login. It also must be able to read ServiceAccount // other JWTs during login. It also must be able to read ServiceAccount
// annotations. // annotations.
ServiceAccountJWT string `json:",omitempty"` ServiceAccountJWT string `json:",omitempty"`
enterpriseConfig `mapstructure:",squash"`
} }
// Validator is the wrapper around the relevant portions of the Kubernetes API // Validator is the wrapper around the relevant portions of the Kubernetes API
@ -116,9 +118,9 @@ 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, error) { func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error) {
if _, err := jwt.ParseSigned(loginToken); err != nil { if _, err := jwt.ParseSigned(loginToken); err != nil {
return nil, fmt.Errorf("failed to parse and validate JWT: %v", err) return nil, 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.
@ -129,24 +131,24 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, error)
}) })
if err != nil { if err != nil {
return nil, err return nil, nil, err
} else if trResp.Status.Error != "" { } else if trResp.Status.Error != "" {
return nil, fmt.Errorf("lookup failed: %s", trResp.Status.Error) return nil, nil, fmt.Errorf("lookup failed: %s", trResp.Status.Error)
} }
if !trResp.Status.Authenticated { if !trResp.Status.Authenticated {
return nil, errors.New("lookup failed: service account jwt not valid") return nil, 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, errors.New("lookup failed: unexpected username format") return nil, 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, errors.New("lookup failed: username returned is not a service account") return nil, nil, errors.New("lookup failed: username returned is not a service account")
} }
var ( var (
@ -158,7 +160,7 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, error)
// 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, fmt.Errorf("annotation lookup failed: %v", err) return nil, nil, fmt.Errorf("annotation lookup failed: %v", err)
} }
annotations := sa.GetObjectMeta().GetAnnotations() annotations := sa.GetObjectMeta().GetAnnotations()
@ -166,11 +168,13 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, error)
saName = serviceNameOverride saName = serviceNameOverride
} }
return map[string]string{ fields := map[string]string{
serviceAccountNamespaceField: saNamespace, serviceAccountNamespaceField: saNamespace,
serviceAccountNameField: saName, serviceAccountNameField: saName,
serviceAccountUIDField: saUID, serviceAccountUIDField: saUID,
}, nil }
return fields, v.k8sEntMetaFromFields(fields), nil
} }
func (p *Validator) AvailableFields() []string { func (p *Validator) AvailableFields() []string {

View File

@ -0,0 +1,11 @@
// +build !consulent
package kubeauth
import "github.com/hashicorp/consul/agent/structs"
type enterpriseConfig struct{}
func (v *Validator) k8sEntMetaFromFields(fields map[string]string) *structs.EnterpriseMeta {
return structs.DefaultEnterpriseMeta()
}

View File

@ -35,12 +35,12 @@ 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("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) fields, _, err := validator.ValidateLogin(goodJWT_B)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"serviceaccount.namespace": "default", "serviceaccount.namespace": "default",
@ -59,7 +59,7 @@ 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) fields, _, err := validator.ValidateLogin(goodJWT_B)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"serviceaccount.namespace": "default", "serviceaccount.namespace": "default",

View File

@ -80,6 +80,8 @@ func GetSessionToken(sessionID string, token string) (map[string]string, bool) {
type Config struct { type Config struct {
SessionID string // unique identifier for this set of tokens in the database SessionID string // unique identifier for this set of tokens in the database
enterpriseConfig `mapstructure:",squash"`
} }
func newValidator(method *structs.ACLAuthMethod) (authmethod.Validator, error) { func newValidator(method *structs.ACLAuthMethod) (authmethod.Validator, error) {
@ -120,13 +122,13 @@ 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, error) { func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error) {
fields, valid := GetSessionToken(v.config.SessionID, loginToken) fields, valid := GetSessionToken(v.config.SessionID, loginToken)
if !valid { if !valid {
return nil, acl.ErrNotFound return nil, nil, acl.ErrNotFound
} }
return fields, nil return fields, v.testAuthEntMetaFromFields(fields), nil
} }
func (v *Validator) AvailableFields() []string { return availableFields } func (v *Validator) AvailableFields() []string { return availableFields }

View File

@ -0,0 +1,13 @@
// +build !consulent
package testauth
import (
"github.com/hashicorp/consul/agent/structs"
)
type enterpriseConfig struct{}
func (v *Validator) testAuthEntMetaFromFields(fields map[string]string) *structs.EnterpriseMeta {
return nil
}

View File

@ -728,7 +728,7 @@ func (s *Store) aclTokenSetTxn(tx *memdb.Txn, idx uint64, token *structs.ACLToke
} }
if token.AuthMethod != "" { if token.AuthMethod != "" {
method, err := s.getAuthMethodWithTxn(tx, nil, token.AuthMethod, &token.EnterpriseMeta) method, err := s.getAuthMethodWithTxn(tx, nil, token.AuthMethod, token.ACLAuthMethodEnterpriseMeta.ToEnterpriseMeta())
if err != nil { if err != nil {
return err return err
} else if method == nil { } else if method == nil {
@ -838,7 +838,7 @@ func (s *Store) aclTokenGetTxn(tx *memdb.Txn, ws memdb.WatchSet, value, index st
} }
// ACLTokenList is used to list out all of the ACLs in the state store. // ACLTokenList is used to list out all of the ACLs in the state store.
func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy, role, methodName string, entMeta *structs.EnterpriseMeta) (uint64, structs.ACLTokens, error) { func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy, role, methodName string, methodMeta, entMeta *structs.EnterpriseMeta) (uint64, structs.ACLTokens, error) {
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -868,7 +868,7 @@ func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy, role
needLocalityFilter = true needLocalityFilter = true
} else if policy == "" && role == "" && methodName != "" { } else if policy == "" && role == "" && methodName != "" {
iter, err = s.aclTokenListByAuthMethod(tx, methodName, entMeta) iter, err = s.aclTokenListByAuthMethod(tx, methodName, methodMeta, entMeta)
needLocalityFilter = true needLocalityFilter = true
} else { } else {
@ -1052,9 +1052,9 @@ func (s *Store) aclTokenDeleteTxn(tx *memdb.Txn, idx uint64, value, index string
return s.aclTokenDeleteWithToken(tx, token.(*structs.ACLToken), idx) return s.aclTokenDeleteWithToken(tx, token.(*structs.ACLToken), idx)
} }
func (s *Store) aclTokenDeleteAllForAuthMethodTxn(tx *memdb.Txn, idx uint64, methodName string, entMeta *structs.EnterpriseMeta) error { func (s *Store) aclTokenDeleteAllForAuthMethodTxn(tx *memdb.Txn, idx uint64, methodName string, methodMeta *structs.EnterpriseMeta) error {
// collect them all // collect all the tokens linked with the given auth method.
iter, err := s.aclTokenListByAuthMethod(tx, methodName, entMeta) iter, err := s.aclTokenListByAuthMethod(tx, methodName, methodMeta, structs.WildcardEnterpriseMeta())
if err != nil { if err != nil {
return fmt.Errorf("failed acl token lookup: %v", err) return fmt.Errorf("failed acl token lookup: %v", err)
} }

View File

@ -297,7 +297,7 @@ func (s *Store) aclTokenListByRole(tx *memdb.Txn, role string, _ *structs.Enterp
return tx.Get("acl-tokens", "roles", role) return tx.Get("acl-tokens", "roles", role)
} }
func (s *Store) aclTokenListByAuthMethod(tx *memdb.Txn, authMethod string, _ *structs.EnterpriseMeta) (memdb.ResultIterator, error) { func (s *Store) aclTokenListByAuthMethod(tx *memdb.Txn, authMethod string, _, _ *structs.EnterpriseMeta) (memdb.ResultIterator, error) {
return tx.Get("acl-tokens", "authmethod", authMethod) return tx.Get("acl-tokens", "authmethod", authMethod)
} }

View File

@ -218,7 +218,7 @@ func TestStateStore_ACLBootstrap(t *testing.T) {
require.Equal(t, uint64(3), index) require.Equal(t, uint64(3), index)
// Make sure the ACLs are in an expected state. // Make sure the ACLs are in an expected state.
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil) _, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, tokens, 1) require.Len(t, tokens, 1)
compareTokens(t, token1, tokens[0]) compareTokens(t, token1, tokens[0])
@ -232,7 +232,7 @@ func TestStateStore_ACLBootstrap(t *testing.T) {
err = s.ACLBootstrap(32, index, token2.Clone(), false) err = s.ACLBootstrap(32, index, token2.Clone(), false)
require.NoError(t, err) require.NoError(t, err)
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil) _, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, tokens, 2) require.Len(t, tokens, 2)
} }
@ -1203,7 +1203,7 @@ func TestStateStore_ACLToken_List(t *testing.T) {
{testPolicyID_A, testRoleID_A, ""}, {testPolicyID_A, testRoleID_A, ""},
} { } {
t.Run(fmt.Sprintf("can't filter on more than one: %s/%s/%s", tc.policy, tc.role, tc.methodName), func(t *testing.T) { t.Run(fmt.Sprintf("can't filter on more than one: %s/%s/%s", tc.policy, tc.role, tc.methodName), func(t *testing.T) {
_, _, err := s.ACLTokenList(nil, false, false, tc.policy, tc.role, tc.methodName, nil) _, _, err := s.ACLTokenList(nil, false, false, tc.policy, tc.role, tc.methodName, nil, nil)
require.Error(t, err) require.Error(t, err)
}) })
} }
@ -1212,7 +1212,7 @@ func TestStateStore_ACLToken_List(t *testing.T) {
tc := tc // capture range variable tc := tc // capture range variable
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
_, tokens, err := s.ACLTokenList(nil, tc.local, tc.global, tc.policy, tc.role, tc.methodName, nil) _, tokens, err := s.ACLTokenList(nil, tc.local, tc.global, tc.policy, tc.role, tc.methodName, nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, tokens, len(tc.accessors)) require.Len(t, tokens, len(tc.accessors))
tokens.Sort() tokens.Sort()
@ -1273,7 +1273,7 @@ func TestStateStore_ACLToken_FixupPolicyLinks(t *testing.T) {
require.Equal(t, "node-read-renamed", retrieved.Policies[0].Name) require.Equal(t, "node-read-renamed", retrieved.Policies[0].Name)
// list tokens without stale links // list tokens without stale links
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil) _, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
found := false found := false
@ -1317,7 +1317,7 @@ func TestStateStore_ACLToken_FixupPolicyLinks(t *testing.T) {
require.Len(t, retrieved.Policies, 0) require.Len(t, retrieved.Policies, 0)
// list tokens without stale links // list tokens without stale links
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil) _, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
found = false found = false
@ -1402,7 +1402,7 @@ func TestStateStore_ACLToken_FixupRoleLinks(t *testing.T) {
require.Equal(t, "node-read-role-renamed", retrieved.Roles[0].Name) require.Equal(t, "node-read-role-renamed", retrieved.Roles[0].Name)
// list tokens without stale links // list tokens without stale links
_, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil) _, tokens, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
found := false found := false
@ -1446,7 +1446,7 @@ func TestStateStore_ACLToken_FixupRoleLinks(t *testing.T) {
require.Len(t, retrieved.Roles, 0) require.Len(t, retrieved.Roles, 0)
// list tokens without stale links // list tokens without stale links
_, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil) _, tokens, err = s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
found = false found = false
@ -3610,7 +3610,7 @@ func TestStateStore_ACLTokens_Snapshot_Restore(t *testing.T) {
require.NoError(t, s.ACLRoleBatchSet(2, roles, false)) require.NoError(t, s.ACLRoleBatchSet(2, roles, false))
// Read the restored ACLs back out and verify that they match. // Read the restored ACLs back out and verify that they match.
idx, res, err := s.ACLTokenList(nil, true, true, "", "", "", nil) idx, res, err := s.ACLTokenList(nil, true, true, "", "", "", nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(4), idx) require.Equal(t, uint64(4), idx)
require.ElementsMatch(t, tokens, res) require.ElementsMatch(t, tokens, res)

View File

@ -44,3 +44,11 @@ func (s *HTTPServer) rewordUnknownEnterpriseFieldError(err error) error {
} }
func (s *HTTPServer) addEnterpriseHTMLTemplateVars(vars map[string]interface{}) {} func (s *HTTPServer) addEnterpriseHTMLTemplateVars(vars map[string]interface{}) {}
func parseACLAuthMethodEnterpriseMeta(req *http.Request, _ *structs.ACLAuthMethodEnterpriseMeta) error {
if methodNS := req.URL.Query().Get("authmethod-ns"); methodNS != "" {
return BadRequestError{Reason: "Invalid query paramter: \"authmethod-ns\" - Namespaces is a Consul Enterprise feature"}
}
return nil
}

View File

@ -12,6 +12,7 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-msgpack/codec"
"golang.org/x/crypto/blake2b" "golang.org/x/crypto/blake2b"
) )
@ -227,6 +228,9 @@ type ACLToken struct {
// AuthMethod is the name of the auth method used to create this token. // AuthMethod is the name of the auth method used to create this token.
AuthMethod string `json:",omitempty"` AuthMethod string `json:",omitempty"`
// ACLAuthMethodEnterpriseMeta is the EnterpriseMeta for the AuthMethod that this token was created from
ACLAuthMethodEnterpriseMeta
// ExpirationTime represents the point after which a token should be // ExpirationTime represents the point after which a token should be
// considered revoked and is eligible for destruction. The zero value // considered revoked and is eligible for destruction. The zero value
// represents NO expiration. // represents NO expiration.
@ -1044,6 +1048,49 @@ type ACLAuthMethod struct {
RaftIndex `hash:"ignore"` RaftIndex `hash:"ignore"`
} }
// MarshalBinary writes ACLAuthMethod as msgpack encoded. It's only here
// because we need custom decoding of the raw interface{} values and this
// completes the interface.
func (m *ACLAuthMethod) MarshalBinary() (data []byte, err error) {
// bs will grow if needed but allocate enough to avoid reallocation in common
// case.
bs := make([]byte, 256)
enc := codec.NewEncoderBytes(&bs, msgpackHandle)
type Alias ACLAuthMethod
if err := enc.Encode((*Alias)(m)); err != nil {
return nil, err
}
return bs, nil
}
// UnmarshalBinary decodes msgpack encoded ACLAuthMethod. It used
// default msgpack encoding but fixes up the uint8 strings and other problems we
// have with encoding map[string]interface{}.
func (m *ACLAuthMethod) UnmarshalBinary(data []byte) error {
dec := codec.NewDecoderBytes(data, msgpackHandle)
type Alias ACLAuthMethod
var a Alias
if err := dec.Decode(&a); err != nil {
return err
}
*m = ACLAuthMethod(a)
var err error
// Fix strings and maps in the returned maps
m.Config, err = lib.MapWalk(m.Config)
if err != nil {
return err
}
return nil
}
type ACLReplicationType string type ACLReplicationType string
const ( const (
@ -1128,6 +1175,7 @@ type ACLTokenListRequest struct {
Role string // Role filter Role string // Role filter
AuthMethod string // Auth Method filter AuthMethod string // Auth Method filter
Datacenter string // The datacenter to perform the request within Datacenter string // The datacenter to perform the request within
ACLAuthMethodEnterpriseMeta
EnterpriseMeta EnterpriseMeta
QueryOptions QueryOptions
} }

View File

@ -28,6 +28,16 @@ node_prefix "" {
}` }`
) )
type ACLAuthMethodEnterpriseMeta struct{}
func (_ *ACLAuthMethodEnterpriseMeta) FillWithEnterpriseMeta(_ *EnterpriseMeta) {
// do nothing
}
func (_ *ACLAuthMethodEnterpriseMeta) ToEnterpriseMeta() *EnterpriseMeta {
return DefaultEnterpriseMeta()
}
func aclServiceIdentityRules(svc string, _ *EnterpriseMeta) string { func aclServiceIdentityRules(svc string, _ *EnterpriseMeta) string {
return fmt.Sprintf(aclPolicyTemplateServiceIdentity, svc) return fmt.Sprintf(aclPolicyTemplateServiceIdentity, svc)
} }
@ -35,3 +45,7 @@ func aclServiceIdentityRules(svc string, _ *EnterpriseMeta) string {
func (p *ACLPolicy) EnterprisePolicyMeta() *acl.EnterprisePolicyMeta { func (p *ACLPolicy) EnterprisePolicyMeta() *acl.EnterprisePolicyMeta {
return nil return nil
} }
func (m *ACLAuthMethod) TargetEnterpriseMeta(_ *EnterpriseMeta) *EnterpriseMeta {
return &m.EnterpriseMeta
}

View File

@ -553,6 +553,12 @@ The table below shows this endpoint's support for
- `authmethod` `(string: "")` - Filters the token list to those tokens that are - `authmethod` `(string: "")` - Filters the token list to those tokens that are
linked with the specific named auth method. linked with the specific named auth method.
- `authmethod-ns` `(string: "")` - **(Enterprise Only)** Specifics the namespace
of the `authmethod` being used for token lookup. If not provided, the namespace
provided by the `ns` parameter will be used. If neither of those is provided
then the namespace will be inherited from the request's ACL token. Added in
Consul 1.7.0.
- `ns` `(string: "")` - **(Enterprise Only)** Specifies the namespace to list - `ns` `(string: "")` - **(Enterprise Only)** Specifies the namespace to list
the tokens for. This value can be specified as the `ns` URL query the tokens for. This value can be specified as the `ns` URL query
parameter or the `X-Consul-Namespace` header. If not provided by either, parameter or the `X-Consul-Namespace` header. If not provided by either,

View File

@ -34,6 +34,23 @@ parameters are required to properly configure an auth method of type
- `ServiceAccountJWT` `(string: <required>)` - A Service Account Token - `ServiceAccountJWT` `(string: <required>)` - A Service Account Token
([JWT](https://jwt.io/ "JSON Web Token")) used by the Consul leader to ([JWT](https://jwt.io/ "JSON Web Token")) used by the Consul leader to
validate application JWTs during login. validate application JWTs during login.
- `MapNamespaces` `(bool: <false>)` - **(Enterprise Only)** Indicates whether
the auth method should attempt to map the Kubernetes namespace to a Consul
namespace instead of creating tokens in the auth methods own namespace. Note
that mapping namespaces requires the auth method to reside within the
`default` namespace.
- `ConsulNamespacePrefix` `(string: <optional>)` - **(Enterprise Only)** When
`MapNamespaces` is enabled, this value will be prefixed to the Kubernetes
namespace to determine the Consul namespace to create the new token within.
- `ConsulNamespaceOverrides` `(map: <string:string>)` - **(Enterprise Only)**
This field is a mapping of Kubernetes namespace names to Consul namespace
names. If a Kubernetes namespace is present within this map, the value will
be used without adding the `ConsulNamespacePrefix`. If the value in the map
is `""` then the auth methods namespace will be used instead of attempting
to determine an alternate namespace.
### Sample Config ### Sample Config