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:
parent
1cd73d7a71
commit
6bfdb48560
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
acl: It is now possible to login and logout using the gRPC API
|
||||||
|
```
|
|
@ -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)
|
||||||
|
}
|
|
@ -344,8 +344,6 @@ func (r *ACLResolver) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ACLResolver) fetchAndCacheIdentityFromToken(token string, cached *structs.IdentityCacheEntry) (structs.ACLIdentity, error) {
|
func (r *ACLResolver) fetchAndCacheIdentityFromToken(token string, cached *structs.IdentityCacheEntry) (structs.ACLIdentity, error) {
|
||||||
cacheID := tokenSecretCacheID(token)
|
|
||||||
|
|
||||||
req := structs.ACLTokenGetRequest{
|
req := structs.ACLTokenGetRequest{
|
||||||
Datacenter: r.backend.ACLDatacenter(),
|
Datacenter: r.backend.ACLDatacenter(),
|
||||||
TokenID: token,
|
TokenID: token,
|
||||||
|
@ -360,20 +358,20 @@ func (r *ACLResolver) fetchAndCacheIdentityFromToken(token string, cached *struc
|
||||||
err := r.backend.RPC("ACL.TokenRead", &req, &resp)
|
err := r.backend.RPC("ACL.TokenRead", &req, &resp)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if resp.Token == nil {
|
if resp.Token == nil {
|
||||||
r.cache.PutIdentity(cacheID, nil, nil)
|
r.cache.RemoveIdentityWithSecretToken(token)
|
||||||
return nil, acl.ErrNotFound
|
return nil, acl.ErrNotFound
|
||||||
} else if resp.Token.Local && r.config.Datacenter != resp.SourceDatacenter {
|
} 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)}
|
return nil, acl.PermissionDeniedError{Cause: fmt.Sprintf("This is a local token in datacenter %q", resp.SourceDatacenter)}
|
||||||
} else {
|
} else {
|
||||||
r.cache.PutIdentity(cacheID, resp.Token, nil)
|
r.cache.PutIdentityWithSecretToken(token, resp.Token)
|
||||||
return resp.Token, nil
|
return resp.Token, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if acl.IsErrNotFound(err) {
|
if acl.IsErrNotFound(err) {
|
||||||
// Make sure to remove from the cache if it was deleted
|
// 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
|
return nil, acl.ErrNotFound
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -381,11 +379,11 @@ func (r *ACLResolver) fetchAndCacheIdentityFromToken(token string, cached *struc
|
||||||
// some other RPC error
|
// some other RPC error
|
||||||
if cached != nil && (r.config.ACLDownPolicy == "extend-cache" || r.config.ACLDownPolicy == "async-cache") {
|
if cached != nil && (r.config.ACLDownPolicy == "extend-cache" || r.config.ACLDownPolicy == "async-cache") {
|
||||||
// extend the cache
|
// extend the cache
|
||||||
r.cache.PutIdentity(cacheID, cached.Identity, err)
|
r.cache.PutIdentityWithSecretToken(token, cached.Identity)
|
||||||
return cached.Identity, nil
|
return cached.Identity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r.cache.PutIdentity(cacheID, nil, err)
|
r.cache.RemoveIdentityWithSecretToken(token)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,13 +397,10 @@ func (r *ACLResolver) resolveIdentityFromToken(token string) (structs.ACLIdentit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the cache before making any RPC requests
|
// 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 {
|
if cacheEntry != nil && cacheEntry.Age() <= r.config.ACLTokenTTL {
|
||||||
metrics.IncrCounter([]string{"acl", "token", "cache_hit"}, 1)
|
metrics.IncrCounter([]string{"acl", "token", "cache_hit"}, 1)
|
||||||
if cacheEntry.Error != nil && !acl.IsErrNotFound(cacheEntry.Error) {
|
return cacheEntry.Identity, nil
|
||||||
return cacheEntry.Identity, ACLRemoteError{Err: cacheEntry.Error}
|
|
||||||
}
|
|
||||||
return cacheEntry.Identity, cacheEntry.Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.IncrCounter([]string{"acl", "token", "cache_miss"}, 1)
|
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"
|
waitForResult := cacheEntry == nil || r.config.ACLDownPolicy != "async-cache"
|
||||||
if !waitForResult {
|
if !waitForResult {
|
||||||
// waitForResult being false requires the cacheEntry to not be nil
|
// waitForResult being false requires the cacheEntry to not be nil
|
||||||
if cacheEntry.Error != nil && !acl.IsErrNotFound(cacheEntry.Error) {
|
return cacheEntry.Identity, nil
|
||||||
return cacheEntry.Identity, ACLRemoteError{Err: cacheEntry.Error}
|
|
||||||
}
|
|
||||||
return cacheEntry.Identity, cacheEntry.Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// block on the read here, this is why we don't need chan buffering
|
// 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) {
|
if acl.IsErrNotFound(err) {
|
||||||
// make sure to indicate that this identity is no longer valid within
|
// make sure to indicate that this identity is no longer valid within
|
||||||
// the cache
|
// 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
|
// Do not touch the cache. Getting a top level ACL not found error
|
||||||
// only indicates that the secret token used in the request
|
// 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) {
|
if acl.IsErrPermissionDenied(err) {
|
||||||
// invalidate our ID cache so that identity resolution will take place
|
// invalidate our ID cache so that identity resolution will take place
|
||||||
// again in the future
|
// again in the future
|
||||||
r.cache.RemoveIdentity(tokenSecretCacheID(identity.SecretToken()))
|
r.cache.RemoveIdentityWithSecretToken(identity.SecretToken())
|
||||||
|
|
||||||
// Do not remove from the cache for permission denied
|
// Do not remove from the cache for permission denied
|
||||||
// what this does indicate is that our view of the token is out of date
|
// 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 (
|
var (
|
||||||
policyIDs = identity.PolicyIDs()
|
policyIDs = identity.PolicyIDs()
|
||||||
roleIDs = identity.RoleIDs()
|
roleIDs = identity.RoleIDs()
|
||||||
serviceIdentities = identity.ServiceIdentityList()
|
serviceIdentities = structs.ACLServiceIdentities(identity.ServiceIdentityList())
|
||||||
nodeIdentities = identity.NodeIdentityList()
|
nodeIdentities = structs.ACLNodeIdentities(identity.NodeIdentityList())
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(policyIDs) == 0 && len(serviceIdentities) == 0 && len(roleIDs) == 0 && len(nodeIdentities) == 0 {
|
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.
|
// Now deduplicate any policies or service identities that occur more than once.
|
||||||
policyIDs = dedupeStringSlice(policyIDs)
|
policyIDs = dedupeStringSlice(policyIDs)
|
||||||
serviceIdentities = dedupeServiceIdentities(serviceIdentities)
|
serviceIdentities = serviceIdentities.Deduplicate()
|
||||||
nodeIdentities = dedupeNodeIdentities(nodeIdentities)
|
nodeIdentities = nodeIdentities.Deduplicate()
|
||||||
|
|
||||||
// Generate synthetic policies for all service identities in effect.
|
// Generate synthetic policies for all service identities in effect.
|
||||||
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities, identity.EnterpriseMetadata())
|
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities, identity.EnterpriseMetadata())
|
||||||
|
@ -690,72 +682,6 @@ func (r plainACLResolver) ResolveTokenAndDefaultMeta(
|
||||||
return r.resolver.ResolveTokenAndDefaultMeta(token, entMeta, authzContext)
|
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 {
|
func mergeStringSlice(a, b []string) []string {
|
||||||
out := make([]string, 0, len(a)+len(b))
|
out := make([]string, 0, len(a)+len(b))
|
||||||
out = append(out, a...)
|
out = append(out, a...)
|
||||||
|
|
|
@ -3,9 +3,6 @@ package consul
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/go-bexpr"
|
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
|
||||||
"github.com/hashicorp/consul/agent/consul/authmethod"
|
"github.com/hashicorp/consul/agent/consul/authmethod"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
|
||||||
|
@ -38,100 +35,3 @@ func (s *Server) loadAuthMethodValidator(idx uint64, method *structs.ACLAuthMeth
|
||||||
|
|
||||||
return v, nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,13 +2,11 @@ package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/armon/go-metrics"
|
"github.com/armon/go-metrics"
|
||||||
|
@ -19,11 +17,11 @@ import (
|
||||||
uuid "github.com/hashicorp/go-uuid"
|
uuid "github.com/hashicorp/go-uuid"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"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/authmethod"
|
||||||
"github.com/hashicorp/consul/agent/consul/state"
|
"github.com/hashicorp/consul/agent/consul/state"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/lib"
|
"github.com/hashicorp/consul/lib"
|
||||||
"github.com/hashicorp/consul/lib/template"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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
|
// ACL endpoint is used to manipulate ACLs
|
||||||
type ACL struct {
|
type ACL struct {
|
||||||
srv *Server
|
srv *Server
|
||||||
|
@ -472,26 +459,26 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
|
||||||
return fmt.Errorf("Cannot clone a legacy ACL with this endpoint")
|
return fmt.Errorf("Cannot clone a legacy ACL with this endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
cloneReq := structs.ACLTokenSetRequest{
|
clone := &structs.ACLToken{
|
||||||
Datacenter: args.Datacenter,
|
Policies: token.Policies,
|
||||||
ACLToken: structs.ACLToken{
|
Roles: token.Roles,
|
||||||
Policies: token.Policies,
|
ServiceIdentities: token.ServiceIdentities,
|
||||||
Roles: token.Roles,
|
NodeIdentities: token.NodeIdentities,
|
||||||
ServiceIdentities: token.ServiceIdentities,
|
Local: token.Local,
|
||||||
NodeIdentities: token.NodeIdentities,
|
Description: token.Description,
|
||||||
Local: token.Local,
|
ExpirationTime: token.ExpirationTime,
|
||||||
Description: token.Description,
|
EnterpriseMeta: args.ACLToken.EnterpriseMeta,
|
||||||
ExpirationTime: token.ExpirationTime,
|
|
||||||
EnterpriseMeta: args.ACLToken.EnterpriseMeta,
|
|
||||||
},
|
|
||||||
WriteRequest: args.WriteRequest,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.ACLToken.Description != "" {
|
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 {
|
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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.tokenSetInternal(args, reply, false)
|
var (
|
||||||
}
|
updated *structs.ACLToken
|
||||||
|
err error
|
||||||
func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.ACLToken, fromLogin bool) error {
|
)
|
||||||
token := &args.ACLToken
|
writer := a.srv.aclTokenWriter()
|
||||||
|
if args.ACLToken.AccessorID == "" || args.Create {
|
||||||
if !a.srv.LocalTokensEnabled() {
|
updated, err = writer.Create(&args.ACLToken, false)
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 {
|
} else {
|
||||||
// Token Update
|
updated, err = writer.Update(&args.ACLToken)
|
||||||
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{})
|
if err == nil {
|
||||||
var policies []structs.ACLTokenPolicyLink
|
*reply = *updated
|
||||||
|
|
||||||
// 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
|
return err
|
||||||
|
|
||||||
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 {
|
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
|
// 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 {
|
if reply != nil {
|
||||||
*reply = token.AccessorID
|
*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")
|
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")
|
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")
|
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")
|
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 == "" {
|
if svcid.ServiceName == "" {
|
||||||
return fmt.Errorf("Service identity is missing the service name field on this role")
|
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)
|
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 {
|
for _, nodeid := range role.NodeIdentities {
|
||||||
if nodeid.NodeName == "" {
|
if nodeid.NodeName == "" {
|
||||||
|
@ -1694,11 +1320,11 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
|
||||||
if nodeid.Datacenter == "" {
|
if nodeid.Datacenter == "" {
|
||||||
return fmt.Errorf("Node identity is missing the datacenter field on this role")
|
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")
|
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
|
// calculate the hash for this role
|
||||||
role.SetHash(true)
|
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)
|
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)
|
return fmt.Errorf("Invalid Binding Rule: invalid BindName: %v", err)
|
||||||
} else if !valid {
|
} else if !valid {
|
||||||
return fmt.Errorf("Invalid Binding Rule: invalid BindName")
|
return fmt.Errorf("Invalid Binding Rule: invalid BindName")
|
||||||
|
@ -2167,7 +1793,7 @@ func (a *ACL) AuthMethodRead(args *structs.ACLAuthMethodGetRequest, reply *struc
|
||||||
return errNotFound
|
return errNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = a.enterpriseAuthMethodTypeValidation(method.Type)
|
_ = a.srv.enterpriseAuthMethodTypeValidation(method.Type)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2207,11 +1833,11 @@ func (a *ACL) AuthMethodSet(args *structs.ACLAuthMethodSetRequest, reply *struct
|
||||||
if method.Name == "" {
|
if method.Name == "" {
|
||||||
return fmt.Errorf("Invalid Auth Method: no Name is set")
|
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")
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2321,7 +1947,7 @@ func (a *ACL) AuthMethodDelete(args *structs.ACLAuthMethodDeleteRequest, reply *
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.enterpriseAuthMethodTypeValidation(method.Type); err != nil {
|
if err := a.srv.enterpriseAuthMethodTypeValidation(method.Type); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2377,7 +2003,7 @@ func (a *ACL) AuthMethodList(args *structs.ACLAuthMethodListRequest, reply *stru
|
||||||
|
|
||||||
var stubs structs.ACLAuthMethodListStubs
|
var stubs structs.ACLAuthMethodListStubs
|
||||||
for _, method := range methods {
|
for _, method := range methods {
|
||||||
_ = a.enterpriseAuthMethodTypeValidation(method.Type)
|
_ = a.srv.enterpriseAuthMethodTypeValidation(method.Type)
|
||||||
stubs = append(stubs, method.Stub())
|
stubs = append(stubs, method.Stub())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2413,132 +2039,28 @@ func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLToken) erro
|
||||||
|
|
||||||
defer metrics.MeasureSince([]string{"acl", "login"}, time.Now())
|
defer metrics.MeasureSince([]string{"acl", "login"}, time.Now())
|
||||||
|
|
||||||
auth := args.Auth
|
authMethod, validator, err := a.srv.loadAuthMethod(args.Auth.AuthMethod, &args.Auth.EnterpriseMeta)
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Send args.Data.BearerToken to method validator and get back a fields map
|
verifiedIdentity, err := validator.ValidateLogin(context.Background(), args.Auth.BearerToken)
|
||||||
verifiedIdentity, err := validator.ValidateLogin(context.Background(), auth.BearerToken)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.tokenSetFromAuthMethod(
|
description, err := auth.BuildTokenDescription("token created via login", args.Auth.Meta)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. send map through role bindings
|
token, err := a.srv.aclLogin().TokenForVerifiedIdentity(verifiedIdentity, authMethod, description)
|
||||||
bindings, err := a.srv.evaluateRoleBindings(validator, verifiedIdentity, entMeta, targetMeta)
|
if err == nil {
|
||||||
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
|
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 {
|
func (a *ACL) Logout(args *structs.ACLLogoutRequest, reply *bool) error {
|
||||||
if err := a.aclPreCheck(); err != nil {
|
if err := a.aclPreCheck(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -2558,39 +2080,18 @@ func (a *ACL) Logout(args *structs.ACLLogoutRequest, reply *bool) error {
|
||||||
|
|
||||||
defer metrics.MeasureSince([]string{"acl", "logout"}, time.Now())
|
defer metrics.MeasureSince([]string{"acl", "logout"}, time.Now())
|
||||||
|
|
||||||
_, token, err := a.srv.fsm.State().ACLTokenGetBySecret(nil, args.Token, nil)
|
// No need to check expiration time because it's being deleted.
|
||||||
if err != nil {
|
err := a.srv.aclTokenWriter().Delete(args.Token, true)
|
||||||
return err
|
switch {
|
||||||
|
case errors.Is(err, auth.ErrCannotWriteGlobalToken):
|
||||||
} else if token == nil {
|
// Writes to global tokens must be forwarded to the primary DC.
|
||||||
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
|
|
||||||
args.Datacenter = a.srv.config.PrimaryDatacenter
|
args.Datacenter = a.srv.config.PrimaryDatacenter
|
||||||
return a.srv.forwardDC("ACL.Logout", a.srv.config.PrimaryDatacenter, args, reply)
|
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
|
*reply = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,21 +27,10 @@ func (a *ACL) roleUpsertValidateEnterprise(role *structs.ACLRole, existing *stru
|
||||||
return state.ACLRoleUpsertValidateEnterprise(role, existing)
|
return state.ACLRoleUpsertValidateEnterprise(role, existing)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ACL) enterpriseAuthMethodTypeValidation(authMethodType string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func enterpriseAuthMethodValidation(method *structs.ACLAuthMethod, validator authmethod.Validator) error {
|
func enterpriseAuthMethodValidation(method *structs.ACLAuthMethod, validator authmethod.Validator) error {
|
||||||
return nil
|
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) {
|
func getTokenNamespaceDefaults(ws memdb.WatchSet, state *state.Store, entMeta *acl.EnterpriseMeta) ([]string, []string, error) {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -478,7 +478,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
}, false)
|
}, false)
|
||||||
waitForLeaderEstablishment(t, srv)
|
waitForLeaderEstablishment(t, srv)
|
||||||
|
|
||||||
acl := ACL{srv: srv}
|
a := ACL{srv: srv}
|
||||||
|
|
||||||
var tokenID string
|
var tokenID string
|
||||||
|
|
||||||
|
@ -501,7 +501,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the token directly to validate that it exists
|
// Get the token directly to validate that it exists
|
||||||
|
@ -532,7 +532,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the token directly to validate that it exists
|
// Get the token directly to validate that it exists
|
||||||
|
@ -572,7 +572,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err = acl.TokenSet(&req, &resp)
|
err = a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Delete both policies to ensure that we skip resolving ID->Name
|
// Delete both policies to ensure that we skip resolving ID->Name
|
||||||
|
@ -618,7 +618,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err = acl.TokenSet(&req, &resp)
|
err = a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Delete both roles to ensure that we skip resolving ID->Name
|
// Delete both roles to ensure that we skip resolving ID->Name
|
||||||
|
@ -651,8 +651,8 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
testutil.RequireErrorContains(t, err, "AuthMethod field is disallowed outside of Login")
|
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) {
|
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{}
|
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")
|
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) {
|
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{
|
req := structs.ACLTokenSetRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
ACLToken: structs.ACLToken{
|
ACLToken: structs.ACLToken{
|
||||||
|
@ -788,7 +788,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -834,7 +834,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
if test.ok {
|
if test.ok {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -867,7 +867,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the token directly to validate that it exists
|
// Get the token directly to validate that it exists
|
||||||
|
@ -901,7 +901,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the token directly to validate that it exists
|
// Get the token directly to validate that it exists
|
||||||
|
@ -931,7 +931,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
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")
|
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{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
if test.errString != "" {
|
if test.errString != "" {
|
||||||
testutil.RequireErrorContains(t, err, test.errString)
|
testutil.RequireErrorContains(t, err, test.errString)
|
||||||
} else {
|
} else {
|
||||||
|
@ -981,7 +981,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
if test.errString != "" {
|
if test.errString != "" {
|
||||||
testutil.RequireErrorContains(t, err, test.errStringTTL)
|
testutil.RequireErrorContains(t, err, test.errStringTTL)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1005,7 +1005,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
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")
|
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{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the token directly to validate that it exists
|
// Get the token directly to validate that it exists
|
||||||
|
@ -1058,7 +1058,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the token directly to validate that it exists
|
// Get the token directly to validate that it exists
|
||||||
|
@ -1090,7 +1090,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
testutil.RequireErrorContains(t, err, "Cannot change expiration time")
|
testutil.RequireErrorContains(t, err, "Cannot change expiration time")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1108,7 +1108,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the token directly to validate that it exists
|
// Get the token directly to validate that it exists
|
||||||
|
@ -1136,7 +1136,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the token directly to validate that it exists
|
// Get the token directly to validate that it exists
|
||||||
|
@ -1172,7 +1172,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err = acl.TokenSet(&req, &resp)
|
err = a.TokenSet(&req, &resp)
|
||||||
testutil.RequireErrorContains(t, err, "Cannot find token")
|
testutil.RequireErrorContains(t, err, "Cannot find token")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1191,7 +1191,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
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")
|
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{}
|
resp := structs.ACLToken{}
|
||||||
|
|
||||||
err := acl.TokenSet(&req, &resp)
|
err := a.TokenSet(&req, &resp)
|
||||||
testutil.RequireErrorContains(t, err, "Node identity has an invalid name.")
|
testutil.RequireErrorContains(t, err, "Node identity has an invalid name.")
|
||||||
})
|
})
|
||||||
t.Run("invalid node identity - no datacenter", func(t *testing.T) {
|
t.Run("invalid node identity - no datacenter", func(t *testing.T) {
|
||||||
|
@ -1229,7 +1229,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLToken{}
|
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")
|
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)
|
_, srv, codec := testACLServerWithConfig(t, nil, false)
|
||||||
waitForLeaderEstablishment(t, srv)
|
waitForLeaderEstablishment(t, srv)
|
||||||
|
|
||||||
acl := ACL{srv: srv}
|
a := ACL{srv: srv}
|
||||||
var roleID string
|
var roleID string
|
||||||
|
|
||||||
testPolicy1, err := upsertTestPolicy(codec, TestDefaultInitialManagementToken, "dc1")
|
testPolicy1, err := upsertTestPolicy(codec, TestDefaultInitialManagementToken, "dc1")
|
||||||
|
@ -2419,7 +2419,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
}
|
}
|
||||||
resp := structs.ACLRole{}
|
resp := structs.ACLRole{}
|
||||||
|
|
||||||
err := acl.RoleSet(&req, &resp)
|
err := a.RoleSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, resp.ID)
|
require.NotNil(t, resp.ID)
|
||||||
|
|
||||||
|
@ -2457,7 +2457,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
}
|
}
|
||||||
resp := structs.ACLRole{}
|
resp := structs.ACLRole{}
|
||||||
|
|
||||||
err := acl.RoleSet(&req, &resp)
|
err := a.RoleSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, resp.ID)
|
require.NotNil(t, resp.ID)
|
||||||
|
|
||||||
|
@ -2498,7 +2498,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
}
|
}
|
||||||
resp := structs.ACLRole{}
|
resp := structs.ACLRole{}
|
||||||
|
|
||||||
err = acl.RoleSet(&req, &resp)
|
err = a.RoleSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, resp.ID)
|
require.NotNil(t, resp.ID)
|
||||||
|
|
||||||
|
@ -2540,12 +2540,12 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
}
|
}
|
||||||
resp := structs.ACLRole{}
|
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")
|
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) {
|
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{
|
req := structs.ACLRoleSetRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Role: structs.ACLRole{
|
Role: structs.ACLRole{
|
||||||
|
@ -2559,7 +2559,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
}
|
}
|
||||||
resp := structs.ACLRole{}
|
resp := structs.ACLRole{}
|
||||||
|
|
||||||
err := acl.RoleSet(&req, &resp)
|
err := a.RoleSet(&req, &resp)
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2604,7 +2604,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLRole{}
|
resp := structs.ACLRole{}
|
||||||
|
|
||||||
err := acl.RoleSet(&req, &resp)
|
err := a.RoleSet(&req, &resp)
|
||||||
if test.ok {
|
if test.ok {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -2635,7 +2635,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLRole{}
|
resp := structs.ACLRole{}
|
||||||
|
|
||||||
err := acl.RoleSet(&req, &resp)
|
err := a.RoleSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the role directly to validate that it exists
|
// Get the role directly to validate that it exists
|
||||||
|
@ -2667,7 +2667,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLRole{}
|
resp := structs.ACLRole{}
|
||||||
|
|
||||||
err := acl.RoleSet(&req, &resp)
|
err := a.RoleSet(&req, &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the role directly to validate that it exists
|
// Get the role directly to validate that it exists
|
||||||
|
@ -2696,7 +2696,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLRole{}
|
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")
|
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{}
|
resp := structs.ACLRole{}
|
||||||
|
|
||||||
err := acl.RoleSet(&req, &resp)
|
err := a.RoleSet(&req, &resp)
|
||||||
testutil.RequireErrorContains(t, err, "Node identity has an invalid name.")
|
testutil.RequireErrorContains(t, err, "Node identity has an invalid name.")
|
||||||
})
|
})
|
||||||
t.Run("invalid node identity - no datacenter", func(t *testing.T) {
|
t.Run("invalid node identity - no datacenter", func(t *testing.T) {
|
||||||
|
@ -2736,7 +2736,7 @@ func TestACLEndpoint_RoleSet(t *testing.T) {
|
||||||
|
|
||||||
resp := structs.ACLRole{}
|
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")
|
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
|
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
|
// upsertTestToken creates a token for testing purposes
|
||||||
func upsertTestTokenInEntMeta(codec rpc.ClientCodec, initialManagementToken string, datacenter string,
|
func upsertTestTokenInEntMeta(codec rpc.ClientCodec, initialManagementToken string, datacenter string,
|
||||||
tokenModificationFn func(token *structs.ACLToken), entMeta *acl.EnterpriseMeta) (*structs.ACLToken, error) {
|
tokenModificationFn func(token *structs.ACLToken), entMeta *acl.EnterpriseMeta) (*structs.ACLToken, error) {
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package consul
|
package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"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"
|
"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{}) {
|
func (s *Server) filterACLWithAuthorizer(authorizer acl.Authorizer, subj interface{}) {
|
||||||
filterACLWithAuthorizer(s.ACLResolver.logger, authorizer, subj)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -19,3 +19,7 @@ func (s *Server) validateEnterpriseToken(identity structs.ACLIdentity) error {
|
||||||
func (s *Server) aclBootstrapAllowed() error {
|
func (s *Server) aclBootstrapAllowed() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*Server) enterpriseAuthMethodTypeValidation(authMethodType string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -768,15 +768,15 @@ func TestACLResolver_ResolveRootACL(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestACLResolver_DownPolicy(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()
|
t.Helper()
|
||||||
|
|
||||||
cacheVal := r.cache.GetIdentity(id)
|
cacheVal := r.cache.GetIdentityWithSecretToken(secretID)
|
||||||
require.NotNil(t, cacheVal)
|
|
||||||
if present {
|
if present {
|
||||||
|
require.NotNil(t, cacheVal, msg)
|
||||||
require.NotNil(t, cacheVal.Identity, msg)
|
require.NotNil(t, cacheVal.Identity, msg)
|
||||||
} else {
|
} 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) {
|
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)
|
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) {
|
t.Run("Allow", func(t *testing.T) {
|
||||||
|
@ -844,7 +844,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
|
||||||
}
|
}
|
||||||
require.Equal(t, expected, authz)
|
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) {
|
t.Run("Expired-Policy", func(t *testing.T) {
|
||||||
|
@ -935,7 +935,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
|
||||||
require.NotNil(t, authz)
|
require.NotNil(t, authz)
|
||||||
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
|
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")
|
authz2, err := r.ResolveToken("found")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -986,7 +986,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
|
||||||
require.NotNil(t, authz)
|
require.NotNil(t, authz)
|
||||||
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
|
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")
|
authz2, err := r.ResolveToken("found-role")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -1245,7 +1245,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
|
||||||
require.NotNil(t, authz)
|
require.NotNil(t, authz)
|
||||||
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
|
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
|
// The identity should have been cached so this should still be valid
|
||||||
authz2, err := r.ResolveToken("found")
|
authz2, err := r.ResolveToken("found")
|
||||||
|
@ -1261,45 +1261,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
|
||||||
assert.True(t, acl.IsErrNotFound(err))
|
assert.True(t, acl.IsErrNotFound(err))
|
||||||
})
|
})
|
||||||
|
|
||||||
requireIdentityCached(t, r, tokenSecretCacheID("found"), false, "no longer cached")
|
requireIdentityCached(t, r, "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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("PolicyResolve-TokenNotFound", func(t *testing.T) {
|
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))
|
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
|
||||||
|
|
||||||
// Verify that the caches are setup properly.
|
// 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, "node-wr", true, "cached") // from "found" token
|
||||||
requirePolicyCached(t, r, "dc2-key-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)
|
_, err = r.ResolveToken(secretID)
|
||||||
require.True(t, acl.IsErrNotFound(err))
|
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")
|
requirePolicyCached(t, r, "node-wr", true, "still cached")
|
||||||
require.Nil(t, r.cache.GetPolicy("dc2-key-wr"), "not stored at all")
|
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))
|
require.Equal(t, acl.Allow, authz.NodeWrite("foo", nil))
|
||||||
|
|
||||||
// Verify that the caches are setup properly.
|
// 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, "node-wr", true, "cached") // from "found" token
|
||||||
requirePolicyCached(t, r, "dc2-key-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)
|
_, err = r.ResolveToken(secretID)
|
||||||
require.True(t, acl.IsErrPermissionDenied(err))
|
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")
|
requirePolicyCached(t, r, "node-wr", true, "still cached")
|
||||||
require.Nil(t, r.cache.GetPolicy("dc2-key-wr"), "not stored at all")
|
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{})
|
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) {
|
func TestACL_LocalToken(t *testing.T) {
|
||||||
t.Run("local token in same dc", func(t *testing.T) {
|
t.Run("local token in same dc", func(t *testing.T) {
|
||||||
d := &ACLResolverTestDelegate{
|
d := &ACLResolverTestDelegate{
|
||||||
|
|
|
@ -108,7 +108,7 @@ func (s *Server) reapExpiredACLTokens(local, global bool) (int, error) {
|
||||||
|
|
||||||
// Purge the identities from the cache
|
// Purge the identities from the cache
|
||||||
for _, secretID := range secretIDs {
|
for _, secretID := range secretIDs {
|
||||||
s.ACLResolver.cache.RemoveIdentity(tokenSecretCacheID(secretID))
|
s.ACLResolver.cache.RemoveIdentityWithSecretToken(secretID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return len(req.TokenIDs), nil
|
return len(req.TokenIDs), nil
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -8,7 +8,11 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/connect"
|
"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/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/pbconnectca"
|
||||||
"github.com/hashicorp/consul/proto-public/pbserverdiscovery"
|
"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.
|
// * Making a request to a follower's public gRPC port.
|
||||||
// * Ensuring that the request is correctly forwarded to the leader.
|
// * Ensuring that the request is correctly forwarded to the leader.
|
||||||
// * Ensuring we get a valid certificate back (so it went through the CAManager).
|
// * 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.Bootstrap = false
|
||||||
c.BootstrapExpect = 2
|
c.BootstrapExpect = 2
|
||||||
})
|
})
|
||||||
|
|
||||||
server2, conn2 := testGRPCIntegrationServer(t, func(c *Config) {
|
server2, conn2, _ := testGRPCIntegrationServer(t, func(c *Config) {
|
||||||
c.Bootstrap = false
|
c.Bootstrap = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -81,7 +85,7 @@ func TestGRPCIntegration_ServerDiscovery_WatchServers(t *testing.T) {
|
||||||
// * Adding another server
|
// * Adding another server
|
||||||
// * Validating another message is sent.
|
// * Validating another message is sent.
|
||||||
|
|
||||||
server1, conn := testGRPCIntegrationServer(t, func(c *Config) {
|
server1, conn, _ := testGRPCIntegrationServer(t, func(c *Config) {
|
||||||
c.Bootstrap = true
|
c.Bootstrap = true
|
||||||
c.BootstrapExpect = 1
|
c.BootstrapExpect = 1
|
||||||
})
|
})
|
||||||
|
@ -115,3 +119,97 @@ func TestGRPCIntegration_ServerDiscovery_WatchServers(t *testing.T) {
|
||||||
require.NotNil(t, rsp)
|
require.NotNil(t, rsp)
|
||||||
require.Len(t, rsp.Servers, 2)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/armon/go-metrics"
|
"github.com/armon/go-metrics"
|
||||||
"github.com/hashicorp/consul-net-rpc/net/rpc"
|
|
||||||
connlimit "github.com/hashicorp/go-connlimit"
|
connlimit "github.com/hashicorp/go-connlimit"
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
|
@ -30,6 +29,8 @@ import (
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul-net-rpc/net/rpc"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/consul/authmethod"
|
"github.com/hashicorp/consul/agent/consul/authmethod"
|
||||||
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
|
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
|
||||||
|
@ -40,6 +41,7 @@ import (
|
||||||
"github.com/hashicorp/consul/agent/consul/wanfed"
|
"github.com/hashicorp/consul/agent/consul/wanfed"
|
||||||
agentgrpc "github.com/hashicorp/consul/agent/grpc/private"
|
agentgrpc "github.com/hashicorp/consul/agent/grpc/private"
|
||||||
"github.com/hashicorp/consul/agent/grpc/private/services/subscribe"
|
"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/connectca"
|
||||||
"github.com/hashicorp/consul/agent/grpc/public/services/dataplane"
|
"github.com/hashicorp/consul/agent/grpc/public/services/dataplane"
|
||||||
"github.com/hashicorp/consul/agent/grpc/public/services/serverdiscovery"
|
"github.com/hashicorp/consul/agent/grpc/public/services/serverdiscovery"
|
||||||
|
@ -239,6 +241,11 @@ type Server struct {
|
||||||
// is only ever closed.
|
// is only ever closed.
|
||||||
leaveCh chan struct{}
|
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
|
// publicConnectCAServer serves the Connect CA service exposed on the public
|
||||||
// gRPC port. It is also exposed on the private multiplexed "server" port to
|
// gRPC port. It is also exposed on the private multiplexed "server" port to
|
||||||
// enable RPC forwarding.
|
// 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})
|
go s.overviewManager.Run(&lib.StopChannelContext{StopCh: s.shutdownCh})
|
||||||
|
|
||||||
// Initialize public gRPC server - register services on public gRPC server.
|
// 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{
|
s.publicConnectCAServer = connectca.NewServer(connectca.Config{
|
||||||
Publisher: s.publisher,
|
Publisher: s.publisher,
|
||||||
GetStore: func() connectca.StateStore { return s.FSM().State() },
|
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)
|
pbpeering.RegisterPeeringServiceServer(srv, s.peeringService)
|
||||||
s.registerEnterpriseGRPCServices(deps, srv)
|
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.
|
// enable RPC forwarding.
|
||||||
|
s.publicACLServer.Register(srv)
|
||||||
s.publicConnectCAServer.Register(srv)
|
s.publicConnectCAServer.Register(srv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -259,8 +259,8 @@ func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToke
|
||||||
return dir, srv, codec
|
return dir, srv, codec
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGRPCIntegrationServer(t *testing.T, cb func(*Config)) (*Server, *grpc.ClientConn) {
|
func testGRPCIntegrationServer(t *testing.T, cb func(*Config)) (*Server, *grpc.ClientConn, rpc.ClientCodec) {
|
||||||
_, srv, _ := testACLServerWithConfig(t, cb, false)
|
_, srv, codec := testACLServerWithConfig(t, cb, false)
|
||||||
|
|
||||||
// Normally the gRPC server listener is created at the agent level and passed down into
|
// 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
|
// 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() })
|
t.Cleanup(func() { _ = conn.Close() })
|
||||||
|
|
||||||
return srv, conn
|
return srv, conn, codec
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServer(t *testing.T, c *Config) (*Server, error) {
|
func newServer(t *testing.T, c *Config) (*Server, error) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -169,6 +169,34 @@ func (s *ACLServiceIdentity) SyntheticPolicy(entMeta *acl.EnterpriseMeta) *ACLPo
|
||||||
return policy
|
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
|
// ACLNodeIdentity represents a high-level grant of all privileges
|
||||||
// necessary to assume the identity of that node and manage it.
|
// necessary to assume the identity of that node and manage it.
|
||||||
type ACLNodeIdentity struct {
|
type ACLNodeIdentity struct {
|
||||||
|
@ -213,6 +241,27 @@ func (s *ACLNodeIdentity) SyntheticPolicy(entMeta *acl.EnterpriseMeta) *ACLPolic
|
||||||
return policy
|
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 {
|
type ACLToken struct {
|
||||||
// This is the UUID used for tracking and management purposes
|
// This is the UUID used for tracking and management purposes
|
||||||
AccessorID string
|
AccessorID string
|
||||||
|
@ -234,10 +283,10 @@ type ACLToken struct {
|
||||||
Roles []ACLTokenRoleLink `json:",omitempty"`
|
Roles []ACLTokenRoleLink `json:",omitempty"`
|
||||||
|
|
||||||
// List of services to generate synthetic policies for.
|
// 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.
|
// 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
|
// Type is the V1 Token Type
|
||||||
// DEPRECATED (ACL-Legacy-Compat) - remove once we no longer support v1 ACL compat
|
// DEPRECATED (ACL-Legacy-Compat) - remove once we no longer support v1 ACL compat
|
||||||
|
@ -497,10 +546,10 @@ type ACLTokenListStub struct {
|
||||||
AccessorID string
|
AccessorID string
|
||||||
SecretID string
|
SecretID string
|
||||||
Description string
|
Description string
|
||||||
Policies []ACLTokenPolicyLink `json:",omitempty"`
|
Policies []ACLTokenPolicyLink `json:",omitempty"`
|
||||||
Roles []ACLTokenRoleLink `json:",omitempty"`
|
Roles []ACLTokenRoleLink `json:",omitempty"`
|
||||||
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
|
ServiceIdentities ACLServiceIdentities `json:",omitempty"`
|
||||||
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
|
NodeIdentities ACLNodeIdentities `json:",omitempty"`
|
||||||
Local bool
|
Local bool
|
||||||
AuthMethod string `json:",omitempty"`
|
AuthMethod string `json:",omitempty"`
|
||||||
ExpirationTime *time.Time `json:",omitempty"`
|
ExpirationTime *time.Time `json:",omitempty"`
|
||||||
|
@ -808,10 +857,10 @@ type ACLRole struct {
|
||||||
Policies []ACLRolePolicyLink `json:",omitempty"`
|
Policies []ACLRolePolicyLink `json:",omitempty"`
|
||||||
|
|
||||||
// List of services to generate synthetic policies for.
|
// List of services to generate synthetic policies for.
|
||||||
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
|
ServiceIdentities ACLServiceIdentities `json:",omitempty"`
|
||||||
|
|
||||||
// List of nodes to generate synthetic policies for.
|
// List of nodes to generate synthetic policies for.
|
||||||
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
|
NodeIdentities ACLNodeIdentities `json:",omitempty"`
|
||||||
|
|
||||||
// Hash of the contents of the role
|
// Hash of the contents of the role
|
||||||
// This does not take into account the ID (which is immutable)
|
// This does not take into account the ID (which is immutable)
|
||||||
|
|
|
@ -27,7 +27,6 @@ type ACLCaches struct {
|
||||||
type IdentityCacheEntry struct {
|
type IdentityCacheEntry struct {
|
||||||
Identity ACLIdentity
|
Identity ACLIdentity
|
||||||
CacheTime time.Time
|
CacheTime time.Time
|
||||||
Error error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *IdentityCacheEntry) Age() time.Duration {
|
func (e *IdentityCacheEntry) Age() time.Duration {
|
||||||
|
@ -135,6 +134,12 @@ func (c *ACLCaches) GetIdentity(id string) *IdentityCacheEntry {
|
||||||
return nil
|
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
|
// GetPolicy fetches a policy from the cache and returns it
|
||||||
func (c *ACLCaches) GetPolicy(policyID string) *PolicyCacheEntry {
|
func (c *ACLCaches) GetPolicy(policyID string) *PolicyCacheEntry {
|
||||||
if c == nil || c.policies == nil {
|
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
|
// 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 {
|
if c == nil || c.identities == nil {
|
||||||
return
|
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) {
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -47,10 +47,18 @@ func TestStructs_ACLCaches(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, cache)
|
require.NotNil(t, cache)
|
||||||
|
|
||||||
cache.PutIdentity("foo", &ACLToken{}, nil)
|
cache.PutIdentity("foo", &ACLToken{})
|
||||||
entry := cache.GetIdentity("foo")
|
entry := cache.GetIdentity("foo")
|
||||||
require.NotNil(t, entry)
|
require.NotNil(t, entry)
|
||||||
require.NotNil(t, entry.Identity)
|
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) {
|
t.Run("Policies", func(t *testing.T) {
|
||||||
|
|
|
@ -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) {
|
func TestStructs_ACLToken_SetHash(t *testing.T) {
|
||||||
|
|
||||||
token := ACLToken{
|
token := ACLToken{
|
||||||
|
|
|
@ -18,11 +18,12 @@ import (
|
||||||
"github.com/golang/protobuf/ptypes"
|
"github.com/golang/protobuf/ptypes"
|
||||||
"github.com/golang/protobuf/ptypes/duration"
|
"github.com/golang/protobuf/ptypes/duration"
|
||||||
"github.com/golang/protobuf/ptypes/timestamp"
|
"github.com/golang/protobuf/ptypes/timestamp"
|
||||||
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/hashicorp/serf/coordinate"
|
"github.com/hashicorp/serf/coordinate"
|
||||||
"github.com/mitchellh/hashstructure"
|
"github.com/mitchellh/hashstructure"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/cache"
|
"github.com/hashicorp/consul/agent/cache"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
|
|
|
@ -92,7 +92,7 @@ func TestLogoutCommand(t *testing.T) {
|
||||||
|
|
||||||
code := cmd.Run(args)
|
code := cmd.Run(args)
|
||||||
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
|
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()
|
testSessionID := testauth.StartSession()
|
||||||
|
@ -222,7 +222,7 @@ func TestLogoutCommand_k8s(t *testing.T) {
|
||||||
|
|
||||||
code := cmd.Run(args)
|
code := cmd.Run(args)
|
||||||
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
|
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
|
// go to the trouble of creating a login token
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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",
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue