acl: gRPC login and logout endpoints (#12935)

Introduces two new public gRPC endpoints (`Login` and `Logout`) and
includes refactoring of the equivalent net/rpc endpoints to enable the
majority of logic to be reused (i.e. by extracting the `Binder` and
`TokenWriter` types).

This contains the OSS portions of the following enterprise commits:

- 75fcdbfcfa6af21d7128cb2544829ead0b1df603
- bce14b714151af74a7f0110843d640204082630a
- cc508b70fbf58eda144d9af3d71bd0f483985893
This commit is contained in:
Dan Upton 2022-05-04 17:38:45 +01:00 committed by GitHub
parent 1cd73d7a71
commit 6bfdb48560
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 3921 additions and 1101 deletions

3
.changelog/12935.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
acl: It is now possible to login and logout using the gRPC API
```

56
acl/validation.go Normal file
View File

@ -0,0 +1,56 @@
package acl
import "regexp"
const (
ServiceIdentityNameMaxLength = 256
NodeIdentityNameMaxLength = 256
)
var (
validServiceIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`)
validNodeIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`)
validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
validRoleName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,256}$`)
validAuthMethodName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
)
// IsValidServiceIdentityName returns true if the provided name can be used as
// an ACLServiceIdentity ServiceName. This is more restrictive than standard
// catalog registration, which basically takes the view that "everything is
// valid".
func IsValidServiceIdentityName(name string) bool {
if len(name) < 1 || len(name) > ServiceIdentityNameMaxLength {
return false
}
return validServiceIdentityName.MatchString(name)
}
// IsValidNodeIdentityName returns true if the provided name can be used as
// an ACLNodeIdentity NodeName. This is more restrictive than standard
// catalog registration, which basically takes the view that "everything is
// valid".
func IsValidNodeIdentityName(name string) bool {
if len(name) < 1 || len(name) > NodeIdentityNameMaxLength {
return false
}
return validNodeIdentityName.MatchString(name)
}
// IsValidPolicyName returns true if the provided name can be used as an
// ACLPolicy Name.
func IsValidPolicyName(name string) bool {
return validPolicyName.MatchString(name)
}
// IsValidRoleName returns true if the provided name can be used as an
// ACLRole Name.
func IsValidRoleName(name string) bool {
return validRoleName.MatchString(name)
}
// IsValidRoleName returns true if the provided name can be used as an
// ACLAuthMethod Name.
func IsValidAuthMethodName(name string) bool {
return validAuthMethodName.MatchString(name)
}

View File

@ -344,8 +344,6 @@ func (r *ACLResolver) Close() {
}
func (r *ACLResolver) fetchAndCacheIdentityFromToken(token string, cached *structs.IdentityCacheEntry) (structs.ACLIdentity, error) {
cacheID := tokenSecretCacheID(token)
req := structs.ACLTokenGetRequest{
Datacenter: r.backend.ACLDatacenter(),
TokenID: token,
@ -360,20 +358,20 @@ func (r *ACLResolver) fetchAndCacheIdentityFromToken(token string, cached *struc
err := r.backend.RPC("ACL.TokenRead", &req, &resp)
if err == nil {
if resp.Token == nil {
r.cache.PutIdentity(cacheID, nil, nil)
r.cache.RemoveIdentityWithSecretToken(token)
return nil, acl.ErrNotFound
} else if resp.Token.Local && r.config.Datacenter != resp.SourceDatacenter {
r.cache.PutIdentity(cacheID, nil, nil)
r.cache.RemoveIdentityWithSecretToken(token)
return nil, acl.PermissionDeniedError{Cause: fmt.Sprintf("This is a local token in datacenter %q", resp.SourceDatacenter)}
} else {
r.cache.PutIdentity(cacheID, resp.Token, nil)
r.cache.PutIdentityWithSecretToken(token, resp.Token)
return resp.Token, nil
}
}
if acl.IsErrNotFound(err) {
// Make sure to remove from the cache if it was deleted
r.cache.PutIdentity(cacheID, nil, err)
r.cache.RemoveIdentityWithSecretToken(token)
return nil, acl.ErrNotFound
}
@ -381,11 +379,11 @@ func (r *ACLResolver) fetchAndCacheIdentityFromToken(token string, cached *struc
// some other RPC error
if cached != nil && (r.config.ACLDownPolicy == "extend-cache" || r.config.ACLDownPolicy == "async-cache") {
// extend the cache
r.cache.PutIdentity(cacheID, cached.Identity, err)
r.cache.PutIdentityWithSecretToken(token, cached.Identity)
return cached.Identity, nil
}
r.cache.PutIdentity(cacheID, nil, err)
r.cache.RemoveIdentityWithSecretToken(token)
return nil, err
}
@ -399,13 +397,10 @@ func (r *ACLResolver) resolveIdentityFromToken(token string) (structs.ACLIdentit
}
// Check the cache before making any RPC requests
cacheEntry := r.cache.GetIdentity(tokenSecretCacheID(token))
cacheEntry := r.cache.GetIdentityWithSecretToken(token)
if cacheEntry != nil && cacheEntry.Age() <= r.config.ACLTokenTTL {
metrics.IncrCounter([]string{"acl", "token", "cache_hit"}, 1)
if cacheEntry.Error != nil && !acl.IsErrNotFound(cacheEntry.Error) {
return cacheEntry.Identity, ACLRemoteError{Err: cacheEntry.Error}
}
return cacheEntry.Identity, cacheEntry.Error
return cacheEntry.Identity, nil
}
metrics.IncrCounter([]string{"acl", "token", "cache_miss"}, 1)
@ -419,10 +414,7 @@ func (r *ACLResolver) resolveIdentityFromToken(token string) (structs.ACLIdentit
waitForResult := cacheEntry == nil || r.config.ACLDownPolicy != "async-cache"
if !waitForResult {
// waitForResult being false requires the cacheEntry to not be nil
if cacheEntry.Error != nil && !acl.IsErrNotFound(cacheEntry.Error) {
return cacheEntry.Identity, ACLRemoteError{Err: cacheEntry.Error}
}
return cacheEntry.Identity, cacheEntry.Error
return cacheEntry.Identity, nil
}
// block on the read here, this is why we don't need chan buffering
@ -555,7 +547,7 @@ func (r *ACLResolver) maybeHandleIdentityErrorDuringFetch(identity structs.ACLId
if acl.IsErrNotFound(err) {
// make sure to indicate that this identity is no longer valid within
// the cache
r.cache.PutIdentity(tokenSecretCacheID(identity.SecretToken()), nil, err)
r.cache.RemoveIdentityWithSecretToken(identity.SecretToken())
// Do not touch the cache. Getting a top level ACL not found error
// only indicates that the secret token used in the request
@ -566,7 +558,7 @@ func (r *ACLResolver) maybeHandleIdentityErrorDuringFetch(identity structs.ACLId
if acl.IsErrPermissionDenied(err) {
// invalidate our ID cache so that identity resolution will take place
// again in the future
r.cache.RemoveIdentity(tokenSecretCacheID(identity.SecretToken()))
r.cache.RemoveIdentityWithSecretToken(identity.SecretToken())
// Do not remove from the cache for permission denied
// what this does indicate is that our view of the token is out of date
@ -599,8 +591,8 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
var (
policyIDs = identity.PolicyIDs()
roleIDs = identity.RoleIDs()
serviceIdentities = identity.ServiceIdentityList()
nodeIdentities = identity.NodeIdentityList()
serviceIdentities = structs.ACLServiceIdentities(identity.ServiceIdentityList())
nodeIdentities = structs.ACLNodeIdentities(identity.NodeIdentityList())
)
if len(policyIDs) == 0 && len(serviceIdentities) == 0 && len(roleIDs) == 0 && len(nodeIdentities) == 0 {
@ -625,8 +617,8 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
// Now deduplicate any policies or service identities that occur more than once.
policyIDs = dedupeStringSlice(policyIDs)
serviceIdentities = dedupeServiceIdentities(serviceIdentities)
nodeIdentities = dedupeNodeIdentities(nodeIdentities)
serviceIdentities = serviceIdentities.Deduplicate()
nodeIdentities = nodeIdentities.Deduplicate()
// Generate synthetic policies for all service identities in effect.
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities, identity.EnterpriseMetadata())
@ -690,72 +682,6 @@ func (r plainACLResolver) ResolveTokenAndDefaultMeta(
return r.resolver.ResolveTokenAndDefaultMeta(token, entMeta, authzContext)
}
func dedupeServiceIdentities(in []*structs.ACLServiceIdentity) []*structs.ACLServiceIdentity {
// From: https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable
if len(in) <= 1 {
return in
}
sort.Slice(in, func(i, j int) bool {
return in[i].ServiceName < in[j].ServiceName
})
j := 0
for i := 1; i < len(in); i++ {
if in[j].ServiceName == in[i].ServiceName {
// Prefer increasing scope.
if len(in[j].Datacenters) == 0 || len(in[i].Datacenters) == 0 {
in[j].Datacenters = nil
} else {
in[j].Datacenters = mergeStringSlice(in[j].Datacenters, in[i].Datacenters)
}
continue
}
j++
in[j] = in[i]
}
// Discard the skipped items.
for i := j + 1; i < len(in); i++ {
in[i] = nil
}
return in[:j+1]
}
func dedupeNodeIdentities(in []*structs.ACLNodeIdentity) []*structs.ACLNodeIdentity {
// From: https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable
if len(in) <= 1 {
return in
}
sort.Slice(in, func(i, j int) bool {
if in[i].NodeName < in[j].NodeName {
return true
}
return in[i].Datacenter < in[j].Datacenter
})
j := 0
for i := 1; i < len(in); i++ {
if in[j].NodeName == in[i].NodeName && in[j].Datacenter == in[i].Datacenter {
continue
}
j++
in[j] = in[i]
}
// Discard the skipped items.
for i := j + 1; i < len(in); i++ {
in[i] = nil
}
return in[:j+1]
}
func mergeStringSlice(a, b []string) []string {
out := make([]string, 0, len(a)+len(b))
out = append(out, a...)

View File

@ -3,9 +3,6 @@ package consul
import (
"fmt"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/structs"
@ -38,100 +35,3 @@ func (s *Server) loadAuthMethodValidator(idx uint64, method *structs.ACLAuthMeth
return v, nil
}
type aclBindings struct {
roles []structs.ACLTokenRoleLink
serviceIdentities []*structs.ACLServiceIdentity
nodeIdentities []*structs.ACLNodeIdentity
}
// evaluateRoleBindings evaluates all current binding rules associated with the
// given auth method against the verified data returned from the authentication
// process.
//
// A list of role links and service identities are returned.
func (s *Server) evaluateRoleBindings(
validator authmethod.Validator,
verifiedIdentity *authmethod.Identity,
methodMeta *acl.EnterpriseMeta,
targetMeta *acl.EnterpriseMeta,
) (*aclBindings, error) {
// Only fetch rules that are relevant for this method.
_, rules, err := s.fsm.State().ACLBindingRuleList(nil, validator.Name(), methodMeta)
if err != nil {
return nil, err
} else if len(rules) == 0 {
return nil, nil
}
// Find all binding rules that match the provided fields.
var matchingRules []*structs.ACLBindingRule
for _, rule := range rules {
if doesSelectorMatch(rule.Selector, verifiedIdentity.SelectableFields) {
matchingRules = append(matchingRules, rule)
}
}
if len(matchingRules) == 0 {
return nil, nil
}
// For all matching rules compute the attributes of a token.
var bindings aclBindings
for _, rule := range matchingRules {
bindName, valid, err := computeBindingRuleBindName(rule.BindType, rule.BindName, verifiedIdentity.ProjectedVars)
if err != nil {
return nil, fmt.Errorf("cannot compute %q bind name for bind target: %v", rule.BindType, err)
} else if !valid {
return nil, fmt.Errorf("computed %q bind name for bind target is invalid: %q", rule.BindType, bindName)
}
switch rule.BindType {
case structs.BindingRuleBindTypeService:
bindings.serviceIdentities = append(bindings.serviceIdentities, &structs.ACLServiceIdentity{
ServiceName: bindName,
})
case structs.BindingRuleBindTypeNode:
bindings.nodeIdentities = append(bindings.nodeIdentities, &structs.ACLNodeIdentity{
NodeName: bindName,
Datacenter: s.config.Datacenter,
})
case structs.BindingRuleBindTypeRole:
_, role, err := s.fsm.State().ACLRoleGetByName(nil, bindName, targetMeta)
if err != nil {
return nil, err
}
if role != nil {
bindings.roles = append(bindings.roles, structs.ACLTokenRoleLink{
ID: role.ID,
})
}
default:
// skip unknown bind type; don't grant privileges
}
}
return &bindings, nil
}
// 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(selector, nil, selectableVars)
if err != nil {
return false // fails to match if selector is invalid
}
result, err := eval.Evaluate(selectableVars)
if err != nil {
return false // fails to match if evaluation fails
}
return result
}

View File

@ -1,46 +0,0 @@
package consul
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDoesSelectorMatch(t *testing.T) {
type matchable struct {
A string `bexpr:"a"`
C string `bexpr:"c"`
}
for _, test := range []struct {
name string
selector string
details interface{}
ok bool
}{
{"no fields",
"a==b", nil, false},
{"1 term ok",
"a==b", &matchable{A: "b"}, true},
{"1 term no field",
"a==b", &matchable{C: "d"}, false},
{"1 term wrong value",
"a==b", &matchable{A: "z"}, false},
{"2 terms ok",
"a==b and c==d", &matchable{A: "b", C: "d"}, true},
{"2 terms one missing field",
"a==b and c==d", &matchable{A: "b"}, false},
{"2 terms one wrong value",
"a==b and c==d", &matchable{A: "z", C: "d"}, false},
///////////////////////////////
{"no fields (no selectors)",
"", nil, true},
{"1 term ok (no selectors)",
"", &matchable{A: "b"}, true},
} {
t.Run(test.name, func(t *testing.T) {
ok := doesSelectorMatch(test.selector, test.details)
require.Equal(t, test.ok, ok)
})
}
}

View File

@ -2,13 +2,11 @@ package consul
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"time"
"github.com/armon/go-metrics"
@ -19,11 +17,11 @@ import (
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/auth"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/template"
)
const (
@ -99,17 +97,6 @@ var ACLEndpointSummaries = []prometheus.SummaryDefinition{
},
}
// Regex for matching
var (
validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
validServiceIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`)
serviceIdentityNameMaxLength = 256
validNodeIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`)
nodeIdentityNameMaxLength = 256
validRoleName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,256}$`)
validAuthMethod = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
)
// ACL endpoint is used to manipulate ACLs
type ACL struct {
srv *Server
@ -472,9 +459,7 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
return fmt.Errorf("Cannot clone a legacy ACL with this endpoint")
}
cloneReq := structs.ACLTokenSetRequest{
Datacenter: args.Datacenter,
ACLToken: structs.ACLToken{
clone := &structs.ACLToken{
Policies: token.Policies,
Roles: token.Roles,
ServiceIdentities: token.ServiceIdentities,
@ -483,15 +468,17 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
Description: token.Description,
ExpirationTime: token.ExpirationTime,
EnterpriseMeta: args.ACLToken.EnterpriseMeta,
},
WriteRequest: args.WriteRequest,
}
if args.ACLToken.Description != "" {
cloneReq.ACLToken.Description = args.ACLToken.Description
clone.Description = args.ACLToken.Description
}
return a.tokenSetInternal(&cloneReq, reply, false)
updated, err := a.srv.aclTokenWriter().Create(clone, false)
if err == nil {
*reply = *updated
}
return err
}
func (a *ACL) TokenSet(args *structs.ACLTokenSetRequest, reply *structs.ACLToken) error {
@ -524,382 +511,21 @@ func (a *ACL) TokenSet(args *structs.ACLTokenSetRequest, reply *structs.ACLToken
return err
}
return a.tokenSetInternal(args, reply, false)
}
func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.ACLToken, fromLogin bool) error {
token := &args.ACLToken
if !a.srv.LocalTokensEnabled() {
// local token operations
return fmt.Errorf("Cannot upsert tokens within this datacenter")
} else if !a.srv.InPrimaryDatacenter() && !token.Local {
return fmt.Errorf("Cannot upsert global tokens within this datacenter")
var (
updated *structs.ACLToken
err error
)
writer := a.srv.aclTokenWriter()
if args.ACLToken.AccessorID == "" || args.Create {
updated, err = writer.Create(&args.ACLToken, false)
} else {
updated, err = writer.Update(&args.ACLToken)
}
state := a.srv.fsm.State()
var accessorMatch *structs.ACLToken
var secretMatch *structs.ACLToken
var err error
if token.AccessorID != "" {
_, accessorMatch, err = state.ACLTokenGetByAccessor(nil, token.AccessorID, nil)
if err != nil {
return fmt.Errorf("Failed acl token lookup by accessor: %v", err)
if err == nil {
*reply = *updated
}
}
if token.SecretID != "" {
_, secretMatch, err = state.ACLTokenGetBySecret(nil, token.SecretID, nil)
if err != nil {
return fmt.Errorf("Failed acl token lookup by secret: %v", err)
}
}
if token.AccessorID == "" || args.Create {
// Token Create
// Generate the AccessorID if not specified
if token.AccessorID == "" {
token.AccessorID, err = lib.GenerateUUID(a.srv.checkTokenUUID)
if err != nil {
return err
}
} else if _, err := uuid.ParseUUID(token.AccessorID); err != nil {
return fmt.Errorf("Invalid Token: AccessorID is not a valid UUID")
} else if accessorMatch != nil {
return fmt.Errorf("Invalid Token: AccessorID is already in use")
} else if _, match, err := state.ACLTokenGetBySecret(nil, token.AccessorID, nil); err != nil || match != nil {
if err != nil {
return fmt.Errorf("Failed to lookup the acl token: %v", err)
}
return fmt.Errorf("Invalid Token: AccessorID is already in use")
} else if structs.ACLIDReserved(token.AccessorID) {
return fmt.Errorf("Invalid Token: UUIDs with the prefix %q are reserved", structs.ACLReservedPrefix)
}
// Generate the SecretID if not specified
if token.SecretID == "" {
token.SecretID, err = lib.GenerateUUID(a.srv.checkTokenUUID)
if err != nil {
return err
}
} else if _, err := uuid.ParseUUID(token.SecretID); err != nil {
return fmt.Errorf("Invalid Token: SecretID is not a valid UUID")
} else if secretMatch != nil {
return fmt.Errorf("Invalid Token: SecretID is already in use")
} else if _, match, err := state.ACLTokenGetByAccessor(nil, token.SecretID, nil); err != nil || match != nil {
if err != nil {
return fmt.Errorf("Failed to lookup the acl token: %v", err)
}
return fmt.Errorf("Invalid Token: SecretID is already in use")
} else if structs.ACLIDReserved(token.SecretID) {
return fmt.Errorf("Invalid Token: UUIDs with the prefix %q are reserved", structs.ACLReservedPrefix)
}
token.CreateTime = time.Now()
if fromLogin {
if token.AuthMethod == "" {
return fmt.Errorf("AuthMethod field is required during Login")
}
} else {
if token.AuthMethod != "" {
return fmt.Errorf("AuthMethod field is disallowed outside of Login")
}
}
// Ensure an ExpirationTTL is valid if provided.
if token.ExpirationTTL != 0 {
if token.ExpirationTTL < 0 {
return fmt.Errorf("Token Expiration TTL '%s' should be > 0", token.ExpirationTTL)
}
if token.HasExpirationTime() {
return fmt.Errorf("Token Expiration TTL and Expiration Time cannot both be set")
}
token.ExpirationTime = timePointer(token.CreateTime.Add(token.ExpirationTTL))
token.ExpirationTTL = 0
}
if token.HasExpirationTime() {
if token.CreateTime.After(*token.ExpirationTime) {
return fmt.Errorf("ExpirationTime cannot be before CreateTime")
}
expiresIn := token.ExpirationTime.Sub(token.CreateTime)
if expiresIn > a.srv.config.ACLTokenMaxExpirationTTL {
return fmt.Errorf("ExpirationTime cannot be more than %s in the future (was %s)",
a.srv.config.ACLTokenMaxExpirationTTL, expiresIn)
} else if expiresIn < a.srv.config.ACLTokenMinExpirationTTL {
return fmt.Errorf("ExpirationTime cannot be less than %s in the future (was %s)",
a.srv.config.ACLTokenMinExpirationTTL, expiresIn)
}
}
} else {
// Token Update
if _, err := uuid.ParseUUID(token.AccessorID); err != nil {
return fmt.Errorf("AccessorID is not a valid UUID")
}
// DEPRECATED (ACL-Legacy-Compat) - maybe get rid of this in the future
// and instead do a ParseUUID check. New tokens will not have
// secrets generated by users but rather they will always be UUIDs.
// However if users just continue the upgrade cycle they may still
// have tokens using secrets that are not UUIDS
// The RootAuthorizer checks that the SecretID is not "allow", "deny"
// or "manage" as a precaution against something accidentally using
// one of these root policies by setting the secret to it.
if acl.RootAuthorizer(token.SecretID) != nil {
return acl.PermissionDeniedError{Cause: "Cannot modify root ACL"}
}
// Verify the token exists
if accessorMatch == nil || accessorMatch.IsExpired(time.Now()) {
return fmt.Errorf("Cannot find token %q", token.AccessorID)
}
if token.SecretID == "" {
token.SecretID = accessorMatch.SecretID
} else if accessorMatch.SecretID != token.SecretID {
return fmt.Errorf("Changing a tokens SecretID is not permitted")
}
// Cannot toggle the "Global" mode
if token.Local != accessorMatch.Local {
return fmt.Errorf("cannot toggle local mode of %s", token.AccessorID)
}
if token.AuthMethod == "" {
token.AuthMethod = accessorMatch.AuthMethod
} else if token.AuthMethod != accessorMatch.AuthMethod {
return fmt.Errorf("Cannot change AuthMethod of %s", token.AccessorID)
}
if token.ExpirationTTL != 0 {
return fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
}
if !token.HasExpirationTime() {
token.ExpirationTime = accessorMatch.ExpirationTime
} else if !accessorMatch.HasExpirationTime() {
return fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
} else if !token.ExpirationTime.Equal(*accessorMatch.ExpirationTime) {
return fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
}
token.CreateTime = accessorMatch.CreateTime
}
policyIDs := make(map[string]struct{})
var policies []structs.ACLTokenPolicyLink
// Validate all the policy names and convert them to policy IDs
for _, link := range token.Policies {
if link.ID == "" {
_, policy, err := state.ACLPolicyGetByName(nil, link.Name, &token.EnterpriseMeta)
if err != nil {
return fmt.Errorf("Error looking up policy for name %q: %v", link.Name, err)
}
if policy == nil {
return fmt.Errorf("No such ACL policy with name %q", link.Name)
}
link.ID = policy.ID
} else {
_, policy, err := state.ACLPolicyGetByID(nil, link.ID, &token.EnterpriseMeta)
if err != nil {
return fmt.Errorf("Error looking up policy for id %q: %v", link.ID, err)
}
if policy == nil {
return fmt.Errorf("No such ACL policy with ID %q", link.ID)
}
}
// Do not store the policy name within raft/memdb as the policy could be renamed in the future.
link.Name = ""
// dedup policy links by id
if _, ok := policyIDs[link.ID]; !ok {
policies = append(policies, link)
policyIDs[link.ID] = struct{}{}
}
}
token.Policies = policies
roleIDs := make(map[string]struct{})
var roles []structs.ACLTokenRoleLink
// Validate all the role names and convert them to role IDs.
for _, link := range token.Roles {
if link.ID == "" {
_, role, err := state.ACLRoleGetByName(nil, link.Name, &token.EnterpriseMeta)
if err != nil {
return fmt.Errorf("Error looking up role for name %q: %v", link.Name, err)
}
if role == nil {
return fmt.Errorf("No such ACL role with name %q", link.Name)
}
link.ID = role.ID
} else {
_, role, err := state.ACLRoleGetByID(nil, link.ID, &token.EnterpriseMeta)
if err != nil {
return fmt.Errorf("Error looking up role for id %q: %v", link.ID, err)
}
if role == nil {
return fmt.Errorf("No such ACL role with ID %q", link.ID)
}
}
// Do not store the role name within raft/memdb as the role could be renamed in the future.
link.Name = ""
// dedup role links by id
if _, ok := roleIDs[link.ID]; !ok {
roles = append(roles, link)
roleIDs[link.ID] = struct{}{}
}
}
token.Roles = roles
for _, svcid := range token.ServiceIdentities {
if svcid.ServiceName == "" {
return fmt.Errorf("Service identity is missing the service name field on this token")
}
if token.Local && len(svcid.Datacenters) > 0 {
return fmt.Errorf("Service identity %q cannot specify a list of datacenters on a local token", svcid.ServiceName)
}
if !isValidServiceIdentityName(svcid.ServiceName) {
return fmt.Errorf("Service identity %q has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed", svcid.ServiceName)
}
}
token.ServiceIdentities = dedupeServiceIdentities(token.ServiceIdentities)
for _, nodeid := range token.NodeIdentities {
if nodeid.NodeName == "" {
return fmt.Errorf("Node identity is missing the node name field on this token")
}
if nodeid.Datacenter == "" {
return fmt.Errorf("Node identity is missing the datacenter field on this token")
}
if !isValidNodeIdentityName(nodeid.NodeName) {
return fmt.Errorf("Node identity has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed")
}
}
token.NodeIdentities = dedupeNodeIdentities(token.NodeIdentities)
if token.Rules != "" {
return fmt.Errorf("Rules cannot be specified for this token")
}
if token.Type != "" {
return fmt.Errorf("Type cannot be specified for this token")
}
token.SetHash(true)
// validate the enterprise specific fields
if err = a.tokenUpsertValidateEnterprise(token, accessorMatch); err != nil {
return err
}
req := &structs.ACLTokenBatchSetRequest{
Tokens: structs.ACLTokens{token},
CAS: false,
}
if fromLogin {
// Logins may attempt to link to roles that do not exist. These
// may be persisted, but don't allow tokens to be created that
// have no privileges (i.e. role links that point nowhere).
req.AllowMissingLinks = true
req.ProhibitUnprivileged = true
}
_, err = a.srv.raftApply(structs.ACLTokenSetRequestType, req)
if err != nil {
return fmt.Errorf("Failed to apply token write request: %v", err)
}
// Purge the identity from the cache to prevent using the previous definition of the identity
a.srv.ACLResolver.cache.RemoveIdentity(tokenSecretCacheID(token.SecretID))
// Don't check expiration times here as it doesn't really matter.
if _, updatedToken, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, token.AccessorID, nil); err == nil && updatedToken != nil {
*reply = *updatedToken
} else {
return fmt.Errorf("Failed to retrieve the token after insertion")
}
return nil
}
func validateBindingRuleBindName(bindType, bindName string, availableFields []string) (bool, error) {
if bindType == "" || bindName == "" {
return false, nil
}
fakeVarMap := make(map[string]string)
for _, v := range availableFields {
fakeVarMap[v] = "fake"
}
_, valid, err := computeBindingRuleBindName(bindType, bindName, fakeVarMap)
if err != nil {
return false, err
}
return valid, nil
}
// computeBindingRuleBindName processes the HIL for the provided bind type+name
// 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, projectedVars map[string]string) (string, bool, error) {
bindName, err := template.InterpolateHIL(bindName, projectedVars, true)
if err != nil {
return "", false, err
}
valid := false
switch bindType {
case structs.BindingRuleBindTypeService:
valid = isValidServiceIdentityName(bindName)
case structs.BindingRuleBindTypeNode:
valid = isValidNodeIdentityName(bindName)
case structs.BindingRuleBindTypeRole:
valid = validRoleName.MatchString(bindName)
default:
return "", false, fmt.Errorf("unknown binding rule bind type: %s", bindType)
}
return bindName, valid, nil
}
// isValidServiceIdentityName returns true if the provided name can be used as
// an ACLServiceIdentity ServiceName. This is more restrictive than standard
// catalog registration, which basically takes the view that "everything is
// valid".
func isValidServiceIdentityName(name string) bool {
if len(name) < 1 || len(name) > serviceIdentityNameMaxLength {
return false
}
return validServiceIdentityName.MatchString(name)
}
// isValidNodeIdentityName returns true if the provided name can be used as
// an ACLNodeIdentity NodeName. This is more restrictive than standard
// catalog registration, which basically takes the view that "everything is
// valid".
func isValidNodeIdentityName(name string) bool {
if len(name) < 1 || len(name) > nodeIdentityNameMaxLength {
return false
}
return validNodeIdentityName.MatchString(name)
}
func (a *ACL) TokenDelete(args *structs.ACLTokenDeleteRequest, reply *string) error {
@ -974,7 +600,7 @@ func (a *ACL) TokenDelete(args *structs.ACLTokenDeleteRequest, reply *string) er
}
// Purge the identity from the cache to prevent using the previous definition of the identity
a.srv.ACLResolver.cache.RemoveIdentity(tokenSecretCacheID(token.SecretID))
a.srv.ACLResolver.cache.RemoveIdentityWithSecretToken(token.SecretID)
if reply != nil {
*reply = token.AccessorID
@ -1218,7 +844,7 @@ func (a *ACL) PolicySet(args *structs.ACLPolicySetRequest, reply *structs.ACLPol
return fmt.Errorf("Invalid Policy: no Name is set")
}
if !validPolicyName.MatchString(policy.Name) {
if !acl.IsValidPolicyName(policy.Name) {
return fmt.Errorf("Invalid Policy: invalid Name. Only alphanumeric characters, '-' and '_' are allowed")
}
@ -1604,7 +1230,7 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
return fmt.Errorf("Invalid Role: no Name is set")
}
if !validRoleName.MatchString(role.Name) {
if !acl.IsValidRoleName(role.Name) {
return fmt.Errorf("Invalid Role: invalid Name. Only alphanumeric characters, '-' and '_' are allowed")
}
@ -1681,11 +1307,11 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
if svcid.ServiceName == "" {
return fmt.Errorf("Service identity is missing the service name field on this role")
}
if !isValidServiceIdentityName(svcid.ServiceName) {
if !acl.IsValidServiceIdentityName(svcid.ServiceName) {
return fmt.Errorf("Service identity %q has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed", svcid.ServiceName)
}
}
role.ServiceIdentities = dedupeServiceIdentities(role.ServiceIdentities)
role.ServiceIdentities = role.ServiceIdentities.Deduplicate()
for _, nodeid := range role.NodeIdentities {
if nodeid.NodeName == "" {
@ -1694,11 +1320,11 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
if nodeid.Datacenter == "" {
return fmt.Errorf("Node identity is missing the datacenter field on this role")
}
if !isValidNodeIdentityName(nodeid.NodeName) {
if !acl.IsValidNodeIdentityName(nodeid.NodeName) {
return fmt.Errorf("Node identity has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed")
}
}
role.NodeIdentities = dedupeNodeIdentities(role.NodeIdentities)
role.NodeIdentities = role.NodeIdentities.Deduplicate()
// calculate the hash for this role
role.SetHash(true)
@ -2018,7 +1644,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, blankID.ProjectedVarNames()); err != nil {
if valid, err := auth.IsValidBindName(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")
@ -2167,7 +1793,7 @@ func (a *ACL) AuthMethodRead(args *structs.ACLAuthMethodGetRequest, reply *struc
return errNotFound
}
_ = a.enterpriseAuthMethodTypeValidation(method.Type)
_ = a.srv.enterpriseAuthMethodTypeValidation(method.Type)
return nil
})
}
@ -2207,11 +1833,11 @@ func (a *ACL) AuthMethodSet(args *structs.ACLAuthMethodSetRequest, reply *struct
if method.Name == "" {
return fmt.Errorf("Invalid Auth Method: no Name is set")
}
if !validAuthMethod.MatchString(method.Name) {
if !acl.IsValidAuthMethodName(method.Name) {
return fmt.Errorf("Invalid Auth Method: invalid Name. Only alphanumeric characters, '-' and '_' are allowed")
}
if err := a.enterpriseAuthMethodTypeValidation(method.Type); err != nil {
if err := a.srv.enterpriseAuthMethodTypeValidation(method.Type); err != nil {
return err
}
@ -2321,7 +1947,7 @@ func (a *ACL) AuthMethodDelete(args *structs.ACLAuthMethodDeleteRequest, reply *
return nil
}
if err := a.enterpriseAuthMethodTypeValidation(method.Type); err != nil {
if err := a.srv.enterpriseAuthMethodTypeValidation(method.Type); err != nil {
return err
}
@ -2377,7 +2003,7 @@ func (a *ACL) AuthMethodList(args *structs.ACLAuthMethodListRequest, reply *stru
var stubs structs.ACLAuthMethodListStubs
for _, method := range methods {
_ = a.enterpriseAuthMethodTypeValidation(method.Type)
_ = a.srv.enterpriseAuthMethodTypeValidation(method.Type)
stubs = append(stubs, method.Stub())
}
@ -2413,130 +2039,26 @@ func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLToken) erro
defer metrics.MeasureSince([]string{"acl", "login"}, time.Now())
auth := args.Auth
// 1. take args.Data.AuthMethod to get an AuthMethod Validator
idx, method, err := a.srv.fsm.State().ACLAuthMethodGetByName(nil, auth.AuthMethod, &auth.EnterpriseMeta)
if err != nil {
return err
} else if method == nil {
return fmt.Errorf("%w: auth method %q not found", acl.ErrNotFound, auth.AuthMethod)
}
if err := a.enterpriseAuthMethodTypeValidation(method.Type); err != nil {
return err
}
validator, err := a.srv.loadAuthMethodValidator(idx, method)
authMethod, validator, err := a.srv.loadAuthMethod(args.Auth.AuthMethod, &args.Auth.EnterpriseMeta)
if err != nil {
return err
}
// 2. Send args.Data.BearerToken to method validator and get back a fields map
verifiedIdentity, err := validator.ValidateLogin(context.Background(), auth.BearerToken)
verifiedIdentity, err := validator.ValidateLogin(context.Background(), args.Auth.BearerToken)
if err != nil {
return err
}
return a.tokenSetFromAuthMethod(
method,
&auth.EnterpriseMeta,
"token created via login",
auth.Meta,
validator,
verifiedIdentity,
&structs.ACLTokenSetRequest{
Datacenter: args.Datacenter,
WriteRequest: args.WriteRequest,
},
reply,
)
}
func (a *ACL) tokenSetFromAuthMethod(
method *structs.ACLAuthMethod,
entMeta *acl.EnterpriseMeta,
tokenDescriptionPrefix string,
tokenMetadata map[string]string,
validator authmethod.Validator,
verifiedIdentity *authmethod.Identity,
createReq *structs.ACLTokenSetRequest, // this should be prepopulated with datacenter+writerequest
reply *structs.ACLToken,
) error {
// This always will return a valid pointer
targetMeta, err := computeTargetEnterpriseMeta(method, verifiedIdentity)
description, err := auth.BuildTokenDescription("token created via login", args.Auth.Meta)
if err != nil {
return err
}
// 3. send map through role bindings
bindings, err := a.srv.evaluateRoleBindings(validator, verifiedIdentity, entMeta, targetMeta)
if err != nil {
token, err := a.srv.aclLogin().TokenForVerifiedIdentity(verifiedIdentity, authMethod, description)
if err == nil {
*reply = *token
}
return err
}
// We try to prevent the creation of a useless token without taking a trip
// through the state store if we can.
if bindings == nil || (len(bindings.serviceIdentities) == 0 && len(bindings.nodeIdentities) == 0 && len(bindings.roles) == 0) {
return acl.ErrPermissionDenied
}
// TODO(sso): add a CapturedField to ACLAuthMethod that would pluck fields from the returned identity and stuff into `auth.Meta`.
description := tokenDescriptionPrefix
loginMeta, err := encodeLoginMeta(tokenMetadata)
if err != nil {
return err
}
if loginMeta != "" {
description += ": " + loginMeta
}
// 4. create token
createReq.ACLToken = structs.ACLToken{
Description: description,
Local: true,
AuthMethod: method.Name,
ServiceIdentities: bindings.serviceIdentities,
NodeIdentities: bindings.nodeIdentities,
Roles: bindings.roles,
ExpirationTTL: method.MaxTokenTTL,
EnterpriseMeta: *targetMeta,
}
if method.TokenLocality == "global" {
if !a.srv.InPrimaryDatacenter() {
return errors.New("creating global tokens via auth methods is only permitted in the primary datacenter")
}
createReq.ACLToken.Local = false
}
createReq.ACLToken.ACLAuthMethodEnterpriseMeta.FillWithEnterpriseMeta(entMeta)
// 5. return token information like a TokenCreate would
err = a.tokenSetInternal(createReq, reply, true)
// If we were in a slight race with a role delete operation then we may
// still end up failing to insert an unprivileged token in the state
// machine instead. Return the same error as earlier so it doesn't
// actually matter which one prevents the insertion.
if err != nil && err.Error() == state.ErrTokenHasNoPrivileges.Error() {
return acl.ErrPermissionDenied
}
return err
}
func encodeLoginMeta(meta map[string]string) (string, error) {
if len(meta) == 0 {
return "", nil
}
d, err := json.Marshal(meta)
if err != nil {
return "", err
}
return string(d), nil
}
func (a *ACL) Logout(args *structs.ACLLogoutRequest, reply *bool) error {
@ -2558,39 +2080,18 @@ func (a *ACL) Logout(args *structs.ACLLogoutRequest, reply *bool) error {
defer metrics.MeasureSince([]string{"acl", "logout"}, time.Now())
_, token, err := a.srv.fsm.State().ACLTokenGetBySecret(nil, args.Token, nil)
if err != nil {
return err
} else if token == nil {
return acl.ErrNotFound
} else if token.AuthMethod == "" {
// Can't "logout" of a token that wasn't a result of login.
return acl.ErrPermissionDenied
} else if !a.srv.InPrimaryDatacenter() && !token.Local {
// global token writes must be forwarded to the primary DC
// No need to check expiration time because it's being deleted.
err := a.srv.aclTokenWriter().Delete(args.Token, true)
switch {
case errors.Is(err, auth.ErrCannotWriteGlobalToken):
// Writes to global tokens must be forwarded to the primary DC.
args.Datacenter = a.srv.config.PrimaryDatacenter
return a.srv.forwardDC("ACL.Logout", a.srv.config.PrimaryDatacenter, args, reply)
case err != nil:
return err
}
// No need to check expiration time because it's being deleted.
req := &structs.ACLTokenBatchDeleteRequest{
TokenIDs: []string{token.AccessorID},
}
_, err = a.srv.raftApply(structs.ACLTokenDeleteRequestType, req)
if err != nil {
return fmt.Errorf("Failed to apply token delete request: %v", err)
}
// Purge the identity from the cache to prevent using the previous definition of the identity
a.srv.ACLResolver.cache.RemoveIdentity(tokenSecretCacheID(token.SecretID))
*reply = true
return nil
}

View File

@ -27,21 +27,10 @@ func (a *ACL) roleUpsertValidateEnterprise(role *structs.ACLRole, existing *stru
return state.ACLRoleUpsertValidateEnterprise(role, existing)
}
func (a *ACL) enterpriseAuthMethodTypeValidation(authMethodType string) error {
return nil
}
func enterpriseAuthMethodValidation(method *structs.ACLAuthMethod, validator authmethod.Validator) error {
return nil
}
func computeTargetEnterpriseMeta(
method *structs.ACLAuthMethod,
verifiedIdentity *authmethod.Identity,
) (*acl.EnterpriseMeta, error) {
return &acl.EnterpriseMeta{}, nil
}
func getTokenNamespaceDefaults(ws memdb.WatchSet, state *state.Store, entMeta *acl.EnterpriseMeta) ([]string, []string, error) {
return nil, nil, nil
}

View File

@ -478,7 +478,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
}, false)
waitForLeaderEstablishment(t, srv)
acl := ACL{srv: srv}
a := ACL{srv: srv}
var tokenID string
@ -501,7 +501,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NoError(t, err)
// Get the token directly to validate that it exists
@ -532,7 +532,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NoError(t, err)
// Get the token directly to validate that it exists
@ -572,7 +572,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err = acl.TokenSet(&req, &resp)
err = a.TokenSet(&req, &resp)
require.NoError(t, err)
// Delete both policies to ensure that we skip resolving ID->Name
@ -618,7 +618,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err = acl.TokenSet(&req, &resp)
err = a.TokenSet(&req, &resp)
require.NoError(t, err)
// Delete both roles to ensure that we skip resolving ID->Name
@ -651,8 +651,8 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "AuthMethod field is disallowed outside of Login")
err := a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "AuthMethod field is disallowed outside of login")
})
t.Run("Update auth method linked token and try to change auth method", func(t *testing.T) {
@ -767,12 +767,12 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Service identity is missing the service name field")
})
t.Run("Create it with invalid service identity (too large)", func(t *testing.T) {
long := strings.Repeat("x", serviceIdentityNameMaxLength+1)
long := strings.Repeat("x", acl.ServiceIdentityNameMaxLength+1)
req := structs.ACLTokenSetRequest{
Datacenter: "dc1",
ACLToken: structs.ACLToken{
@ -788,7 +788,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NotNil(t, err)
})
@ -834,7 +834,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
if test.ok {
require.NoError(t, err)
@ -867,7 +867,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NoError(t, err)
// Get the token directly to validate that it exists
@ -901,7 +901,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NoError(t, err)
// Get the token directly to validate that it exists
@ -931,7 +931,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "cannot specify a list of datacenters on a local token")
})
@ -959,7 +959,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
if test.errString != "" {
testutil.RequireErrorContains(t, err, test.errString)
} else {
@ -981,7 +981,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
if test.errString != "" {
testutil.RequireErrorContains(t, err, test.errStringTTL)
} else {
@ -1005,7 +1005,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Expiration TTL and Expiration Time cannot both be set")
})
@ -1023,7 +1023,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NoError(t, err)
// Get the token directly to validate that it exists
@ -1058,7 +1058,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NoError(t, err)
// Get the token directly to validate that it exists
@ -1090,7 +1090,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Cannot change expiration time")
})
@ -1108,7 +1108,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NoError(t, err)
// Get the token directly to validate that it exists
@ -1136,7 +1136,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
require.NoError(t, err)
// Get the token directly to validate that it exists
@ -1172,7 +1172,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err = acl.TokenSet(&req, &resp)
err = a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Cannot find token")
})
@ -1191,7 +1191,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Node identity is missing the node name field on this token")
})
@ -1211,7 +1211,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Node identity has an invalid name.")
})
t.Run("invalid node identity - no datacenter", func(t *testing.T) {
@ -1229,7 +1229,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
resp := structs.ACLToken{}
err := acl.TokenSet(&req, &resp)
err := a.TokenSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Node identity is missing the datacenter field on this token")
})
}
@ -2389,7 +2389,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
_, srv, codec := testACLServerWithConfig(t, nil, false)
waitForLeaderEstablishment(t, srv)
acl := ACL{srv: srv}
a := ACL{srv: srv}
var roleID string
testPolicy1, err := upsertTestPolicy(codec, TestDefaultInitialManagementToken, "dc1")
@ -2419,7 +2419,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
}
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
require.NoError(t, err)
require.NotNil(t, resp.ID)
@ -2457,7 +2457,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
}
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
require.NoError(t, err)
require.NotNil(t, resp.ID)
@ -2498,7 +2498,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
}
resp := structs.ACLRole{}
err = acl.RoleSet(&req, &resp)
err = a.RoleSet(&req, &resp)
require.NoError(t, err)
require.NotNil(t, resp.ID)
@ -2540,12 +2540,12 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
}
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Service identity is missing the service name field")
})
t.Run("Create it with invalid service identity (too large)", func(t *testing.T) {
long := strings.Repeat("x", serviceIdentityNameMaxLength+1)
long := strings.Repeat("x", acl.ServiceIdentityNameMaxLength+1)
req := structs.ACLRoleSetRequest{
Datacenter: "dc1",
Role: structs.ACLRole{
@ -2559,7 +2559,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
}
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
require.NotNil(t, err)
})
@ -2604,7 +2604,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
if test.ok {
require.NoError(t, err)
@ -2635,7 +2635,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
require.NoError(t, err)
// Get the role directly to validate that it exists
@ -2667,7 +2667,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
require.NoError(t, err)
// Get the role directly to validate that it exists
@ -2696,7 +2696,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Node identity is missing the node name field on this role")
})
@ -2717,7 +2717,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Node identity has an invalid name.")
})
t.Run("invalid node identity - no datacenter", func(t *testing.T) {
@ -2736,7 +2736,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
resp := structs.ACLRole{}
err := acl.RoleSet(&req, &resp)
err := a.RoleSet(&req, &resp)
testutil.RequireErrorContains(t, err, "Node identity is missing the datacenter field on this role")
})
}
@ -5314,106 +5314,6 @@ func gatherIDs(t *testing.T, v interface{}) []string {
return out
}
func TestValidateBindingRuleBindName(t *testing.T) {
t.Parallel()
type testcase struct {
name string
bindType string
bindName string
fields string
valid bool // valid HIL, invalid contents
err bool // invalid HIL
}
for _, test := range []testcase{
{"no bind type",
"", "", "", false, false},
{"bad bind type",
"invalid", "blah", "", false, true},
// valid HIL, invalid name
{"empty",
"both", "", "", false, false},
{"just end",
"both", "}", "", false, false},
{"var without start",
"both", " item }", "item", false, false},
{"two vars missing second start",
"both", "before-${ item }after--more }", "item,more", false, false},
// names for the two types are validated differently
{"@ is disallowed",
"both", "bad@name", "", false, false},
{"leading dash",
"role", "-name", "", true, false},
{"leading dash",
"service", "-name", "", false, false},
{"trailing dash",
"role", "name-", "", true, false},
{"trailing dash",
"service", "name-", "", false, false},
{"inner dash",
"both", "name-end", "", true, false},
{"upper case",
"role", "NAME", "", true, false},
{"upper case",
"service", "NAME", "", false, false},
// valid HIL, valid name
{"no vars",
"both", "nothing", "", true, false},
{"just var",
"both", "${item}", "item", true, false},
{"var in middle",
"both", "before-${item}after", "item", true, false},
{"two vars",
"both", "before-${item}after-${more}", "item,more", true, false},
// bad
{"no bind name",
"both", "", "", false, false},
{"just start",
"both", "${", "", false, true},
{"backwards",
"both", "}${", "", false, true},
{"no varname",
"both", "${}", "", false, true},
{"missing map key",
"both", "${item}", "", false, true},
{"var without end",
"both", "${ item ", "item", false, true},
{"two vars missing first end",
"both", "before-${ item after-${ more }", "item,more", false, true},
} {
var cases []testcase
if test.bindType == "both" {
test1 := test
test1.bindType = "role"
test2 := test
test2.bindType = "service"
cases = []testcase{test1, test2}
} else {
cases = []testcase{test}
}
for _, test := range cases {
test := test
t.Run(test.bindType+"--"+test.name, func(t *testing.T) {
t.Parallel()
valid, err := validateBindingRuleBindName(
test.bindType,
test.bindName,
strings.Split(test.fields, ","),
)
if test.err {
require.NotNil(t, err)
require.False(t, valid)
} else {
require.NoError(t, err)
require.Equal(t, test.valid, valid)
}
})
}
}
}
// upsertTestToken creates a token for testing purposes
func upsertTestTokenInEntMeta(codec rpc.ClientCodec, initialManagementToken string, datacenter string,
tokenModificationFn func(token *structs.ACLToken), entMeta *acl.EnterpriseMeta) (*structs.ACLToken, error) {

View File

@ -1,9 +1,12 @@
package consul
import (
"fmt"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/auth"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/structs"
)
@ -172,3 +175,44 @@ func (s *Server) filterACL(token string, subj interface{}) error {
func (s *Server) filterACLWithAuthorizer(authorizer acl.Authorizer, subj interface{}) {
filterACLWithAuthorizer(s.ACLResolver.logger, authorizer, subj)
}
func (s *Server) aclLogin() *auth.Login {
return auth.NewLogin(s.aclBinder(), s.aclTokenWriter())
}
func (s *Server) aclBinder() *auth.Binder {
return auth.NewBinder(s.fsm.State(), s.config.Datacenter)
}
func (s *Server) aclTokenWriter() *auth.TokenWriter {
return auth.NewTokenWriter(auth.TokenWriterConfig{
RaftApply: s.raftApply,
ACLCache: s.ACLResolver.cache,
Store: s.fsm.State(),
CheckUUID: s.checkTokenUUID,
MaxExpirationTTL: s.config.ACLTokenMaxExpirationTTL,
MinExpirationTTL: s.config.ACLTokenMinExpirationTTL,
PrimaryDatacenter: s.config.PrimaryDatacenter,
InPrimaryDatacenter: s.InPrimaryDatacenter(),
LocalTokensEnabled: s.LocalTokensEnabled(),
})
}
func (s *Server) loadAuthMethod(methodName string, entMeta *acl.EnterpriseMeta) (*structs.ACLAuthMethod, authmethod.Validator, error) {
idx, method, err := s.fsm.State().ACLAuthMethodGetByName(nil, methodName, entMeta)
if err != nil {
return nil, nil, err
} else if method == nil {
return nil, nil, fmt.Errorf("%w: auth method %q not found", acl.ErrNotFound, methodName)
}
if err := s.enterpriseAuthMethodTypeValidation(method.Type); err != nil {
return nil, nil, err
}
validator, err := s.loadAuthMethodValidator(idx, method)
if err != nil {
return nil, nil, err
}
return method, validator, nil
}

View File

@ -19,3 +19,7 @@ func (s *Server) validateEnterpriseToken(identity structs.ACLIdentity) error {
func (s *Server) aclBootstrapAllowed() error {
return nil
}
func (*Server) enterpriseAuthMethodTypeValidation(authMethodType string) error {
return nil
}

View File

@ -768,15 +768,15 @@ func TestACLResolver_ResolveRootACL(t *testing.T) {
}
func TestACLResolver_DownPolicy(t *testing.T) {
requireIdentityCached := func(t *testing.T, r *ACLResolver, id string, present bool, msg string) {
requireIdentityCached := func(t *testing.T, r *ACLResolver, secretID string, present bool, msg string) {
t.Helper()
cacheVal := r.cache.GetIdentity(id)
require.NotNil(t, cacheVal)
cacheVal := r.cache.GetIdentityWithSecretToken(secretID)
if present {
require.NotNil(t, cacheVal, msg)
require.NotNil(t, cacheVal.Identity, msg)
} else {
require.Nil(t, cacheVal.Identity, msg)
require.Nil(t, cacheVal, msg)
}
}
requirePolicyCached := func(t *testing.T, r *ACLResolver, policyID string, present bool, msg string) {
@ -816,7 +816,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
}
require.Equal(t, expected, authz)
requireIdentityCached(t, r, tokenSecretCacheID("foo"), false, "not present")
requireIdentityCached(t, r, "foo", false, "not present")
})
t.Run("Allow", func(t *testing.T) {
@ -844,7 +844,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
}
require.Equal(t, expected, authz)
requireIdentityCached(t, r, tokenSecretCacheID("foo"), false, "not present")
requireIdentityCached(t, r, "foo", false, "not present")
})
t.Run("Expired-Policy", func(t *testing.T) {
@ -935,7 +935,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
require.NotNil(t, authz)
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
requireIdentityCached(t, r, tokenSecretCacheID("found"), true, "cached")
requireIdentityCached(t, r, "found", true, "cached")
authz2, err := r.ResolveToken("found")
require.NoError(t, err)
@ -986,7 +986,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
require.NotNil(t, authz)
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
requireIdentityCached(t, r, tokenSecretCacheID("found-role"), true, "still cached")
requireIdentityCached(t, r, "found-role", true, "still cached")
authz2, err := r.ResolveToken("found-role")
require.NoError(t, err)
@ -1245,7 +1245,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
require.NotNil(t, authz)
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
requireIdentityCached(t, r, tokenSecretCacheID("found"), true, "cached")
requireIdentityCached(t, r, "found", true, "cached")
// The identity should have been cached so this should still be valid
authz2, err := r.ResolveToken("found")
@ -1261,45 +1261,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
assert.True(t, acl.IsErrNotFound(err))
})
requireIdentityCached(t, r, tokenSecretCacheID("found"), false, "no longer cached")
})
t.Run("Cache-Error", func(t *testing.T) {
_, rawToken, _ := testIdentityForToken("found")
foundToken := rawToken.(*structs.ACLToken)
secretID := foundToken.SecretID
remoteErr := fmt.Errorf("network error")
delegate := &ACLResolverTestDelegate{
enabled: true,
datacenter: "dc1",
legacy: false,
localTokens: false,
localPolicies: false,
tokenReadFn: func(_ *structs.ACLTokenGetRequest, reply *structs.ACLTokenResponse) error {
return remoteErr
},
}
r := newTestACLResolver(t, delegate, func(config *ACLResolverConfig) {
config.Config.ACLDownPolicy = "deny"
})
// Attempt to resolve the token - this should set up the cache entry with a nil
// identity and permission denied error.
authz, err := r.ResolveToken(secretID)
require.NoError(t, err)
require.NotNil(t, authz)
entry := r.cache.GetIdentity(tokenSecretCacheID(secretID))
require.Nil(t, entry.Identity)
require.Equal(t, remoteErr, entry.Error)
// Attempt to resolve again to pull from the cache.
authz, err = r.ResolveToken(secretID)
require.NoError(t, err)
require.NotNil(t, authz)
entry = r.cache.GetIdentity(tokenSecretCacheID(secretID))
require.Nil(t, entry.Identity)
require.Equal(t, remoteErr, entry.Error)
requireIdentityCached(t, r, "found", false, "no longer cached")
})
t.Run("PolicyResolve-TokenNotFound", func(t *testing.T) {
@ -1351,7 +1313,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
// Verify that the caches are setup properly.
requireIdentityCached(t, r, tokenSecretCacheID(secretID), true, "cached")
requireIdentityCached(t, r, secretID, true, "cached")
requirePolicyCached(t, r, "node-wr", true, "cached") // from "found" token
requirePolicyCached(t, r, "dc2-key-wr", true, "cached") // from "found" token
@ -1362,7 +1324,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
_, err = r.ResolveToken(secretID)
require.True(t, acl.IsErrNotFound(err))
requireIdentityCached(t, r, tokenSecretCacheID(secretID), false, "identity not found cached")
requireIdentityCached(t, r, secretID, false, "identity not found cached")
requirePolicyCached(t, r, "node-wr", true, "still cached")
require.Nil(t, r.cache.GetPolicy("dc2-key-wr"), "not stored at all")
})
@ -1411,7 +1373,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
// Verify that the caches are setup properly.
requireIdentityCached(t, r, tokenSecretCacheID(secretID), true, "cached")
requireIdentityCached(t, r, secretID, true, "cached")
requirePolicyCached(t, r, "node-wr", true, "cached") // from "found" token
requirePolicyCached(t, r, "dc2-key-wr", true, "cached") // from "found" token
@ -1422,7 +1384,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
_, err = r.ResolveToken(secretID)
require.True(t, acl.IsErrPermissionDenied(err))
require.Nil(t, r.cache.GetIdentity(tokenSecretCacheID(secretID)), "identity not stored at all")
require.Nil(t, r.cache.GetIdentityWithSecretToken(secretID), "identity not stored at all")
requirePolicyCached(t, r, "node-wr", true, "still cached")
require.Nil(t, r.cache.GetPolicy("dc2-key-wr"), "not stored at all")
})
@ -3815,94 +3777,6 @@ func TestACL_unhandledFilterType(t *testing.T) {
srv.filterACL(token, &structs.HealthCheck{})
}
func TestDedupeServiceIdentities(t *testing.T) {
srvid := func(name string, datacenters ...string) *structs.ACLServiceIdentity {
return &structs.ACLServiceIdentity{
ServiceName: name,
Datacenters: datacenters,
}
}
tests := []struct {
name string
in []*structs.ACLServiceIdentity
expect []*structs.ACLServiceIdentity
}{
{
name: "empty",
in: nil,
expect: nil,
},
{
name: "one",
in: []*structs.ACLServiceIdentity{
srvid("foo"),
},
expect: []*structs.ACLServiceIdentity{
srvid("foo"),
},
},
{
name: "just names",
in: []*structs.ACLServiceIdentity{
srvid("fooZ"),
srvid("fooA"),
srvid("fooY"),
srvid("fooB"),
},
expect: []*structs.ACLServiceIdentity{
srvid("fooA"),
srvid("fooB"),
srvid("fooY"),
srvid("fooZ"),
},
},
{
name: "just names with dupes",
in: []*structs.ACLServiceIdentity{
srvid("fooZ"),
srvid("fooA"),
srvid("fooY"),
srvid("fooB"),
srvid("fooA"),
srvid("fooB"),
srvid("fooY"),
srvid("fooZ"),
},
expect: []*structs.ACLServiceIdentity{
srvid("fooA"),
srvid("fooB"),
srvid("fooY"),
srvid("fooZ"),
},
},
{
name: "names with dupes and datacenters",
in: []*structs.ACLServiceIdentity{
srvid("fooZ", "dc2", "dc4"),
srvid("fooA"),
srvid("fooY", "dc1"),
srvid("fooB"),
srvid("fooA", "dc9", "dc8"),
srvid("fooB"),
srvid("fooY", "dc1"),
srvid("fooZ", "dc3", "dc4"),
},
expect: []*structs.ACLServiceIdentity{
srvid("fooA"),
srvid("fooB"),
srvid("fooY", "dc1"),
srvid("fooZ", "dc2", "dc3", "dc4"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := dedupeServiceIdentities(test.in)
require.ElementsMatch(t, test.expect, got)
})
}
}
func TestACL_LocalToken(t *testing.T) {
t.Run("local token in same dc", func(t *testing.T) {
d := &ACLResolverTestDelegate{

View File

@ -108,7 +108,7 @@ func (s *Server) reapExpiredACLTokens(local, global bool) (int, error) {
// Purge the identities from the cache
for _, secretID := range secretIDs {
s.ACLResolver.cache.RemoveIdentity(tokenSecretCacheID(secretID))
s.ACLResolver.cache.RemoveIdentityWithSecretToken(secretID)
}
return len(req.TokenIDs), nil

189
agent/consul/auth/binder.go Normal file
View File

@ -0,0 +1,189 @@
package auth
import (
"fmt"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib/template"
)
// Binder is responsible for collecting the ACL roles, service identities, node
// identities, and enterprise metadata to be assigned to a token generated as a
// result of "logging in" via an auth method.
//
// It does so by applying the auth method's configured binding rules and in the
// case of enterprise, namespace rules.
type Binder struct {
store BinderStateStore
datacenter string
}
// NewBinder creates a Binder with the given state store and datacenter.
func NewBinder(store BinderStateStore, datacenter string) *Binder {
return &Binder{store, datacenter}
}
// BinderStateStore is the subset of state store methods used by the binder.
type BinderStateStore interface {
ACLBindingRuleList(ws memdb.WatchSet, methodName string, entMeta *acl.EnterpriseMeta) (uint64, structs.ACLBindingRules, error)
ACLRoleGetByName(ws memdb.WatchSet, roleName string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLRole, error)
}
// Bindings contains the ACL roles, service identities, node identities and
// enterprise meta to be assigned to the created token.
type Bindings struct {
Roles []structs.ACLTokenRoleLink
ServiceIdentities []*structs.ACLServiceIdentity
NodeIdentities []*structs.ACLNodeIdentity
EnterpriseMeta acl.EnterpriseMeta
}
// None indicates that the resulting bindings would not give the created token
// access to any resources.
func (b *Bindings) None() bool {
if b == nil {
return true
}
return len(b.ServiceIdentities) == 0 &&
len(b.NodeIdentities) == 0 &&
len(b.Roles) == 0
}
// Bind collects the ACL roles, service identities, etc. to be assigned to the
// created token.
func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, verifiedIdentity *authmethod.Identity) (*Bindings, error) {
var (
bindings Bindings
err error
)
if bindings.EnterpriseMeta, err = bindEnterpriseMeta(authMethod, verifiedIdentity); err != nil {
return nil, err
}
// Load the auth method's binding rules.
_, rules, err := b.store.ACLBindingRuleList(nil, authMethod.Name, &authMethod.EnterpriseMeta)
if err != nil {
return nil, err
}
// Find the rules with selectors that match the identity's fields.
matchingRules := make(structs.ACLBindingRules, 0, len(rules))
for _, rule := range rules {
if doesSelectorMatch(rule.Selector, verifiedIdentity.SelectableFields) {
matchingRules = append(matchingRules, rule)
}
}
if len(matchingRules) == 0 {
return &bindings, nil
}
// Compute role, service identity, or node identity names by interpolating
// the identity's projected variables into the rule BindName templates.
for _, rule := range matchingRules {
bindName, valid, err := computeBindName(rule.BindType, rule.BindName, verifiedIdentity.ProjectedVars)
switch {
case err != nil:
return nil, fmt.Errorf("cannot compute %q bind name for bind target: %w", rule.BindType, err)
case !valid:
return nil, fmt.Errorf("computed %q bind name for bind target is invalid: %q", rule.BindType, bindName)
}
switch rule.BindType {
case structs.BindingRuleBindTypeService:
bindings.ServiceIdentities = append(bindings.ServiceIdentities, &structs.ACLServiceIdentity{
ServiceName: bindName,
})
case structs.BindingRuleBindTypeNode:
bindings.NodeIdentities = append(bindings.NodeIdentities, &structs.ACLNodeIdentity{
NodeName: bindName,
Datacenter: b.datacenter,
})
case structs.BindingRuleBindTypeRole:
_, role, err := b.store.ACLRoleGetByName(nil, bindName, &bindings.EnterpriseMeta)
if err != nil {
return nil, err
}
if role != nil {
bindings.Roles = append(bindings.Roles, structs.ACLTokenRoleLink{
ID: role.ID,
})
}
}
}
return &bindings, nil
}
// IsValidBindName returns whether the given BindName template produces valid
// results when interpolating the auth method's available variables.
func IsValidBindName(bindType, bindName string, availableVariables []string) (bool, error) {
if bindType == "" || bindName == "" {
return false, nil
}
fakeVarMap := make(map[string]string)
for _, v := range availableVariables {
fakeVarMap[v] = "fake"
}
_, valid, err := computeBindName(bindType, bindName, fakeVarMap)
if err != nil {
return false, err
}
return valid, nil
}
// computeBindName processes the HIL for the provided bind type+name 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 computeBindName(bindType, bindName string, projectedVars map[string]string) (string, bool, error) {
bindName, err := template.InterpolateHIL(bindName, projectedVars, true)
if err != nil {
return "", false, err
}
var valid bool
switch bindType {
case structs.BindingRuleBindTypeService:
valid = acl.IsValidServiceIdentityName(bindName)
case structs.BindingRuleBindTypeNode:
valid = acl.IsValidNodeIdentityName(bindName)
case structs.BindingRuleBindTypeRole:
valid = acl.IsValidRoleName(bindName)
default:
return "", false, fmt.Errorf("unknown binding rule bind type: %s", bindType)
}
return bindName, valid, nil
}
// 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(selector, nil, selectableVars)
if err != nil {
return false // fails to match if selector is invalid
}
result, err := eval.Evaluate(selectableVars)
if err != nil {
return false // fails to match if evaluation fails
}
return result
}

View File

@ -0,0 +1,14 @@
//go:build !consulent
// +build !consulent
package auth
import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/structs"
)
func bindEnterpriseMeta(authMethod *structs.ACLAuthMethod, verifiedIdentity *authmethod.Identity) (acl.EnterpriseMeta, error) {
return acl.EnterpriseMeta{}, nil
}

View File

@ -0,0 +1,372 @@
package auth
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
)
func TestBindings_None(t *testing.T) {
var b *Bindings
require.True(t, b.None())
b = &Bindings{}
require.True(t, b.None())
b = &Bindings{Roles: []structs.ACLTokenRoleLink{{ID: generateID(t)}}}
require.False(t, b.None())
b = &Bindings{ServiceIdentities: []*structs.ACLServiceIdentity{{ServiceName: "web"}}}
require.False(t, b.None())
b = &Bindings{NodeIdentities: []*structs.ACLNodeIdentity{{NodeName: "node-123"}}}
require.False(t, b.None())
}
func TestBinder_Roles_Success(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
targetRole := &structs.ACLRole{
ID: generateID(t),
Name: "vim-role",
}
require.NoError(t, store.ACLRoleSet(0, targetRole))
otherRole := &structs.ACLRole{
ID: generateID(t),
Name: "frontend-engineers",
}
require.NoError(t, store.ACLRoleSet(0, otherRole))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "role==engineer",
BindType: structs.BindingRuleBindTypeRole,
BindName: "${editor}-role",
AuthMethod: authMethod.Name,
},
{
ID: generateID(t),
Selector: "role==engineer",
BindType: structs.BindingRuleBindTypeRole,
BindName: "this-role-does-not-exist",
AuthMethod: authMethod.Name,
},
{
ID: generateID(t),
Selector: "language==js",
BindType: structs.BindingRuleBindTypeRole,
BindName: otherRole.Name,
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
result, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{
SelectableFields: map[string]string{
"role": "engineer",
"language": "go",
},
ProjectedVars: map[string]string{
"editor": "vim",
},
})
require.NoError(t, err)
require.Equal(t, []structs.ACLTokenRoleLink{
{ID: targetRole.ID},
}, result.Roles)
}
func TestBinder_Roles_NameValidation(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "",
BindType: structs.BindingRuleBindTypeRole,
BindName: "INVALID!",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
}
func TestBinder_ServiceIdentities_Success(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "tier==web",
BindType: structs.BindingRuleBindTypeService,
BindName: "web-service-${name}",
AuthMethod: authMethod.Name,
},
{
ID: generateID(t),
Selector: "tier==db",
BindType: structs.BindingRuleBindTypeService,
BindName: "database-${name}",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
result, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{
SelectableFields: map[string]string{
"tier": "web",
},
ProjectedVars: map[string]string{
"name": "billing",
},
})
require.NoError(t, err)
require.Equal(t, []*structs.ACLServiceIdentity{
{ServiceName: "web-service-billing"},
}, result.ServiceIdentities)
}
func TestBinder_ServiceIdentities_NameValidation(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "",
BindType: structs.BindingRuleBindTypeService,
BindName: "INVALID!",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
}
func TestBinder_NodeIdentities_Success(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store, datacenter: "dc1"}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "provider==gcp",
BindType: structs.BindingRuleBindTypeNode,
BindName: "gcp-${os}",
AuthMethod: authMethod.Name,
},
{
ID: generateID(t),
Selector: "provider==aws",
BindType: structs.BindingRuleBindTypeNode,
BindName: "aws-${os}",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
result, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{
SelectableFields: map[string]string{
"provider": "gcp",
},
ProjectedVars: map[string]string{
"os": "linux",
},
})
require.NoError(t, err)
require.Equal(t, []*structs.ACLNodeIdentity{
{NodeName: "gcp-linux", Datacenter: "dc1"},
}, result.NodeIdentities)
}
func TestBinder_NodeIdentities_NameValidation(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "",
BindType: structs.BindingRuleBindTypeNode,
BindName: "INVALID!",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
}
func Test_IsValidBindName(t *testing.T) {
type testcase struct {
name string
bindType string
bindName string
fields string
valid bool // valid HIL, invalid contents
err bool // invalid HIL
}
for _, test := range []testcase{
{"no bind type",
"", "", "", false, false},
{"bad bind type",
"invalid", "blah", "", false, true},
// valid HIL, invalid name
{"empty",
"both", "", "", false, false},
{"just end",
"both", "}", "", false, false},
{"var without start",
"both", " item }", "item", false, false},
{"two vars missing second start",
"both", "before-${ item }after--more }", "item,more", false, false},
// names for the two types are validated differently
{"@ is disallowed",
"both", "bad@name", "", false, false},
{"leading dash",
"role", "-name", "", true, false},
{"leading dash",
"service", "-name", "", false, false},
{"trailing dash",
"role", "name-", "", true, false},
{"trailing dash",
"service", "name-", "", false, false},
{"inner dash",
"both", "name-end", "", true, false},
{"upper case",
"role", "NAME", "", true, false},
{"upper case",
"service", "NAME", "", false, false},
// valid HIL, valid name
{"no vars",
"both", "nothing", "", true, false},
{"just var",
"both", "${item}", "item", true, false},
{"var in middle",
"both", "before-${item}after", "item", true, false},
{"two vars",
"both", "before-${item}after-${more}", "item,more", true, false},
// bad
{"no bind name",
"both", "", "", false, false},
{"just start",
"both", "${", "", false, true},
{"backwards",
"both", "}${", "", false, true},
{"no varname",
"both", "${}", "", false, true},
{"missing map key",
"both", "${item}", "", false, true},
{"var without end",
"both", "${ item ", "item", false, true},
{"two vars missing first end",
"both", "before-${ item after-${ more }", "item,more", false, true},
} {
var cases []testcase
if test.bindType == "both" {
test1 := test
test1.bindType = "role"
test2 := test
test2.bindType = "service"
cases = []testcase{test1, test2}
} else {
cases = []testcase{test}
}
for _, test := range cases {
test := test
t.Run(test.bindType+"--"+test.name, func(t *testing.T) {
t.Parallel()
valid, err := IsValidBindName(
test.bindType,
test.bindName,
strings.Split(test.fields, ","),
)
if test.err {
require.NotNil(t, err)
require.False(t, valid)
} else {
require.NoError(t, err)
require.Equal(t, test.valid, valid)
}
})
}
}
}
func generateID(t *testing.T) string {
t.Helper()
id, err := uuid.GenerateUUID()
require.NoError(t, err)
return id
}
func testStateStore(t *testing.T) *state.Store {
t.Helper()
gc, err := state.NewTombstoneGC(time.Second, time.Millisecond)
require.NoError(t, err)
return state.NewStateStore(gc)
}

View File

@ -0,0 +1,77 @@
package auth
import (
"encoding/json"
"fmt"
"strings"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
)
// Login wraps the process of creating an ACLToken from the identity verified
// by an auth method.
type Login struct {
binder *Binder
writer *TokenWriter
}
// NewLogin returns a new Login with the given binder and writer.
func NewLogin(binder *Binder, writer *TokenWriter) *Login {
return &Login{binder, writer}
}
// TokenForVerifiedIdentity creates an ACLToken for the given identity verified
// by an auth method.
func (l *Login) TokenForVerifiedIdentity(identity *authmethod.Identity, authMethod *structs.ACLAuthMethod, description string) (*structs.ACLToken, error) {
bindings, err := l.binder.Bind(authMethod, identity)
switch {
case err != nil:
return nil, err
case bindings.None():
// We try to prevent the creation of a useless token without taking a trip
// through Raft and the state store if we can.
return nil, acl.ErrPermissionDenied
}
token := &structs.ACLToken{
Description: description,
Local: authMethod.TokenLocality != "global", // TokenWriter prevents the creation of global tokens in secondary datacenters.
AuthMethod: authMethod.Name,
ExpirationTTL: authMethod.MaxTokenTTL,
ServiceIdentities: bindings.ServiceIdentities,
NodeIdentities: bindings.NodeIdentities,
Roles: bindings.Roles,
EnterpriseMeta: bindings.EnterpriseMeta,
}
token.ACLAuthMethodEnterpriseMeta.FillWithEnterpriseMeta(&authMethod.EnterpriseMeta)
updated, err := l.writer.Create(token, true)
switch {
case err != nil && strings.Contains(err.Error(), state.ErrTokenHasNoPrivileges.Error()):
// If we were in a slight race with a role delete operation then we may
// still end up failing to insert an unprivileged token in the state
// machine instead. Return the same error as earlier so it doesn't
// actually matter which one prevents the insertion.
return nil, acl.ErrPermissionDenied
case err != nil:
return nil, err
}
return updated, nil
}
// BuildTokenDescription builds a description for an ACLToken by encoding the
// given meta as JSON and applying the prefix.
func BuildTokenDescription(prefix string, meta map[string]string) (string, error) {
if len(meta) == 0 {
return prefix, nil
}
d, err := json.Marshal(meta)
if err != nil {
return "", err
}
return fmt.Sprintf("%s: %s", prefix, d), nil
}

View File

@ -0,0 +1,29 @@
// Code generated by mockery v2.12.0. DO NOT EDIT.
package auth
import (
testing "testing"
mock "github.com/stretchr/testify/mock"
)
// MockACLCache is an autogenerated mock type for the ACLCache type
type MockACLCache struct {
mock.Mock
}
// RemoveIdentityWithSecretToken provides a mock function with given fields: secretToken
func (_m *MockACLCache) RemoveIdentityWithSecretToken(secretToken string) {
_m.Called(secretToken)
}
// NewMockACLCache creates a new instance of MockACLCache. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockACLCache(t testing.TB) *MockACLCache {
mock := &MockACLCache{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,449 @@
package auth
import (
"errors"
"fmt"
"time"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
)
// ErrCannotWriteGlobalToken indicates that writing a token failed because
// the token is global and this is a non-primary datacenter.
var ErrCannotWriteGlobalToken = errors.New("Cannot upsert global tokens within this datacenter")
// NewTokenWriter creates a new token writer.
func NewTokenWriter(cfg TokenWriterConfig) *TokenWriter {
return &TokenWriter{cfg}
}
// TokenWriter encapsulates the logic of writing ACL tokens to the state store
// including validation, cache purging, etc.
type TokenWriter struct {
TokenWriterConfig
}
type TokenWriterConfig struct {
RaftApply RaftApplyFn
ACLCache ACLCache
Store TokenWriterStore
CheckUUID lib.UUIDCheckFunc
MaxExpirationTTL time.Duration
MinExpirationTTL time.Duration
PrimaryDatacenter string
InPrimaryDatacenter bool
LocalTokensEnabled bool
}
type RaftApplyFn func(structs.MessageType, interface{}) (interface{}, error)
//go:generate mockery --name ACLCache --inpackage
type ACLCache interface {
RemoveIdentityWithSecretToken(secretToken string)
}
type TokenWriterStore interface {
ACLTokenGetByAccessor(ws memdb.WatchSet, accessorID string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLToken, error)
ACLTokenGetBySecret(ws memdb.WatchSet, secretID string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLToken, error)
ACLRoleGetByID(ws memdb.WatchSet, id string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLRole, error)
ACLRoleGetByName(ws memdb.WatchSet, name string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLRole, error)
ACLPolicyGetByID(ws memdb.WatchSet, id string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLPolicy, error)
ACLPolicyGetByName(ws memdb.WatchSet, name string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLPolicy, error)
ACLTokenUpsertValidateEnterprise(token *structs.ACLToken, existing *structs.ACLToken) error
}
// Create a new token. Setting fromLogin to true changes behavior slightly for
// tokens created by login (as opposed to set manually via the API).
func (w *TokenWriter) Create(token *structs.ACLToken, fromLogin bool) (*structs.ACLToken, error) {
if err := w.checkCanWriteToken(token); err != nil {
return nil, err
}
if token.AccessorID == "" {
// Caller didn't provide an AccessorID, so generate one.
id, err := lib.GenerateUUID(w.CheckUUID)
if err != nil {
return nil, fmt.Errorf("Failed to generate AccessorID: %w", err)
}
token.AccessorID = id
} else {
// Check the AccessorID is valid and not already in-use.
if err := validateTokenID(token.AccessorID); err != nil {
return nil, fmt.Errorf("Invalid Token: AccessorID - %w", err)
}
if inUse, err := w.tokenIDInUse(token.AccessorID); err != nil {
return nil, fmt.Errorf("Failed to lookup ACL token: %w", err)
} else if inUse {
return nil, errors.New("Invalid Token: AccessorID is already in use")
}
}
if token.SecretID == "" {
// Caller didn't provide a SecretID, so generate one.
id, err := lib.GenerateUUID(w.CheckUUID)
if err != nil {
return nil, fmt.Errorf("Failed to generate SecretID: %w", err)
}
token.SecretID = id
} else {
// Check the SecretID is valid and not already in-use.
if err := validateTokenID(token.SecretID); err != nil {
return nil, fmt.Errorf("Invalid Token: SecretID - %w", err)
}
if inUse, err := w.tokenIDInUse(token.SecretID); err != nil {
return nil, fmt.Errorf("Failed to lookup ACL token: %w", err)
} else if inUse {
return nil, errors.New("Invalid Token: SecretID is already in use")
}
}
token.CreateTime = time.Now()
// Ensure ExpirationTTL is valid if provided.
if token.ExpirationTTL < 0 {
return nil, fmt.Errorf("Token Expiration TTL '%s' should be > 0", token.ExpirationTTL)
} else if token.ExpirationTTL > 0 {
if token.HasExpirationTime() {
return nil, errors.New("Token Expiration TTL and Expiration Time cannot both be set")
}
expirationTime := token.CreateTime.Add(token.ExpirationTTL)
token.ExpirationTime = &expirationTime
token.ExpirationTTL = 0
}
if token.HasExpirationTime() {
if token.ExpirationTime.Before(token.CreateTime) {
return nil, errors.New("ExpirationTime cannot be before CreateTime")
}
expiresIn := token.ExpirationTime.Sub(token.CreateTime)
if expiresIn > w.MaxExpirationTTL {
return nil, fmt.Errorf("ExpirationTime cannot be more than %s in the future (was %s)",
w.MaxExpirationTTL, expiresIn)
}
if expiresIn < w.MinExpirationTTL {
return nil, fmt.Errorf("ExpirationTime cannot be less than %s in the future (was %s)",
w.MinExpirationTTL, expiresIn)
}
}
if fromLogin {
if token.AuthMethod == "" {
return nil, errors.New("AuthMethod field is required during login")
}
} else {
if token.AuthMethod != "" {
return nil, errors.New("AuthMethod field is disallowed outside of login")
}
}
return w.write(token, nil, fromLogin)
}
// Update an existing token.
func (w *TokenWriter) Update(token *structs.ACLToken) (*structs.ACLToken, error) {
if err := w.checkCanWriteToken(token); err != nil {
return nil, err
}
if _, err := uuid.ParseUUID(token.AccessorID); err != nil {
return nil, errors.New("AccessorID is not a valid UUID")
}
// DEPRECATED (ACL-Legacy-Compat) - maybe get rid of this in the future
// and instead do a ParseUUID check. New tokens will not have
// secrets generated by users but rather they will always be UUIDs.
// However if users just continue the upgrade cycle they may still
// have tokens using secrets that are not UUIDS
// The RootAuthorizer checks that the SecretID is not "allow", "deny"
// or "manage" as a precaution against something accidentally using
// one of these root policies by setting the secret to it.
if acl.RootAuthorizer(token.SecretID) != nil {
return nil, acl.PermissionDeniedError{Cause: "Cannot modify root ACL"}
}
_, match, err := w.Store.ACLTokenGetByAccessor(nil, token.AccessorID, nil)
switch {
case err != nil:
return nil, fmt.Errorf("Failed acl token lookup by accessor: %w", err)
case match == nil || match.IsExpired(time.Now()):
return nil, fmt.Errorf("Cannot find token %q", token.AccessorID)
}
if token.SecretID == "" {
token.SecretID = match.SecretID
} else if match.SecretID != token.SecretID {
return nil, errors.New("Changing a token's SecretID is not permitted")
}
if token.Local != match.Local {
return nil, fmt.Errorf("Cannot toggle local mode of %s", token.AccessorID)
}
if token.AuthMethod == "" {
token.AuthMethod = match.AuthMethod
} else if match.AuthMethod != token.AuthMethod {
return nil, fmt.Errorf("Cannot change AuthMethod of %s", token.AccessorID)
}
if token.ExpirationTTL != 0 {
return nil, fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
}
if token.HasExpirationTime() {
if !match.HasExpirationTime() || !match.ExpirationTime.Equal(*token.ExpirationTime) {
return nil, fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
}
} else {
token.ExpirationTime = match.ExpirationTime
}
token.CreateTime = match.CreateTime
return w.write(token, match, false)
}
// Delete the ACL token with the given SecretID from the state store.
func (w *TokenWriter) Delete(secretID string, fromLogout bool) error {
_, token, err := w.Store.ACLTokenGetBySecret(nil, secretID, nil)
switch {
case err != nil:
return err
case token == nil:
return acl.ErrNotFound
case token.AuthMethod == "" && fromLogout:
return fmt.Errorf("%w: token wasn't created via login", acl.ErrPermissionDenied)
}
if err := w.checkCanWriteToken(token); err != nil {
return err
}
if _, err := w.RaftApply(structs.ACLTokenDeleteRequestType, &structs.ACLTokenBatchDeleteRequest{
TokenIDs: []string{token.AccessorID},
}); err != nil {
return fmt.Errorf("Failed to apply token delete request: %w", err)
}
w.ACLCache.RemoveIdentityWithSecretToken(token.SecretID)
return nil
}
func validateTokenID(id string) error {
if structs.ACLIDReserved(id) {
return fmt.Errorf("UUIDs with the prefix %q are reserved", structs.ACLReservedPrefix)
}
if _, err := uuid.ParseUUID(id); err != nil {
return errors.New("not a valid UUID")
}
return nil
}
func (w *TokenWriter) checkCanWriteToken(token *structs.ACLToken) error {
if !w.LocalTokensEnabled {
return fmt.Errorf("Cannot upsert tokens within this datacenter")
}
if !w.InPrimaryDatacenter && !token.Local {
return ErrCannotWriteGlobalToken
}
return nil
}
func (w *TokenWriter) tokenIDInUse(id string) (bool, error) {
_, accessorMatch, err := w.Store.ACLTokenGetByAccessor(nil, id, nil)
switch {
case err != nil:
return false, err
case accessorMatch != nil:
return true, nil
}
_, secretMatch, err := w.Store.ACLTokenGetBySecret(nil, id, nil)
switch {
case err != nil:
return false, err
case secretMatch != nil:
return true, nil
}
return false, nil
}
func (w *TokenWriter) write(token, existing *structs.ACLToken, fromLogin bool) (*structs.ACLToken, error) {
roles, err := w.normalizeRoleLinks(token.Roles, &token.EnterpriseMeta)
if err != nil {
return nil, err
}
token.Roles = roles
policies, err := w.normalizePolicyLinks(token.Policies, &token.EnterpriseMeta)
if err != nil {
return nil, err
}
token.Policies = policies
serviceIdentities, err := w.normalizeServiceIdentities(token.ServiceIdentities, token.Local)
if err != nil {
return nil, err
}
token.ServiceIdentities = serviceIdentities
nodeIdentities, err := w.normalizeNodeIdentities(token.NodeIdentities)
if err != nil {
return nil, err
}
token.NodeIdentities = nodeIdentities
if token.Rules != "" {
return nil, errors.New("Rules cannot be specified for this token")
}
if token.Type != "" {
return nil, errors.New("Type cannot be specified for this token")
}
if err := w.enterpriseValidation(token, existing); err != nil {
return nil, err
}
token.SetHash(true)
// Persist the token by writing to Raft.
_, err = w.RaftApply(structs.ACLTokenSetRequestType, &structs.ACLTokenBatchSetRequest{
Tokens: structs.ACLTokens{token},
// Logins may attempt to link to roles that do not exist. These may be
// persisted, but don't allow tokens to be created that have no privileges
// (i.e. role links that point nowhere).
AllowMissingLinks: fromLogin,
ProhibitUnprivileged: fromLogin,
})
if err != nil {
return nil, fmt.Errorf("Failed to apply token write request: %w", err)
}
// Purge the token from the ACL cache.
w.ACLCache.RemoveIdentityWithSecretToken(token.SecretID)
// Refresh the token from the state store.
_, updatedToken, err := w.Store.ACLTokenGetByAccessor(nil, token.AccessorID, nil)
if err != nil || updatedToken == nil {
return nil, errors.New("Failed to retrieve token after insertion")
}
return updatedToken, nil
}
func (w *TokenWriter) normalizeRoleLinks(links []structs.ACLTokenRoleLink, entMeta *acl.EnterpriseMeta) ([]structs.ACLTokenRoleLink, error) {
var normalized []structs.ACLTokenRoleLink
uniqueIDs := make(map[string]struct{})
for _, link := range links {
if link.ID == "" {
_, role, err := w.Store.ACLRoleGetByName(nil, link.Name, entMeta)
switch {
case err != nil:
return nil, fmt.Errorf("Error looking up role for name: %q: %w", link.Name, err)
case role == nil:
return nil, fmt.Errorf("No such ACL role with name %q", link.Name)
}
link.ID = role.ID
} else {
_, role, err := w.Store.ACLRoleGetByID(nil, link.ID, entMeta)
switch {
case err != nil:
return nil, fmt.Errorf("Error looking up role for ID: %q: %w", link.ID, err)
case role == nil:
return nil, fmt.Errorf("No such ACL role with ID %q", link.ID)
}
}
// Do not persist the role name as the role could be renamed in the future.
link.Name = ""
// De-duplicate role links by ID.
if _, ok := uniqueIDs[link.ID]; !ok {
normalized = append(normalized, link)
uniqueIDs[link.ID] = struct{}{}
}
}
return normalized, nil
}
func (w *TokenWriter) normalizePolicyLinks(links []structs.ACLTokenPolicyLink, entMeta *acl.EnterpriseMeta) ([]structs.ACLTokenPolicyLink, error) {
var normalized []structs.ACLTokenPolicyLink
uniqueIDs := make(map[string]struct{})
for _, link := range links {
if link.ID == "" {
_, role, err := w.Store.ACLPolicyGetByName(nil, link.Name, entMeta)
switch {
case err != nil:
return nil, fmt.Errorf("Error looking up policy for name: %q: %w", link.Name, err)
case role == nil:
return nil, fmt.Errorf("No such ACL policy with name %q", link.Name)
}
link.ID = role.ID
} else {
_, role, err := w.Store.ACLPolicyGetByID(nil, link.ID, entMeta)
switch {
case err != nil:
return nil, fmt.Errorf("Error looking up policy for ID: %q: %w", link.ID, err)
case role == nil:
return nil, fmt.Errorf("No such ACL policy with ID %q", link.ID)
}
}
// Do not persist the role name as the role could be renamed in the future.
link.Name = ""
// De-duplicate role links by ID.
if _, ok := uniqueIDs[link.ID]; !ok {
normalized = append(normalized, link)
uniqueIDs[link.ID] = struct{}{}
}
}
return normalized, nil
}
func (w *TokenWriter) normalizeServiceIdentities(svcIDs structs.ACLServiceIdentities, tokenLocal bool) (structs.ACLServiceIdentities, error) {
for _, id := range svcIDs {
if id.ServiceName == "" {
return nil, errors.New("Service identity is missing the service name field on this token")
}
if tokenLocal && len(id.Datacenters) > 0 {
return nil, fmt.Errorf("Service identity %q cannot specify a list of datacenters on a local token", id.ServiceName)
}
if !acl.IsValidServiceIdentityName(id.ServiceName) {
return nil, fmt.Errorf("Service identity %q has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed", id.ServiceName)
}
}
return svcIDs.Deduplicate(), nil
}
func (w *TokenWriter) normalizeNodeIdentities(nodeIDs structs.ACLNodeIdentities) (structs.ACLNodeIdentities, error) {
for _, id := range nodeIDs {
if id.NodeName == "" {
return nil, errors.New("Node identity is missing the node name field on this token")
}
if id.Datacenter == "" {
return nil, errors.New("Node identity is missing the datacenter field on this token")
}
if !acl.IsValidNodeIdentityName(id.NodeName) {
return nil, fmt.Errorf("Node identity has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed")
}
}
return nodeIDs.Deduplicate(), nil
}

View File

@ -0,0 +1,10 @@
//go:build !consulent
// +build !consulent
package auth
import "github.com/hashicorp/consul/agent/structs"
func (w *TokenWriter) enterpriseValidation(token, existing *structs.ACLToken) error {
return nil
}

View File

@ -0,0 +1,703 @@
package auth
import (
"errors"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
)
func TestTokenWriter_Create_Validation(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
existingToken := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
}
require.NoError(t, store.ACLTokenSet(0, existingToken))
writer := buildTokenWriter(store, aclCache)
testCases := map[string]struct {
token structs.ACLToken
fromLogin bool
errorContains string
}{
"AccessorID not a UUID": {
token: structs.ACLToken{AccessorID: "not-a-uuid"},
errorContains: "not a valid UUID",
},
"AccessorID is reserved": {
token: structs.ACLToken{AccessorID: structs.ACLReservedPrefix + generateID(t)},
errorContains: "reserved",
},
"AccessorID already in use (as AccessorID)": {
token: structs.ACLToken{AccessorID: existingToken.AccessorID},
errorContains: "already in use",
},
"AccessorID already in use (as SecretID)": {
token: structs.ACLToken{AccessorID: existingToken.SecretID},
errorContains: "already in use",
},
"SecretID not a UUID": {
token: structs.ACLToken{SecretID: "not-a-uuid"},
errorContains: "not a valid UUID",
},
"SecretID is reserved": {
token: structs.ACLToken{SecretID: structs.ACLReservedPrefix + generateID(t)},
errorContains: "reserved",
},
"SecretID already in use (as AccessorID)": {
token: structs.ACLToken{SecretID: existingToken.AccessorID},
errorContains: "already in use",
},
"SecretID already in use (as SecretID)": {
token: structs.ACLToken{SecretID: existingToken.SecretID},
errorContains: "already in use",
},
"ExpirationTTL is negative": {
token: structs.ACLToken{ExpirationTTL: -1},
errorContains: "should be > 0",
},
"ExpirationTTL and ExpirationTime both set": {
token: structs.ACLToken{
ExpirationTTL: 2 * time.Hour,
ExpirationTime: timePointer(time.Now().Add(1 * time.Hour)),
},
errorContains: "cannot both be set",
},
"ExpirationTTL > MaxExpirationTTL": {
token: structs.ACLToken{ExpirationTTL: 48 * time.Hour},
errorContains: "cannot be more than 24h0m0s in the future",
},
"ExpirationTTL < MinExpirationTTL": {
token: structs.ACLToken{ExpirationTTL: 30 * time.Second},
errorContains: "cannot be less than 1m0s in the future",
},
"ExpirationTime before CreateTime": {
token: structs.ACLToken{ExpirationTime: timePointer(time.Now().Add(-5 * time.Minute))},
errorContains: "ExpirationTime cannot be before CreateTime",
},
"AuthMethod not set for login": {
token: structs.ACLToken{},
fromLogin: true,
errorContains: "AuthMethod field is required during login",
},
"AuthMethod set outside of login": {
token: structs.ACLToken{AuthMethod: "some-auth-method"},
fromLogin: false,
errorContains: "AuthMethod field is disallowed outside of login",
},
"Rules set": {
token: structs.ACLToken{Rules: "some rules"},
errorContains: "Rules cannot be specified for this token",
},
"Type set": {
token: structs.ACLToken{Type: "some-type"},
errorContains: "Type cannot be specified for this token",
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
_, err := writer.Create(&tc.token, tc.fromLogin)
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
})
}
}
func TestTokenWriter_Create_IDGeneration(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
writer := buildTokenWriter(store, aclCache)
t.Run("AccessorID", func(t *testing.T) {
token := &structs.ACLToken{
SecretID: generateID(t),
ServiceIdentities: []*structs.ACLServiceIdentity{
{ServiceName: "some-service"},
},
}
updated, err := writer.Create(token, false)
require.NoError(t, err)
require.NotEmpty(t, updated.AccessorID)
})
t.Run("SecretID", func(t *testing.T) {
token := &structs.ACLToken{
AccessorID: generateID(t),
ServiceIdentities: []*structs.ACLServiceIdentity{
{ServiceName: "some-service"},
},
}
updated, err := writer.Create(token, false)
require.NoError(t, err)
require.NotEmpty(t, updated.SecretID)
})
}
func TestTokenWriter_Roles(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
role := &structs.ACLRole{
ID: generateID(t),
Name: generateID(t),
}
require.NoError(t, store.ACLRoleSet(0, role))
writer := buildTokenWriter(store, aclCache)
testCases := map[string]struct {
input []structs.ACLTokenRoleLink
output []structs.ACLTokenRoleLink
errorContains string
}{
"valid role ID": {
input: []structs.ACLTokenRoleLink{{ID: role.ID}},
output: []structs.ACLTokenRoleLink{{ID: role.ID, Name: role.Name}},
},
"valid role name": {
input: []structs.ACLTokenRoleLink{{Name: role.Name}},
output: []structs.ACLTokenRoleLink{{ID: role.ID, Name: role.Name}},
},
"invalid role ID": {
input: []structs.ACLTokenRoleLink{{ID: generateID(t)}},
errorContains: "No such ACL role with ID",
},
"invalid role name": {
input: []structs.ACLTokenRoleLink{{Name: "invalid-role-name"}},
errorContains: "No such ACL role with name",
},
"links are de-duplicated": {
input: []structs.ACLTokenRoleLink{{ID: role.ID}, {ID: role.ID}},
output: []structs.ACLTokenRoleLink{{ID: role.ID, Name: role.Name}},
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
updated, err := writer.Create(&structs.ACLToken{Roles: tc.input}, false)
if tc.errorContains == "" {
require.NoError(t, err)
require.ElementsMatch(t, tc.output, updated.Roles)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
}
})
}
}
func TestTokenWriter_Policies(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
policy := &structs.ACLPolicy{
ID: generateID(t),
Name: generateID(t),
}
require.NoError(t, store.ACLPolicySet(0, policy))
writer := buildTokenWriter(store, aclCache)
testCases := map[string]struct {
input []structs.ACLTokenPolicyLink
output []structs.ACLTokenPolicyLink
errorContains string
}{
"valid policy ID": {
input: []structs.ACLTokenPolicyLink{{ID: policy.ID}},
output: []structs.ACLTokenPolicyLink{{ID: policy.ID, Name: policy.Name}},
},
"valid policy name": {
input: []structs.ACLTokenPolicyLink{{Name: policy.Name}},
output: []structs.ACLTokenPolicyLink{{ID: policy.ID, Name: policy.Name}},
},
"invalid policy ID": {
input: []structs.ACLTokenPolicyLink{{ID: generateID(t)}},
errorContains: "No such ACL policy with ID",
},
"invalid policy name": {
input: []structs.ACLTokenPolicyLink{{Name: "invalid-policy-name"}},
errorContains: "No such ACL policy with name",
},
"links are de-duplicated": {
input: []structs.ACLTokenPolicyLink{{ID: policy.ID}, {ID: policy.ID}},
output: []structs.ACLTokenPolicyLink{{ID: policy.ID, Name: policy.Name}},
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
updated, err := writer.Create(&structs.ACLToken{Policies: tc.input}, false)
if tc.errorContains == "" {
require.NoError(t, err)
require.ElementsMatch(t, tc.output, updated.Policies)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
}
})
}
}
func TestTokenWriter_ServiceIdentities(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
writer := buildTokenWriter(store, aclCache)
testCases := map[string]struct {
input []*structs.ACLServiceIdentity
tokenLocal bool
output []*structs.ACLServiceIdentity
errorContains string
}{
"empty service name": {
input: []*structs.ACLServiceIdentity{{ServiceName: ""}},
errorContains: "missing the service name",
},
"datacenters given on local token": {
input: []*structs.ACLServiceIdentity{{ServiceName: "web", Datacenters: []string{"dc1", "dc2"}}},
tokenLocal: true,
errorContains: "cannot specify a list of datacenters on a local token",
},
"invalid service name": {
input: []*structs.ACLServiceIdentity{{ServiceName: "INVALID!"}},
errorContains: "has an invalid name",
},
"duplicate identities are merged": {
input: []*structs.ACLServiceIdentity{
{ServiceName: "web", Datacenters: []string{"dc1"}},
{ServiceName: "web", Datacenters: []string{"dc2"}},
},
output: []*structs.ACLServiceIdentity{{ServiceName: "web", Datacenters: []string{"dc1", "dc2"}}},
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
updated, err := writer.Create(&structs.ACLToken{
ServiceIdentities: tc.input,
Local: tc.tokenLocal,
}, false)
if tc.errorContains == "" {
require.NoError(t, err)
require.ElementsMatch(t, tc.output, updated.ServiceIdentities)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
}
})
}
}
func TestTokenWriter_NodeIdentities(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
writer := buildTokenWriter(store, aclCache)
testCases := map[string]struct {
input []*structs.ACLNodeIdentity
output []*structs.ACLNodeIdentity
errorContains string
}{
"empty service name": {
input: []*structs.ACLNodeIdentity{{NodeName: "", Datacenter: "dc1"}},
errorContains: "missing the node name",
},
"empty datacenter": {
input: []*structs.ACLNodeIdentity{{NodeName: "web"}},
errorContains: "missing the datacenter field",
},
"invalid node name": {
input: []*structs.ACLNodeIdentity{{NodeName: "INVALID!", Datacenter: "dc1"}},
errorContains: "has an invalid name",
},
"duplicate identities are removed": {
input: []*structs.ACLNodeIdentity{
{NodeName: "web", Datacenter: "dc1"},
{NodeName: "web", Datacenter: "dc2"},
{NodeName: "web", Datacenter: "dc1"},
},
output: []*structs.ACLNodeIdentity{
{NodeName: "web", Datacenter: "dc1"},
{NodeName: "web", Datacenter: "dc2"},
},
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
updated, err := writer.Create(&structs.ACLToken{NodeIdentities: tc.input}, false)
if tc.errorContains == "" {
require.NoError(t, err)
require.ElementsMatch(t, tc.output, updated.NodeIdentities)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
}
})
}
}
func TestTokenWriter_Create_Expiration(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
role := &structs.ACLRole{
ID: generateID(t),
Name: generateID(t),
}
require.NoError(t, store.ACLRoleSet(0, role))
writer := buildTokenWriter(store, aclCache)
t.Run("ExpirationTTL", func(t *testing.T) {
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
Roles: []structs.ACLTokenRoleLink{
{ID: role.ID},
},
ExpirationTTL: 10 * time.Minute,
}
updated, err := writer.Create(token, false)
require.NoError(t, err)
require.InEpsilon(t, 10*time.Minute, updated.ExpirationTime.Sub(time.Now()), 0.1)
require.Zero(t, updated.ExpirationTTL)
})
t.Run("ExpirationTime", func(t *testing.T) {
expirationTime := time.Now().Add(10 * time.Minute)
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
Roles: []structs.ACLTokenRoleLink{
{ID: role.ID},
},
ExpirationTime: &expirationTime,
}
updated, err := writer.Create(token, false)
require.NoError(t, err)
require.Equal(t, expirationTime, *updated.ExpirationTime)
})
}
func TestTokenWriter_Create_Success(t *testing.T) {
store := testStateStore(t)
role := &structs.ACLRole{
ID: generateID(t),
Name: "cluster-operators",
}
require.NoError(t, store.ACLRoleSet(0, role))
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
Roles: []structs.ACLTokenRoleLink{
{ID: role.ID},
},
}
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", token.SecretID)
defer aclCache.AssertExpectations(t)
writer := buildTokenWriter(store, aclCache)
updated, err := writer.Create(token, false)
require.NoError(t, err)
require.NotNil(t, updated)
}
func TestTokenWriter_Update_Validation(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
ExpirationTime: timePointer(time.Now().Add(1 * time.Hour)),
}
expiredToken := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
ExpirationTime: timePointer(time.Now().Add(-1 * time.Hour)),
}
require.NoError(t, store.ACLTokenBatchSet(0, []*structs.ACLToken{token, expiredToken}, state.ACLTokenSetOptions{}))
writer := buildTokenWriter(store, aclCache)
testCases := map[string]struct {
token structs.ACLToken
errorContains string
}{
"AccessorID not a UUID": {
token: structs.ACLToken{AccessorID: "not-a-uuid"},
errorContains: "not a valid UUID",
},
"SecretID is a legacy root policy name": {
token: structs.ACLToken{AccessorID: token.AccessorID, SecretID: "allow"},
errorContains: "Cannot modify root ACL",
},
"AccessorID does not match any token": {
token: structs.ACLToken{AccessorID: generateID(t)},
errorContains: "Cannot find token",
},
"AccessorID matches expired token": {
token: structs.ACLToken{AccessorID: expiredToken.AccessorID},
errorContains: "Cannot find token",
},
"SecretID changed": {
token: structs.ACLToken{AccessorID: token.AccessorID, SecretID: generateID(t)},
errorContains: "Changing a token's SecretID is not permitted",
},
"Local changed": {
token: structs.ACLToken{AccessorID: token.AccessorID, Local: !token.Local},
errorContains: "Cannot toggle local mode",
},
"AuthMethod changed": {
token: structs.ACLToken{AccessorID: token.AccessorID, AuthMethod: "some-other-auth-method"},
errorContains: "Cannot change AuthMethod",
},
"ExpirationTTL is set": {
token: structs.ACLToken{AccessorID: token.AccessorID, ExpirationTTL: 5 * time.Minute},
errorContains: "Cannot change expiration time",
},
"ExpirationTime changed": {
token: structs.ACLToken{AccessorID: token.AccessorID, ExpirationTime: timePointer(token.ExpirationTime.Add(1 * time.Minute))},
errorContains: "Cannot change expiration time",
},
"Rules set": {
token: structs.ACLToken{AccessorID: token.AccessorID, Rules: "some rules"},
errorContains: "Rules cannot be specified for this token",
},
"Type set": {
token: structs.ACLToken{AccessorID: token.AccessorID, Type: "some-type"},
errorContains: "Type cannot be specified for this token",
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
_, err := writer.Update(&tc.token)
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
})
}
}
func TestTokenWriter_Update_Success(t *testing.T) {
store := testStateStore(t)
authMethod := &structs.ACLAuthMethod{
Name: generateID(t),
Type: "jwt",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
ExpirationTime: timePointer(time.Now().Add(1 * time.Hour)),
AuthMethod: authMethod.Name,
}
token.SetHash(true)
require.NoError(t, store.ACLTokenSet(0, token))
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", token.SecretID)
defer aclCache.AssertExpectations(t)
writer := buildTokenWriter(store, aclCache)
updated, err := writer.Update(&structs.ACLToken{
AccessorID: token.AccessorID,
Description: "New Description",
})
require.NoError(t, err)
require.Equal(t, "New Description", updated.Description)
// These should've been left as-is.
require.Equal(t, token.SecretID, updated.SecretID)
require.Equal(t, token.Local, updated.Local)
require.Equal(t, token.AuthMethod, updated.AuthMethod)
require.Equal(t, token.ExpirationTime, updated.ExpirationTime)
require.Equal(t, token.CreateTime, updated.CreateTime)
require.NotEqual(t, token.Hash, updated.Hash)
}
func TestTokenWriter_Delete(t *testing.T) {
t.Run("success", func(t *testing.T) {
store := testStateStore(t)
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
Local: true,
}
require.NoError(t, store.ACLTokenSet(0, token))
aclCache := NewMockACLCache(t)
aclCache.On("RemoveIdentityWithSecretToken", token.SecretID).Return()
var deletedIDs []string
writer := NewTokenWriter(TokenWriterConfig{
LocalTokensEnabled: true,
ACLCache: aclCache,
Store: store,
RaftApply: func(msgType structs.MessageType, msg interface{}) (interface{}, error) {
if msgType != structs.ACLTokenDeleteRequestType {
return nil, fmt.Errorf("unexpected message type: %v", msgType)
}
req, ok := msg.(*structs.ACLTokenBatchDeleteRequest)
if !ok {
return nil, fmt.Errorf("unexpected message: %T", msg)
}
deletedIDs = req.TokenIDs
return nil, nil
},
})
err := writer.Delete(token.SecretID, false)
require.NoError(t, err)
require.Equal(t, []string{token.AccessorID}, deletedIDs)
})
t.Run("local tokens disabled", func(t *testing.T) {
store := testStateStore(t)
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
Local: true,
}
require.NoError(t, store.ACLTokenSet(0, token))
writer := NewTokenWriter(TokenWriterConfig{
LocalTokensEnabled: false,
Store: store,
})
err := writer.Delete(token.SecretID, false)
require.Error(t, err)
require.Contains(t, err.Error(), "Cannot upsert tokens within this datacenter")
})
t.Run("global token in non-primary datacenter", func(t *testing.T) {
store := testStateStore(t)
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
Local: false,
}
require.NoError(t, store.ACLTokenSet(0, token))
writer := NewTokenWriter(TokenWriterConfig{
LocalTokensEnabled: true,
InPrimaryDatacenter: false,
Store: store,
})
err := writer.Delete(token.SecretID, false)
require.Error(t, err)
require.Equal(t, ErrCannotWriteGlobalToken, err)
})
t.Run("token not found", func(t *testing.T) {
store := testStateStore(t)
writer := NewTokenWriter(TokenWriterConfig{
LocalTokensEnabled: true,
Store: store,
})
err := writer.Delete(generateID(t), false)
require.Error(t, err)
require.True(t, errors.Is(err, acl.ErrNotFound))
})
t.Run("logout requires token to be created by login", func(t *testing.T) {
store := testStateStore(t)
token := &structs.ACLToken{
AccessorID: generateID(t),
SecretID: generateID(t),
Local: true,
}
require.NoError(t, store.ACLTokenSet(0, token))
writer := NewTokenWriter(TokenWriterConfig{
LocalTokensEnabled: true,
Store: store,
})
err := writer.Delete(token.SecretID, true)
require.Error(t, err)
require.True(t, errors.Is(err, acl.ErrPermissionDenied))
require.Contains(t, err.Error(), "wasn't created via login")
})
}
func raftApplyACLTokenSet(store *state.Store) RaftApplyFn {
return func(msgType structs.MessageType, msg interface{}) (interface{}, error) {
if msgType != structs.ACLTokenSetRequestType {
return nil, fmt.Errorf("unexpected message type: %v", msgType)
}
req, ok := msg.(*structs.ACLTokenBatchSetRequest)
if !ok {
return nil, fmt.Errorf("unexpected message: %T", msg)
}
err := store.ACLTokenBatchSet(0, req.Tokens, state.ACLTokenSetOptions{
CAS: req.CAS,
AllowMissingPolicyAndRoleIDs: req.AllowMissingLinks,
ProhibitUnprivileged: req.ProhibitUnprivileged,
})
return nil, err
}
}
func timePointer(t time.Time) *time.Time { return &t }
func buildTokenWriter(store *state.Store, aclCache ACLCache) *TokenWriter {
return NewTokenWriter(TokenWriterConfig{
RaftApply: raftApplyACLTokenSet(store),
ACLCache: aclCache,
Store: store,
MinExpirationTTL: 1 * time.Minute,
MaxExpirationTTL: 24 * time.Hour,
PrimaryDatacenter: "dc1",
InPrimaryDatacenter: true,
LocalTokensEnabled: true,
})
}

View File

@ -8,7 +8,11 @@ import (
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
"github.com/hashicorp/consul/agent/grpc/public"
"github.com/hashicorp/consul/agent/structs"
tokenStore "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/proto-public/pbacl"
"github.com/hashicorp/consul/proto-public/pbconnectca"
"github.com/hashicorp/consul/proto-public/pbserverdiscovery"
)
@ -25,12 +29,12 @@ func TestGRPCIntegration_ConnectCA_Sign(t *testing.T) {
// * Making a request to a follower's public gRPC port.
// * Ensuring that the request is correctly forwarded to the leader.
// * Ensuring we get a valid certificate back (so it went through the CAManager).
server1, conn1 := testGRPCIntegrationServer(t, func(c *Config) {
server1, conn1, _ := testGRPCIntegrationServer(t, func(c *Config) {
c.Bootstrap = false
c.BootstrapExpect = 2
})
server2, conn2 := testGRPCIntegrationServer(t, func(c *Config) {
server2, conn2, _ := testGRPCIntegrationServer(t, func(c *Config) {
c.Bootstrap = false
})
@ -81,7 +85,7 @@ func TestGRPCIntegration_ServerDiscovery_WatchServers(t *testing.T) {
// * Adding another server
// * Validating another message is sent.
server1, conn := testGRPCIntegrationServer(t, func(c *Config) {
server1, conn, _ := testGRPCIntegrationServer(t, func(c *Config) {
c.Bootstrap = true
c.BootstrapExpect = 1
})
@ -115,3 +119,97 @@ func TestGRPCIntegration_ServerDiscovery_WatchServers(t *testing.T) {
require.NotNil(t, rsp)
require.Len(t, rsp.Servers, 2)
}
func TestGRPCIntegration_ACL_Login_Logout(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
// The gRPC endpoints themselves are well unit tested - this test ensures we're
// correctly wiring everything up and exercises the cross-dc RPC forwarding by:
//
// * Starting two servers in different datacenters.
// * WAN federating them.
// * Configuring ACL token replication in the secondary datacenter.
// * Registering an auth method (configured for global tokens) in the primary
// datacenter.
// * Making a Login request to the secondary DC, with the request's Datacenter
// field set to "primary" (to exercise user requested DC forwarding).
// * Waiting for the token to be replicated to the secondary DC.
// * Making a Logout request to the secondary DC, with the request's Datacenter
// field set to "secondary" — the request will be forwarded to the primary
// datacenter anyway because the token is global.
// Start the primary DC.
primary, _, primaryCodec := testGRPCIntegrationServer(t, func(c *Config) {
c.Bootstrap = true
c.BootstrapExpect = 1
c.Datacenter = "primary"
c.PrimaryDatacenter = "primary"
})
waitForLeaderEstablishment(t, primary)
// Configured the auth method.
testSessionID := testauth.StartSession()
defer testauth.ResetSession(testSessionID)
testauth.InstallSessionToken(testSessionID, "fake-token", "default", "demo", "abc123")
authMethod, err := upsertTestCustomizedAuthMethod(primaryCodec, TestDefaultInitialManagementToken, "primary", func(method *structs.ACLAuthMethod) {
method.Config = map[string]interface{}{
"SessionID": testSessionID,
}
method.TokenLocality = "global"
})
require.NoError(t, err)
_, err = upsertTestBindingRule(primaryCodec, TestDefaultInitialManagementToken, "primary", authMethod.Name, "", structs.BindingRuleBindTypeService, "demo")
require.NoError(t, err)
// Start the secondary DC.
secondary, secondaryConn, _ := testGRPCIntegrationServer(t, func(c *Config) {
c.Bootstrap = true
c.BootstrapExpect = 1
c.Datacenter = "secondary"
c.PrimaryDatacenter = "primary"
c.ACLTokenReplication = true
})
secondary.tokens.UpdateReplicationToken(TestDefaultInitialManagementToken, tokenStore.TokenSourceConfig)
waitForLeaderEstablishment(t, secondary)
// WAN federate the primary and secondary DCs.
joinWAN(t, primary, secondary)
client := pbacl.NewACLServiceClient(secondaryConn)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
t.Cleanup(cancel)
// Make a Login request to the secondary DC, but request that it is forwarded
// to the primary DC.
rsp, err := client.Login(ctx, &pbacl.LoginRequest{
AuthMethod: authMethod.Name,
BearerToken: "fake-token",
Datacenter: "primary",
})
require.NoError(t, err)
require.NotNil(t, rsp.Token)
require.NotEmpty(t, rsp.Token.AccessorId)
require.NotEmpty(t, rsp.Token.SecretId)
// Check token was created in the primary DC.
tokenIdx, token, err := primary.FSM().State().ACLTokenGetByAccessor(nil, rsp.Token.AccessorId, nil)
require.NoError(t, err)
require.NotNil(t, token)
require.False(t, token.Local, "token should be global")
// Wait for token to be replicated to the secondary DC.
waitForNewACLReplication(t, secondary, structs.ACLReplicateTokens, 0, tokenIdx, 0)
// Make a Logout request to the secondary DC, the request should be forwarded
// to the primary DC anyway because the token is global.
_, err = client.Logout(ctx, &pbacl.LogoutRequest{
Token: rsp.Token.SecretId,
Datacenter: "secondary",
})
require.NoError(t, err)
}

View File

@ -17,7 +17,6 @@ import (
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul-net-rpc/net/rpc"
connlimit "github.com/hashicorp/go-connlimit"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
@ -30,6 +29,8 @@ import (
"golang.org/x/time/rate"
"google.golang.org/grpc"
"github.com/hashicorp/consul-net-rpc/net/rpc"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
@ -40,6 +41,7 @@ import (
"github.com/hashicorp/consul/agent/consul/wanfed"
agentgrpc "github.com/hashicorp/consul/agent/grpc/private"
"github.com/hashicorp/consul/agent/grpc/private/services/subscribe"
aclgrpc "github.com/hashicorp/consul/agent/grpc/public/services/acl"
"github.com/hashicorp/consul/agent/grpc/public/services/connectca"
"github.com/hashicorp/consul/agent/grpc/public/services/dataplane"
"github.com/hashicorp/consul/agent/grpc/public/services/serverdiscovery"
@ -239,6 +241,11 @@ type Server struct {
// is only ever closed.
leaveCh chan struct{}
// publicACLServer serves the ACL service exposed on the public gRPC port.
// It is also exposed on the private multiplexed "server" port to enable
// RPC forwarding.
publicACLServer *aclgrpc.Server
// publicConnectCAServer serves the Connect CA service exposed on the public
// gRPC port. It is also exposed on the private multiplexed "server" port to
// enable RPC forwarding.
@ -667,6 +674,24 @@ func NewServer(config *Config, flat Deps, publicGRPCServer *grpc.Server) (*Serve
go s.overviewManager.Run(&lib.StopChannelContext{StopCh: s.shutdownCh})
// Initialize public gRPC server - register services on public gRPC server.
s.publicACLServer = aclgrpc.NewServer(aclgrpc.Config{
ACLsEnabled: s.config.ACLsEnabled,
ForwardRPC: func(info structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) {
return s.ForwardGRPC(s.grpcConnPool, info, fn)
},
InPrimaryDatacenter: s.InPrimaryDatacenter(),
LoadAuthMethod: func(methodName string, entMeta *acl.EnterpriseMeta) (*structs.ACLAuthMethod, aclgrpc.Validator, error) {
return s.loadAuthMethod(methodName, entMeta)
},
LocalTokensEnabled: s.LocalTokensEnabled,
Logger: logger.Named("grpc-api.acl"),
NewLogin: func() aclgrpc.Login { return s.aclLogin() },
NewTokenWriter: func() aclgrpc.TokenWriter { return s.aclTokenWriter() },
PrimaryDatacenter: s.config.PrimaryDatacenter,
ValidateEnterpriseRequest: s.validateEnterpriseRequest,
})
s.publicACLServer.Register(s.publicGRPCServer)
s.publicConnectCAServer = connectca.NewServer(connectca.Config{
Publisher: s.publisher,
GetStore: func() connectca.StateStore { return s.FSM().State() },
@ -748,8 +773,9 @@ func newGRPCHandlerFromConfig(deps Deps, config *Config, s *Server) connHandler
pbpeering.RegisterPeeringServiceServer(srv, s.peeringService)
s.registerEnterpriseGRPCServices(deps, srv)
// Note: this public gRPC service is also exposed on the private server to
// Note: these public gRPC services are also exposed on the private server to
// enable RPC forwarding.
s.publicACLServer.Register(srv)
s.publicConnectCAServer.Register(srv)
}

View File

@ -259,8 +259,8 @@ func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToke
return dir, srv, codec
}
func testGRPCIntegrationServer(t *testing.T, cb func(*Config)) (*Server, *grpc.ClientConn) {
_, srv, _ := testACLServerWithConfig(t, cb, false)
func testGRPCIntegrationServer(t *testing.T, cb func(*Config)) (*Server, *grpc.ClientConn, rpc.ClientCodec) {
_, srv, codec := testACLServerWithConfig(t, cb, false)
// Normally the gRPC server listener is created at the agent level and passed down into
// the Server creation. For our tests, we need to ensure
@ -276,7 +276,7 @@ func testGRPCIntegrationServer(t *testing.T, cb func(*Config)) (*Server, *grpc.C
t.Cleanup(func() { _ = conn.Close() })
return srv, conn
return srv, conn, codec
}
func newServer(t *testing.T, c *Config) (*Server, error) {

View File

@ -0,0 +1,92 @@
package acl
import (
"context"
"errors"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/auth"
"github.com/hashicorp/consul/agent/grpc/public"
"github.com/hashicorp/consul/proto-public/pbacl"
)
// Login exchanges the presented bearer token for a Consul ACL token using a
// configured auth method.
func (s *Server) Login(ctx context.Context, req *pbacl.LoginRequest) (*pbacl.LoginResponse, error) {
logger := s.Logger.Named("login").With("request_id", public.TraceID())
logger.Trace("request received")
if err := s.requireACLsEnabled(logger); err != nil {
return nil, err
}
entMeta := acl.NewEnterpriseMetaWithPartition(req.Partition, req.Namespace)
if err := s.ValidateEnterpriseRequest(&entMeta, true); err != nil {
logger.Error("error during enterprise request validation", "error", err.Error())
return nil, status.Errorf(codes.Internal, err.Error())
}
// Forward request to leader in the correct datacenter.
var rsp *pbacl.LoginResponse
handled, err := s.forwardWriteDC(req.Datacenter, func(conn *grpc.ClientConn) error {
var err error
rsp, err = pbacl.NewACLServiceClient(conn).Login(ctx, req)
return err
}, logger)
if handled || err != nil {
return rsp, err
}
// This is also validated by the TokenWriter, but doing it early saves any
// work done by the validator (e.g. roundtrip to the Kubernetes API server).
if err := s.requireLocalTokens(logger); err != nil {
return nil, err
}
authMethod, validator, err := s.LoadAuthMethod(req.AuthMethod, &entMeta)
switch {
case errors.Is(err, acl.ErrNotFound):
return nil, status.Errorf(codes.InvalidArgument, "auth method %q not found", req.AuthMethod)
case err != nil:
logger.Error("failed to load auth method", "error", err.Error())
return nil, status.Error(codes.Internal, "failed to load auth method")
}
verifiedIdentity, err := validator.ValidateLogin(ctx, req.BearerToken)
if err != nil {
// TODO(agentless): errors returned from validators aren't standardized so
// it's hard to tell whether validation failed because of an invalid bearer
// token or something internal/transient. We currently return Unauthenticated
// for all errors because it's the most likely, but we should make validators
// return a typed or sentinel error instead.
logger.Error("failed to validate login", "error", err.Error())
return nil, status.Error(codes.Unauthenticated, err.Error())
}
description, err := auth.BuildTokenDescription("token created via login", req.Meta)
if err != nil {
logger.Error("failed to build token description", "error", err.Error())
return nil, status.Error(codes.Internal, err.Error())
}
token, err := s.NewLogin().TokenForVerifiedIdentity(verifiedIdentity, authMethod, description)
switch {
case acl.IsErrPermissionDenied(err):
return nil, status.Error(codes.PermissionDenied, err.Error())
case err != nil:
logger.Error("failed to create token", "error", err.Error())
return nil, status.Error(codes.Internal, "failed to create token")
}
return &pbacl.LoginResponse{
Token: &pbacl.LoginToken{
AccessorId: token.AccessorID,
SecretId: token.SecretID,
},
}, nil
}

View File

@ -0,0 +1,256 @@
package acl
import (
"context"
"errors"
"fmt"
"testing"
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/grpc/public/testutils"
structs "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto-public/pbacl"
)
const bearerToken = "bearer-token"
func TestServer_Login_Success(t *testing.T) {
authMethod := &structs.ACLAuthMethod{}
identity := &authmethod.Identity{}
validator := NewMockValidator(t)
validator.On("ValidateLogin", mock.Anything, bearerToken).
Return(identity, nil)
token := &structs.ACLToken{
AccessorID: "accessor-id",
SecretID: "secret-id",
}
login := NewMockLogin(t)
login.On("TokenForVerifiedIdentity", identity, authMethod, "token created via login").
Return(token, nil)
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
LoadAuthMethod: func(methodName string, entMeta *acl.EnterpriseMeta) (*structs.ACLAuthMethod, Validator, error) {
return authMethod, validator, nil
},
ForwardRPC: noopForwardRPC,
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
LocalTokensEnabled: noopLocalTokensEnabled,
NewLogin: func() Login { return login },
})
rsp, err := server.Login(context.Background(), &pbacl.LoginRequest{
BearerToken: bearerToken,
})
require.NoError(t, err)
require.Equal(t, token.AccessorID, rsp.Token.AccessorId)
require.Equal(t, token.SecretID, rsp.Token.SecretId)
}
func TestServer_Login_LoadAuthMethodErrors(t *testing.T) {
testCases := map[string]struct {
error error
code codes.Code
}{
"auth method not found": {
// Note: we wrap the error here to make sure we correctly unwrap it in the handler.
error: fmt.Errorf("%w auth method not found", acl.ErrNotFound),
code: codes.InvalidArgument,
},
"unexpected error": {
error: errors.New("BOOM"),
code: codes.Internal,
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
LoadAuthMethod: func(methodName string, entMeta *acl.EnterpriseMeta) (*structs.ACLAuthMethod, Validator, error) {
return nil, nil, tc.error
},
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
LocalTokensEnabled: noopLocalTokensEnabled,
ForwardRPC: noopForwardRPC,
})
_, err := server.Login(context.Background(), &pbacl.LoginRequest{
BearerToken: bearerToken,
})
require.Error(t, err)
require.Equal(t, tc.code.String(), status.Code(err).String())
})
}
}
func TestServer_Login_ValidateEnterpriseRequest(t *testing.T) {
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
ValidateEnterpriseRequest: func(*acl.EnterpriseMeta, bool) error { return errors.New("BOOM") },
ForwardRPC: noopForwardRPC,
})
_, err := server.Login(context.Background(), &pbacl.LoginRequest{
BearerToken: bearerToken,
})
require.Error(t, err)
require.Equal(t, codes.Internal.String(), status.Code(err).String())
}
func TestServer_Login_ACLsDisabled(t *testing.T) {
server := NewServer(Config{
ACLsEnabled: false,
Logger: hclog.NewNullLogger(),
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
ForwardRPC: noopForwardRPC,
LocalTokensEnabled: noopLocalTokensEnabled,
})
_, err := server.Login(context.Background(), &pbacl.LoginRequest{
BearerToken: bearerToken,
})
require.Error(t, err)
require.Equal(t, codes.FailedPrecondition.String(), status.Code(err).String())
}
func TestServer_Login_LocalTokensDisabled(t *testing.T) {
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
ForwardRPC: noopForwardRPC,
LocalTokensEnabled: func() bool { return false },
})
_, err := server.Login(context.Background(), &pbacl.LoginRequest{
BearerToken: bearerToken,
})
require.Error(t, err)
require.Equal(t, codes.FailedPrecondition.String(), status.Code(err).String())
}
func TestServer_Login_ValidateLoginError(t *testing.T) {
validator := NewMockValidator(t)
validator.On("ValidateLogin", mock.Anything, bearerToken).
Return(nil, errors.New("BOOM"))
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
LoadAuthMethod: func(methodName string, entMeta *acl.EnterpriseMeta) (*structs.ACLAuthMethod, Validator, error) {
return &structs.ACLAuthMethod{}, validator, nil
},
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
LocalTokensEnabled: noopLocalTokensEnabled,
ForwardRPC: noopForwardRPC,
})
_, err := server.Login(context.Background(), &pbacl.LoginRequest{
BearerToken: bearerToken,
})
require.Error(t, err)
require.Equal(t, codes.Unauthenticated.String(), status.Code(err).String())
}
func TestServer_Login_TokenForVerifiedIdentityErrors(t *testing.T) {
testCases := map[string]struct {
error error
code codes.Code
}{
"permission denied": {
error: acl.ErrPermissionDenied,
code: codes.PermissionDenied,
},
"unexpected error": {
error: errors.New("BOOM"),
code: codes.Internal,
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
validator := NewMockValidator(t)
validator.On("ValidateLogin", mock.Anything, bearerToken).
Return(&authmethod.Identity{}, nil)
login := NewMockLogin(t)
login.On("TokenForVerifiedIdentity", mock.Anything, mock.Anything, mock.Anything).
Return(nil, tc.error)
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
LoadAuthMethod: func(methodName string, entMeta *acl.EnterpriseMeta) (*structs.ACLAuthMethod, Validator, error) {
return &structs.ACLAuthMethod{}, validator, nil
},
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
LocalTokensEnabled: noopLocalTokensEnabled,
ForwardRPC: noopForwardRPC,
NewLogin: func() Login { return login },
})
_, err := server.Login(context.Background(), &pbacl.LoginRequest{
BearerToken: bearerToken,
})
require.Error(t, err)
require.Equal(t, tc.code.String(), status.Code(err).String())
})
}
}
func TestServer_Login_RPCForwarding(t *testing.T) {
validator := NewMockValidator(t)
validator.On("ValidateLogin", mock.Anything, mock.Anything).
Return(&authmethod.Identity{}, nil)
login := NewMockLogin(t)
login.On("TokenForVerifiedIdentity", mock.Anything, mock.Anything, mock.Anything).
Return(&structs.ACLToken{AccessorID: "leader response"}, nil)
dc2 := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
LoadAuthMethod: func(methodName string, entMeta *acl.EnterpriseMeta) (*structs.ACLAuthMethod, Validator, error) {
return &structs.ACLAuthMethod{}, validator, nil
},
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
LocalTokensEnabled: noopLocalTokensEnabled,
ForwardRPC: noopForwardRPC,
NewLogin: func() Login { return login },
})
leaderConn, err := grpc.Dial(testutils.RunTestServer(t, dc2).String(), grpc.WithInsecure())
require.NoError(t, err)
dc1 := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
ForwardRPC: func(info structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) {
if dc := info.RequestDatacenter(); dc != "dc2" {
return false, fmt.Errorf("unexpected target datacenter: %s", dc)
}
return true, fn(leaderConn)
},
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
})
rsp, err := dc1.Login(context.Background(), &pbacl.LoginRequest{
BearerToken: bearerToken,
Datacenter: "dc2",
})
require.NoError(t, err)
require.Equal(t, "leader response", rsp.Token.AccessorId)
}

View File

@ -0,0 +1,69 @@
package acl
import (
"context"
"errors"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/auth"
"github.com/hashicorp/consul/agent/grpc/public"
"github.com/hashicorp/consul/proto-public/pbacl"
)
// Logout destroys the given ACL token once the caller is done with it.
func (s *Server) Logout(ctx context.Context, req *pbacl.LogoutRequest) (*emptypb.Empty, error) {
logger := s.Logger.Named("logout").With("request_id", public.TraceID())
logger.Trace("request received")
if err := s.requireACLsEnabled(logger); err != nil {
return nil, err
}
if req.Token == "" {
return nil, status.Error(codes.InvalidArgument, "token is required")
}
// Forward request to leader in the requested datacenter.
var rsp *emptypb.Empty
handled, err := s.forwardWriteDC(req.Datacenter, func(conn *grpc.ClientConn) error {
var err error
rsp, err = pbacl.NewACLServiceClient(conn).Logout(ctx, req)
return err
}, logger)
if handled || err != nil {
return rsp, err
}
if err := s.requireLocalTokens(logger); err != nil {
return nil, err
}
err = s.NewTokenWriter().Delete(req.Token, true)
switch {
case errors.Is(err, auth.ErrCannotWriteGlobalToken):
// Writes to global tokens must be forwarded to the primary DC.
req.Datacenter = s.PrimaryDatacenter
_, err = s.forwardWriteDC(s.PrimaryDatacenter, func(conn *grpc.ClientConn) error {
var err error
rsp, err = pbacl.NewACLServiceClient(conn).Logout(ctx, req)
return err
}, logger)
return rsp, err
case errors.Is(err, acl.ErrNotFound):
// No token? Pretend the delete was successful (for idempotency).
return &emptypb.Empty{}, nil
case errors.Is(err, acl.ErrPermissionDenied):
return nil, status.Error(codes.PermissionDenied, err.Error())
case err != nil:
logger.Error("failed to delete token", "error", err.Error())
return nil, status.Error(codes.Internal, "failed to delete token")
}
return &emptypb.Empty{}, nil
}

View File

@ -0,0 +1,224 @@
package acl
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/auth"
"github.com/hashicorp/consul/agent/grpc/public/testutils"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto-public/pbacl"
)
func TestServer_Logout_Success(t *testing.T) {
secretID := generateID(t)
tokenWriter := NewMockTokenWriter(t)
tokenWriter.On("Delete", secretID, true).Return(nil)
server := NewServer(Config{
ACLsEnabled: true,
InPrimaryDatacenter: true,
ForwardRPC: noopForwardRPC,
LocalTokensEnabled: noopLocalTokensEnabled,
Logger: hclog.NewNullLogger(),
NewTokenWriter: func() TokenWriter { return tokenWriter },
})
_, err := server.Logout(context.Background(), &pbacl.LogoutRequest{
Token: secretID,
})
require.NoError(t, err)
}
func TestServer_Logout_EmptyToken(t *testing.T) {
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
})
_, err := server.Logout(context.Background(), &pbacl.LogoutRequest{
Token: "",
})
require.Error(t, err)
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
require.Contains(t, err.Error(), "token is required")
}
func TestServer_Logout_ACLsDisabled(t *testing.T) {
server := NewServer(Config{
ACLsEnabled: false,
Logger: hclog.NewNullLogger(),
ValidateEnterpriseRequest: noopValidateEnterpriseRequest,
ForwardRPC: noopForwardRPC,
LocalTokensEnabled: noopLocalTokensEnabled,
})
_, err := server.Logout(context.Background(), &pbacl.LogoutRequest{
Token: generateID(t),
})
require.Error(t, err)
require.Equal(t, codes.FailedPrecondition.String(), status.Code(err).String())
}
func TestServer_Logout_LocalTokensDisabled(t *testing.T) {
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
ForwardRPC: noopForwardRPC,
LocalTokensEnabled: func() bool { return false },
})
_, err := server.Logout(context.Background(), &pbacl.LogoutRequest{
Token: generateID(t),
})
require.Error(t, err)
require.Equal(t, codes.FailedPrecondition.String(), status.Code(err).String())
require.Contains(t, err.Error(), "token replication is required")
}
func TestServer_Logout_NoSuchToken(t *testing.T) {
tokenWriter := NewMockTokenWriter(t)
tokenWriter.On("Delete", mock.Anything, true).Return(acl.ErrNotFound)
server := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
ForwardRPC: noopForwardRPC,
LocalTokensEnabled: noopLocalTokensEnabled,
NewTokenWriter: func() TokenWriter { return tokenWriter },
})
_, err := server.Logout(context.Background(), &pbacl.LogoutRequest{
Token: generateID(t),
})
require.NoError(t, err)
}
func TestServer_Logout_PermissionDenied(t *testing.T) {
tokenWriter := NewMockTokenWriter(t)
tokenWriter.On("Delete", mock.Anything, true).Return(acl.ErrPermissionDenied)
server := NewServer(Config{
ACLsEnabled: true,
InPrimaryDatacenter: true,
ForwardRPC: noopForwardRPC,
LocalTokensEnabled: noopLocalTokensEnabled,
Logger: hclog.NewNullLogger(),
NewTokenWriter: func() TokenWriter { return tokenWriter },
})
_, err := server.Logout(context.Background(), &pbacl.LogoutRequest{
Token: generateID(t),
})
require.Error(t, err)
require.Equal(t, codes.PermissionDenied.String(), status.Code(err).String())
}
func TestServer_Logout_RPCForwarding(t *testing.T) {
tokenWriter := NewMockTokenWriter(t)
tokenWriter.On("Delete", mock.Anything, true).Return(nil)
dc1 := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
NewTokenWriter: func() TokenWriter { return tokenWriter },
ForwardRPC: noopForwardRPC,
LocalTokensEnabled: func() bool { return true },
})
dc1Conn, err := grpc.Dial(
testutils.RunTestServer(t, dc1).String(),
grpc.WithInsecure(),
)
require.NoError(t, err)
dc2 := NewServer(Config{
ACLsEnabled: true,
Logger: hclog.NewNullLogger(),
ForwardRPC: func(rpcInfo structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) {
return true, fn(dc1Conn)
},
})
_, err = dc2.Logout(context.Background(), &pbacl.LogoutRequest{
Token: generateID(t),
})
require.NoError(t, err)
}
func TestServer_Logout_GlobalWritesForwardedToPrimaryDC(t *testing.T) {
tokenWriter := NewMockTokenWriter(t)
tokenWriter.On("Delete", mock.Anything, true).Return(nil)
// This test checks that requests to delete global tokens are forwared to the
// primary datacenter by:
//
// 1. Setting up 2 servers (1 in the primary DC, 1 in the secondary).
// 2. Making a logout request to the secondary DC.
// 3. Mocking TokenWriter.Delete to return ErrCannotWriteGlobalToken in the
// secondary DC.
// 4. Checking that the primary DC server's TokenWriter receives a call to
// Delete.
// 5. Capturing the forwarded request's Datacenter in the primary DC server's
// ForwardRPC (to check that we overwrote the user-supplied Datacenter
// field to prevent infinite forwarding loops!)
var forwardedRequestDatacenter string
primary := NewServer(Config{
ACLsEnabled: true,
InPrimaryDatacenter: true,
LocalTokensEnabled: noopLocalTokensEnabled,
Logger: hclog.NewNullLogger(),
NewTokenWriter: func() TokenWriter { return tokenWriter },
ForwardRPC: func(info structs.RPCInfo, _ func(*grpc.ClientConn) error) (bool, error) {
forwardedRequestDatacenter = info.RequestDatacenter()
return false, nil
},
})
primaryConn, err := grpc.Dial(
testutils.RunTestServer(t, primary).String(),
grpc.WithInsecure(),
)
require.NoError(t, err)
secondary := NewServer(Config{
ACLsEnabled: true,
InPrimaryDatacenter: false,
LocalTokensEnabled: noopLocalTokensEnabled,
Logger: hclog.NewNullLogger(),
PrimaryDatacenter: "primary",
ForwardRPC: func(info structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) {
dc := info.RequestDatacenter()
switch dc {
case "secondary":
return false, nil
case "primary":
return true, fn(primaryConn)
default:
return false, fmt.Errorf("unexpected target datacenter: %s", dc)
}
},
NewTokenWriter: func() TokenWriter {
tokenWriter := NewMockTokenWriter(t)
tokenWriter.On("Delete", mock.Anything, true).Return(auth.ErrCannotWriteGlobalToken)
return tokenWriter
},
})
_, err = secondary.Logout(context.Background(), &pbacl.LogoutRequest{
Token: generateID(t),
Datacenter: "secondary",
})
require.NoError(t, err)
require.Equal(t, "primary", forwardedRequestDatacenter)
}

View File

@ -0,0 +1,50 @@
// Code generated by mockery v2.12.0. DO NOT EDIT.
package acl
import (
authmethod "github.com/hashicorp/consul/agent/consul/authmethod"
mock "github.com/stretchr/testify/mock"
structs "github.com/hashicorp/consul/agent/structs"
testing "testing"
)
// MockLogin is an autogenerated mock type for the Login type
type MockLogin struct {
mock.Mock
}
// TokenForVerifiedIdentity provides a mock function with given fields: identity, authMethod, description
func (_m *MockLogin) TokenForVerifiedIdentity(identity *authmethod.Identity, authMethod *structs.ACLAuthMethod, description string) (*structs.ACLToken, error) {
ret := _m.Called(identity, authMethod, description)
var r0 *structs.ACLToken
if rf, ok := ret.Get(0).(func(*authmethod.Identity, *structs.ACLAuthMethod, string) *structs.ACLToken); ok {
r0 = rf(identity, authMethod, description)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*structs.ACLToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*authmethod.Identity, *structs.ACLAuthMethod, string) error); ok {
r1 = rf(identity, authMethod, description)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewMockLogin creates a new instance of MockLogin. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockLogin(t testing.TB) *MockLogin {
mock := &MockLogin{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,38 @@
// Code generated by mockery v2.12.0. DO NOT EDIT.
package acl
import (
testing "testing"
mock "github.com/stretchr/testify/mock"
)
// MockTokenWriter is an autogenerated mock type for the TokenWriter type
type MockTokenWriter struct {
mock.Mock
}
// Delete provides a mock function with given fields: secretID, fromLogout
func (_m *MockTokenWriter) Delete(secretID string, fromLogout bool) error {
ret := _m.Called(secretID, fromLogout)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(secretID, fromLogout)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewMockTokenWriter creates a new instance of MockTokenWriter. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockTokenWriter(t testing.TB) *MockTokenWriter {
mock := &MockTokenWriter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,51 @@
// Code generated by mockery v2.12.0. DO NOT EDIT.
package acl
import (
context "context"
authmethod "github.com/hashicorp/consul/agent/consul/authmethod"
mock "github.com/stretchr/testify/mock"
testing "testing"
)
// MockValidator is an autogenerated mock type for the Validator type
type MockValidator struct {
mock.Mock
}
// ValidateLogin provides a mock function with given fields: ctx, loginToken
func (_m *MockValidator) ValidateLogin(ctx context.Context, loginToken string) (*authmethod.Identity, error) {
ret := _m.Called(ctx, loginToken)
var r0 *authmethod.Identity
if rf, ok := ret.Get(0).(func(context.Context, string) *authmethod.Identity); ok {
r0 = rf(ctx, loginToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*authmethod.Identity)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, loginToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewMockValidator creates a new instance of MockValidator. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockValidator(t testing.TB) *MockValidator {
mock := &MockValidator{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,88 @@
package acl
import (
"context"
"github.com/hashicorp/go-hclog"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto-public/pbacl"
)
type Config struct {
ACLsEnabled bool
Logger hclog.Logger
LoadAuthMethod func(authMethod string, entMeta *acl.EnterpriseMeta) (*structs.ACLAuthMethod, Validator, error)
NewLogin func() Login
ForwardRPC func(structs.RPCInfo, func(*grpc.ClientConn) error) (bool, error)
ValidateEnterpriseRequest func(*acl.EnterpriseMeta, bool) error
LocalTokensEnabled func() bool
InPrimaryDatacenter bool
PrimaryDatacenter string
NewTokenWriter func() TokenWriter
}
//go:generate mockery --name Login --inpackage
type Login interface {
TokenForVerifiedIdentity(identity *authmethod.Identity, authMethod *structs.ACLAuthMethod, description string) (*structs.ACLToken, error)
}
//go:generate mockery --name Validator --inpackage
type Validator interface {
ValidateLogin(ctx context.Context, loginToken string) (*authmethod.Identity, error)
}
//go:generate mockery --name TokenWriter --inpackage
type TokenWriter interface {
Delete(secretID string, fromLogout bool) error
}
type Server struct {
Config
}
func NewServer(cfg Config) *Server {
return &Server{cfg}
}
func (s *Server) Register(grpcServer *grpc.Server) {
pbacl.RegisterACLServiceServer(grpcServer, s)
}
func (s *Server) requireACLsEnabled(logger hclog.Logger) error {
if s.ACLsEnabled {
return nil
}
logger.Warn("request blocked ACLs are disabled")
return status.Error(codes.FailedPrecondition, acl.ErrDisabled.Error())
}
func (s *Server) requireLocalTokens(logger hclog.Logger) error {
if s.LocalTokensEnabled() {
return nil
}
logger.Warn("request blocked because we're in a non-primary datacenter and token replication is disabled")
return status.Error(codes.FailedPrecondition, "token replication is required for auth methods to function")
}
func (s *Server) forwardWriteDC(dc string, fn func(*grpc.ClientConn) error, logger hclog.Logger) (bool, error) {
// For private/internal gRPC handlers, protoc-gen-rpc-glue generates the
// requisite methods to satisfy the structs.RPCInfo interface using fields
// from the pbcommon package. This service is public, so we can't use those
// fields in our proto definition. Instead, we construct our RPCInfo manually.
var rpcInfo struct {
structs.WriteRequest // Ensure RPCs are forwarded to the leader.
structs.DCSpecificRequest // Ensure RPCs are forwarded to the correct datacenter.
}
rpcInfo.Datacenter = dc
return s.ForwardRPC(&rpcInfo, func(conn *grpc.ClientConn) error {
logger.Trace("forwarding RPC", "datacenter", dc)
return fn(conn)
})
}

View File

@ -0,0 +1,37 @@
package acl
import (
"testing"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"github.com/hashicorp/consul/acl"
structs "github.com/hashicorp/consul/agent/structs"
)
func generateID(t *testing.T) string {
t.Helper()
id, err := uuid.GenerateUUID()
require.NoError(t, err)
return id
}
func noopForwardRPC(structs.RPCInfo, func(*grpc.ClientConn) error) (bool, error) {
return false, nil
}
func noopValidateEnterpriseRequest(*acl.EnterpriseMeta, bool) error {
return nil
}
func noopLocalTokensEnabled() bool {
return true
}
func noopACLsEnabled() bool {
return true
}

View File

@ -169,6 +169,34 @@ func (s *ACLServiceIdentity) SyntheticPolicy(entMeta *acl.EnterpriseMeta) *ACLPo
return policy
}
type ACLServiceIdentities []*ACLServiceIdentity
// Deduplicate returns a new list of service identities without duplicates.
// Identities with the same ServiceName but different datacenters will be
// merged into a single identity with all datacenters.
func (ids ACLServiceIdentities) Deduplicate() ACLServiceIdentities {
unique := make(map[string]*ACLServiceIdentity)
for _, id := range ids {
entry, ok := unique[id.ServiceName]
if ok {
dcs := stringslice.CloneStringSlice(id.Datacenters)
sort.Strings(dcs)
entry.Datacenters = stringslice.MergeSorted(dcs, entry.Datacenters)
} else {
entry = id.Clone()
sort.Strings(entry.Datacenters)
unique[id.ServiceName] = entry
}
}
results := make(ACLServiceIdentities, 0, len(unique))
for _, id := range unique {
results = append(results, id)
}
return results
}
// ACLNodeIdentity represents a high-level grant of all privileges
// necessary to assume the identity of that node and manage it.
type ACLNodeIdentity struct {
@ -213,6 +241,27 @@ func (s *ACLNodeIdentity) SyntheticPolicy(entMeta *acl.EnterpriseMeta) *ACLPolic
return policy
}
type ACLNodeIdentities []*ACLNodeIdentity
// Deduplicate returns a new list of node identities without duplicates.
func (ids ACLNodeIdentities) Deduplicate() ACLNodeIdentities {
type mapKey struct {
nodeName, datacenter string
}
seen := make(map[mapKey]struct{})
var results ACLNodeIdentities
for _, id := range ids {
key := mapKey{id.NodeName, id.Datacenter}
if _, ok := seen[key]; ok {
continue
}
results = append(results, id.Clone())
seen[key] = struct{}{}
}
return results
}
type ACLToken struct {
// This is the UUID used for tracking and management purposes
AccessorID string
@ -234,10 +283,10 @@ type ACLToken struct {
Roles []ACLTokenRoleLink `json:",omitempty"`
// List of services to generate synthetic policies for.
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
ServiceIdentities ACLServiceIdentities `json:",omitempty"`
// The node identities that this token should be allowed to manage.
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
NodeIdentities ACLNodeIdentities `json:",omitempty"`
// Type is the V1 Token Type
// DEPRECATED (ACL-Legacy-Compat) - remove once we no longer support v1 ACL compat
@ -499,8 +548,8 @@ type ACLTokenListStub struct {
Description string
Policies []ACLTokenPolicyLink `json:",omitempty"`
Roles []ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
ServiceIdentities ACLServiceIdentities `json:",omitempty"`
NodeIdentities ACLNodeIdentities `json:",omitempty"`
Local bool
AuthMethod string `json:",omitempty"`
ExpirationTime *time.Time `json:",omitempty"`
@ -808,10 +857,10 @@ type ACLRole struct {
Policies []ACLRolePolicyLink `json:",omitempty"`
// List of services to generate synthetic policies for.
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
ServiceIdentities ACLServiceIdentities `json:",omitempty"`
// List of nodes to generate synthetic policies for.
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
NodeIdentities ACLNodeIdentities `json:",omitempty"`
// Hash of the contents of the role
// This does not take into account the ID (which is immutable)

View File

@ -27,7 +27,6 @@ type ACLCaches struct {
type IdentityCacheEntry struct {
Identity ACLIdentity
CacheTime time.Time
Error error
}
func (e *IdentityCacheEntry) Age() time.Duration {
@ -135,6 +134,12 @@ func (c *ACLCaches) GetIdentity(id string) *IdentityCacheEntry {
return nil
}
// GetIdentityWithSecretToken fetches the identity with the given secret token
// from the cache.
func (c *ACLCaches) GetIdentityWithSecretToken(secretToken string) *IdentityCacheEntry {
return c.GetIdentity(cacheIDSecretToken(secretToken))
}
// GetPolicy fetches a policy from the cache and returns it
func (c *ACLCaches) GetPolicy(policyID string) *PolicyCacheEntry {
if c == nil || c.policies == nil {
@ -188,12 +193,28 @@ func (c *ACLCaches) GetRole(roleID string) *RoleCacheEntry {
}
// PutIdentity adds a new identity to the cache
func (c *ACLCaches) PutIdentity(id string, ident ACLIdentity, err error) {
func (c *ACLCaches) PutIdentity(id string, ident ACLIdentity) {
if c == nil || c.identities == nil {
return
}
c.identities.Add(id, &IdentityCacheEntry{Identity: ident, CacheTime: time.Now(), Error: err})
c.identities.Add(id, &IdentityCacheEntry{Identity: ident, CacheTime: time.Now()})
}
// PutIdentityWithSecretToken adds a new identity to the cache, keyed by the
// given secret token (with a prefix to prevent collisions).
func (c *ACLCaches) PutIdentityWithSecretToken(secretToken string, identity ACLIdentity) {
c.PutIdentity(cacheIDSecretToken(secretToken), identity)
}
// RemoveIdentityWithSecretToken removes the identity from the cache with the
// given secret token.
func (c *ACLCaches) RemoveIdentityWithSecretToken(secretToken string) {
if c == nil || c.identities == nil {
return
}
c.identities.Remove(cacheIDSecretToken(secretToken))
}
func (c *ACLCaches) PutPolicy(policyId string, policy *ACLPolicy) {
@ -265,3 +286,7 @@ func (c *ACLCaches) Purge() {
}
}
}
func cacheIDSecretToken(token string) string {
return "token-secret:" + token
}

View File

@ -47,10 +47,18 @@ func TestStructs_ACLCaches(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, cache)
cache.PutIdentity("foo", &ACLToken{}, nil)
cache.PutIdentity("foo", &ACLToken{})
entry := cache.GetIdentity("foo")
require.NotNil(t, entry)
require.NotNil(t, entry.Identity)
cache.PutIdentityWithSecretToken("secret", &ACLToken{})
entry = cache.GetIdentityWithSecretToken("secret")
require.NotNil(t, entry)
require.NotNil(t, entry.Identity)
cache.RemoveIdentityWithSecretToken("secret")
entry = cache.GetIdentityWithSecretToken("secret")
require.Nil(t, entry)
})
t.Run("Policies", func(t *testing.T) {

View File

@ -85,6 +85,36 @@ func TestStructs_ACLServiceIdentity_SyntheticPolicy(t *testing.T) {
}
}
func TestStructs_ACLServiceIdentities_Deduplicate(t *testing.T) {
identities := ACLServiceIdentities{
{ServiceName: "web", Datacenters: []string{"dc1"}},
{ServiceName: "web", Datacenters: []string{"dc2"}},
{ServiceName: "db", Datacenters: []string{"dc3"}},
}
require.ElementsMatch(t, ACLServiceIdentities{
{ServiceName: "web", Datacenters: []string{"dc1", "dc2"}},
{ServiceName: "db", Datacenters: []string{"dc3"}},
}, identities.Deduplicate())
require.Len(t, identities, 3, "original slice shouldn't have been mutated")
}
func TestStructs_ACLNodeIdentities_Deduplicate(t *testing.T) {
identities := ACLNodeIdentities{
{NodeName: "web", Datacenter: "dc1"},
{NodeName: "web", Datacenter: "dc2"},
{NodeName: "web", Datacenter: "dc1"},
}
require.Equal(t, ACLNodeIdentities{
{NodeName: "web", Datacenter: "dc1"},
{NodeName: "web", Datacenter: "dc2"},
}, identities.Deduplicate())
require.Len(t, identities, 3, "original slice shouldn't have been mutated")
}
func TestStructs_ACLToken_SetHash(t *testing.T) {
token := ACLToken{

View File

@ -18,11 +18,12 @@ import (
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/duration"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/serf/coordinate"
"github.com/mitchellh/hashstructure"
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/api"

View File

@ -92,7 +92,7 @@ func TestLogoutCommand(t *testing.T) {
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied)")
require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied: token wasn't created via login)")
})
testSessionID := testauth.StartSession()
@ -222,7 +222,7 @@ func TestLogoutCommand_k8s(t *testing.T) {
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied)")
require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied: token wasn't created via login)")
})
// go to the trouble of creating a login token

View File

@ -0,0 +1,48 @@
// Code generated by protoc-gen-go-binary. DO NOT EDIT.
// source: proto-public/pbacl/acl.proto
package pbacl
import (
"github.com/golang/protobuf/proto"
)
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *LoginRequest) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *LoginRequest) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *LoginResponse) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *LoginResponse) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *LoginToken) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *LoginToken) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *LogoutRequest) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *LogoutRequest) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}

View File

@ -0,0 +1,574 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.23.0
// protoc v3.15.8
// source: proto-public/pbacl/acl.proto
package pbacl
import (
context "context"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type LoginRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// auth_method is the name of the configured auth method that will be used to
// validate the presented bearer token.
AuthMethod string `protobuf:"bytes,1,opt,name=auth_method,json=authMethod,proto3" json:"auth_method,omitempty"`
// bearer_token is a token produced by a trusted identity provider as
// configured by the auth method.
BearerToken string `protobuf:"bytes,2,opt,name=bearer_token,json=bearerToken,proto3" json:"bearer_token,omitempty"`
// meta is a collection of arbitrary key-value pairs associated to the token,
// it is useful for tracking the origin of tokens.
Meta map[string]string `protobuf:"bytes,3,rep,name=meta,proto3" json:"meta,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
// namespace (enterprise only) is the namespace in which the auth method
// resides.
Namespace string `protobuf:"bytes,4,opt,name=namespace,proto3" json:"namespace,omitempty"`
// partition (enterprise only) is the partition in which the auth method
// resides.
Partition string `protobuf:"bytes,5,opt,name=partition,proto3" json:"partition,omitempty"`
// datacenter is the target datacenter in which the request will be processed.
Datacenter string `protobuf:"bytes,6,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
}
func (x *LoginRequest) Reset() {
*x = LoginRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_public_pbacl_acl_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LoginRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginRequest) ProtoMessage() {}
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_public_pbacl_acl_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead.
func (*LoginRequest) Descriptor() ([]byte, []int) {
return file_proto_public_pbacl_acl_proto_rawDescGZIP(), []int{0}
}
func (x *LoginRequest) GetAuthMethod() string {
if x != nil {
return x.AuthMethod
}
return ""
}
func (x *LoginRequest) GetBearerToken() string {
if x != nil {
return x.BearerToken
}
return ""
}
func (x *LoginRequest) GetMeta() map[string]string {
if x != nil {
return x.Meta
}
return nil
}
func (x *LoginRequest) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *LoginRequest) GetPartition() string {
if x != nil {
return x.Partition
}
return ""
}
func (x *LoginRequest) GetDatacenter() string {
if x != nil {
return x.Datacenter
}
return ""
}
type LoginResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// token is the generated ACL token.
Token *LoginToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
}
func (x *LoginResponse) Reset() {
*x = LoginResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_public_pbacl_acl_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LoginResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginResponse) ProtoMessage() {}
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_public_pbacl_acl_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead.
func (*LoginResponse) Descriptor() ([]byte, []int) {
return file_proto_public_pbacl_acl_proto_rawDescGZIP(), []int{1}
}
func (x *LoginResponse) GetToken() *LoginToken {
if x != nil {
return x.Token
}
return nil
}
type LoginToken struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// accessor_id is a UUID used to identify the ACL token.
AccessorId string `protobuf:"bytes,1,opt,name=accessor_id,json=accessorId,proto3" json:"accessor_id,omitempty"`
// secret_id is a UUID presented as a credential by clients.
SecretId string `protobuf:"bytes,2,opt,name=secret_id,json=secretId,proto3" json:"secret_id,omitempty"`
}
func (x *LoginToken) Reset() {
*x = LoginToken{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_public_pbacl_acl_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LoginToken) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginToken) ProtoMessage() {}
func (x *LoginToken) ProtoReflect() protoreflect.Message {
mi := &file_proto_public_pbacl_acl_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginToken.ProtoReflect.Descriptor instead.
func (*LoginToken) Descriptor() ([]byte, []int) {
return file_proto_public_pbacl_acl_proto_rawDescGZIP(), []int{2}
}
func (x *LoginToken) GetAccessorId() string {
if x != nil {
return x.AccessorId
}
return ""
}
func (x *LoginToken) GetSecretId() string {
if x != nil {
return x.SecretId
}
return ""
}
type LogoutRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// token is the ACL token's secret ID.
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
// datacenter is the target datacenter in which the request will be processed.
Datacenter string `protobuf:"bytes,2,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
}
func (x *LogoutRequest) Reset() {
*x = LogoutRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_public_pbacl_acl_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LogoutRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogoutRequest) ProtoMessage() {}
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_public_pbacl_acl_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead.
func (*LogoutRequest) Descriptor() ([]byte, []int) {
return file_proto_public_pbacl_acl_proto_rawDescGZIP(), []int{3}
}
func (x *LogoutRequest) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
func (x *LogoutRequest) GetDatacenter() string {
if x != nil {
return x.Datacenter
}
return ""
}
var File_proto_public_pbacl_acl_proto protoreflect.FileDescriptor
var file_proto_public_pbacl_acl_proto_rawDesc = []byte{
0x0a, 0x1c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x70,
0x62, 0x61, 0x63, 0x6c, 0x2f, 0x61, 0x63, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03,
0x61, 0x63, 0x6c, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x22, 0x98, 0x02, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68,
0x6f, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b,
0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2f, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x63, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79,
0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
0x61, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f,
0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69,
0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72,
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74,
0x65, 0x72, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x36, 0x0a, 0x0d, 0x4c,
0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x05,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x63,
0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f,
0x6b, 0x65, 0x6e, 0x22, 0x4a, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x6f, 0x6b, 0x65,
0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x5f, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72,
0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x49, 0x64, 0x22,
0x45, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x63, 0x65,
0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61,
0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x32, 0x76, 0x0a, 0x0a, 0x41, 0x43, 0x4c, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x11, 0x2e,
0x61, 0x63, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x12, 0x2e, 0x61, 0x63, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74,
0x12, 0x12, 0x2e, 0x61, 0x63, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x30,
0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73,
0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x61, 0x63, 0x6c,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_public_pbacl_acl_proto_rawDescOnce sync.Once
file_proto_public_pbacl_acl_proto_rawDescData = file_proto_public_pbacl_acl_proto_rawDesc
)
func file_proto_public_pbacl_acl_proto_rawDescGZIP() []byte {
file_proto_public_pbacl_acl_proto_rawDescOnce.Do(func() {
file_proto_public_pbacl_acl_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_public_pbacl_acl_proto_rawDescData)
})
return file_proto_public_pbacl_acl_proto_rawDescData
}
var file_proto_public_pbacl_acl_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_proto_public_pbacl_acl_proto_goTypes = []interface{}{
(*LoginRequest)(nil), // 0: acl.LoginRequest
(*LoginResponse)(nil), // 1: acl.LoginResponse
(*LoginToken)(nil), // 2: acl.LoginToken
(*LogoutRequest)(nil), // 3: acl.LogoutRequest
nil, // 4: acl.LoginRequest.MetaEntry
(*emptypb.Empty)(nil), // 5: google.protobuf.Empty
}
var file_proto_public_pbacl_acl_proto_depIdxs = []int32{
4, // 0: acl.LoginRequest.meta:type_name -> acl.LoginRequest.MetaEntry
2, // 1: acl.LoginResponse.token:type_name -> acl.LoginToken
0, // 2: acl.ACLService.Login:input_type -> acl.LoginRequest
3, // 3: acl.ACLService.Logout:input_type -> acl.LogoutRequest
1, // 4: acl.ACLService.Login:output_type -> acl.LoginResponse
5, // 5: acl.ACLService.Logout:output_type -> google.protobuf.Empty
4, // [4:6] is the sub-list for method output_type
2, // [2:4] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_proto_public_pbacl_acl_proto_init() }
func file_proto_public_pbacl_acl_proto_init() {
if File_proto_public_pbacl_acl_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_public_pbacl_acl_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LoginRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_public_pbacl_acl_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LoginResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_public_pbacl_acl_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LoginToken); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_public_pbacl_acl_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LogoutRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_public_pbacl_acl_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_public_pbacl_acl_proto_goTypes,
DependencyIndexes: file_proto_public_pbacl_acl_proto_depIdxs,
MessageInfos: file_proto_public_pbacl_acl_proto_msgTypes,
}.Build()
File_proto_public_pbacl_acl_proto = out.File
file_proto_public_pbacl_acl_proto_rawDesc = nil
file_proto_public_pbacl_acl_proto_goTypes = nil
file_proto_public_pbacl_acl_proto_depIdxs = nil
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// ACLServiceClient is the client API for ACLService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type ACLServiceClient interface {
// Login exchanges the presented bearer token for a Consul ACL token using a
// configured auth method.
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error)
// Logout destroys the given ACL token once the caller is done with it.
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type aCLServiceClient struct {
cc grpc.ClientConnInterface
}
func NewACLServiceClient(cc grpc.ClientConnInterface) ACLServiceClient {
return &aCLServiceClient{cc}
}
func (c *aCLServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
out := new(LoginResponse)
err := c.cc.Invoke(ctx, "/acl.ACLService/Login", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aCLServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/acl.ACLService/Logout", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ACLServiceServer is the server API for ACLService service.
type ACLServiceServer interface {
// Login exchanges the presented bearer token for a Consul ACL token using a
// configured auth method.
Login(context.Context, *LoginRequest) (*LoginResponse, error)
// Logout destroys the given ACL token once the caller is done with it.
Logout(context.Context, *LogoutRequest) (*emptypb.Empty, error)
}
// UnimplementedACLServiceServer can be embedded to have forward compatible implementations.
type UnimplementedACLServiceServer struct {
}
func (*UnimplementedACLServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
}
func (*UnimplementedACLServiceServer) Logout(context.Context, *LogoutRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented")
}
func RegisterACLServiceServer(s *grpc.Server, srv ACLServiceServer) {
s.RegisterService(&_ACLService_serviceDesc, srv)
}
func _ACLService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoginRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ACLServiceServer).Login(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/acl.ACLService/Login",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ACLServiceServer).Login(ctx, req.(*LoginRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ACLService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LogoutRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ACLServiceServer).Logout(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/acl.ACLService/Logout",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ACLServiceServer).Logout(ctx, req.(*LogoutRequest))
}
return interceptor(ctx, in, info, handler)
}
var _ACLService_serviceDesc = grpc.ServiceDesc{
ServiceName: "acl.ACLService",
HandlerType: (*ACLServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Login",
Handler: _ACLService_Login_Handler,
},
{
MethodName: "Logout",
Handler: _ACLService_Logout_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto-public/pbacl/acl.proto",
}

View File

@ -0,0 +1,62 @@
syntax = "proto3";
package acl;
import "google/protobuf/empty.proto";
option go_package = "github.com/hashicorp/consul/proto-public/pbacl";
service ACLService {
// Login exchanges the presented bearer token for a Consul ACL token using a
// configured auth method.
rpc Login(LoginRequest) returns (LoginResponse) {}
// Logout destroys the given ACL token once the caller is done with it.
rpc Logout(LogoutRequest) returns (google.protobuf.Empty) {}
}
message LoginRequest {
// auth_method is the name of the configured auth method that will be used to
// validate the presented bearer token.
string auth_method = 1;
// bearer_token is a token produced by a trusted identity provider as
// configured by the auth method.
string bearer_token = 2;
// meta is a collection of arbitrary key-value pairs associated to the token,
// it is useful for tracking the origin of tokens.
map<string, string> meta = 3;
// namespace (enterprise only) is the namespace in which the auth method
// resides.
string namespace = 4;
// partition (enterprise only) is the partition in which the auth method
// resides.
string partition = 5;
// datacenter is the target datacenter in which the request will be processed.
string datacenter = 6;
}
message LoginResponse {
// token is the generated ACL token.
LoginToken token = 1;
}
message LoginToken {
// accessor_id is a UUID used to identify the ACL token.
string accessor_id = 1;
// secret_id is a UUID presented as a credential by clients.
string secret_id = 2;
}
message LogoutRequest {
// token is the ACL token's secret ID.
string token = 1;
// datacenter is the target datacenter in which the request will be processed.
string datacenter = 2;
}