package agent
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
pkg refactor
command/agent/* -> agent/*
command/consul/* -> agent/consul/*
command/agent/command{,_test}.go -> command/agent{,_test}.go
command/base/command.go -> command/base.go
command/base/* -> command/*
commands.go -> command/commands.go
The script which did the refactor is:
(
cd $GOPATH/src/github.com/hashicorp/consul
git mv command/agent/command.go command/agent.go
git mv command/agent/command_test.go command/agent_test.go
git mv command/agent/flag_slice_value{,_test}.go command/
git mv command/agent .
git mv command/base/command.go command/base.go
git mv command/base/config_util{,_test}.go command/
git mv commands.go command/
git mv consul agent
rmdir command/base/
gsed -i -e 's|package agent|package command|' command/agent{,_test}.go
gsed -i -e 's|package agent|package command|' command/flag_slice_value{,_test}.go
gsed -i -e 's|package base|package command|' command/base.go command/config_util{,_test}.go
gsed -i -e 's|package main|package command|' command/commands.go
gsed -i -e 's|base.Command|BaseCommand|' command/commands.go
gsed -i -e 's|agent.Command|AgentCommand|' command/commands.go
gsed -i -e 's|\tCommand:|\tBaseCommand:|' command/commands.go
gsed -i -e 's|base\.||' command/commands.go
gsed -i -e 's|command\.||' command/commands.go
gsed -i -e 's|command|c|' main.go
gsed -i -e 's|range Commands|range command.Commands|' main.go
gsed -i -e 's|Commands: Commands|Commands: command.Commands|' main.go
gsed -i -e 's|base\.BoolValue|BoolValue|' command/operator_autopilot_set.go
gsed -i -e 's|base\.DurationValue|DurationValue|' command/operator_autopilot_set.go
gsed -i -e 's|base\.StringValue|StringValue|' command/operator_autopilot_set.go
gsed -i -e 's|base\.UintValue|UintValue|' command/operator_autopilot_set.go
gsed -i -e 's|\bCommand\b|BaseCommand|' command/base.go
gsed -i -e 's|BaseCommand Options|Command Options|' command/base.go
gsed -i -e 's|base.Command|BaseCommand|' command/*.go
gsed -i -e 's|c\.Command|c.BaseCommand|g' command/*.go
gsed -i -e 's|\tCommand:|\tBaseCommand:|' command/*_test.go
gsed -i -e 's|base\.||' command/*_test.go
gsed -i -e 's|\bCommand\b|AgentCommand|' command/agent{,_test}.go
gsed -i -e 's|cmd.AgentCommand|cmd.BaseCommand|' command/agent.go
gsed -i -e 's|cli.AgentCommand = new(Command)|cli.Command = new(AgentCommand)|' command/agent_test.go
gsed -i -e 's|exec.AgentCommand|exec.Command|' command/agent_test.go
gsed -i -e 's|exec.BaseCommand|exec.Command|' command/agent_test.go
gsed -i -e 's|NewTestAgent|agent.NewTestAgent|' command/agent_test.go
gsed -i -e 's|= TestConfig|= agent.TestConfig|' command/agent_test.go
gsed -i -e 's|: RetryJoin|: agent.RetryJoin|' command/agent_test.go
gsed -i -e 's|\.\./\.\./|../|' command/config_util_test.go
gsed -i -e 's|\bverifyUniqueListeners|VerifyUniqueListeners|' agent/config{,_test}.go command/agent.go
gsed -i -e 's|\bserfLANKeyring\b|SerfLANKeyring|g' agent/{agent,keyring,testagent}.go command/agent.go
gsed -i -e 's|\bserfWANKeyring\b|SerfWANKeyring|g' agent/{agent,keyring,testagent}.go command/agent.go
gsed -i -e 's|\bNewAgent\b|agent.New|g' command/agent{,_test}.go
gsed -i -e 's|\bNewAgent|New|' agent/{acl_test,agent,testagent}.go
gsed -i -e 's|\bAgent\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bBool\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bConfig\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bDefaultConfig\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bDevConfig\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bMergeConfig\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bReadConfigPaths\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bParseMetaPair\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bSerfLANKeyring\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|\bSerfWANKeyring\b|agent.&|g' command/agent{,_test}.go
gsed -i -e 's|circonus\.agent|circonus|g' command/agent{,_test}.go
gsed -i -e 's|logger\.agent|logger|g' command/agent{,_test}.go
gsed -i -e 's|metrics\.agent|metrics|g' command/agent{,_test}.go
gsed -i -e 's|// agent.Agent|// agent|' command/agent{,_test}.go
gsed -i -e 's|a\.agent\.Config|a.Config|' command/agent{,_test}.go
gsed -i -e 's|agent\.AppendSliceValue|AppendSliceValue|' command/{configtest,validate}.go
gsed -i -e 's|consul/consul|agent/consul|' GNUmakefile
gsed -i -e 's|\.\./test|../../test|' agent/consul/server_test.go
# fix imports
f=$(grep -rl 'github.com/hashicorp/consul/command/agent' * | grep '\.go')
gsed -i -e 's|github.com/hashicorp/consul/command/agent|github.com/hashicorp/consul/agent|' $f
goimports -w $f
f=$(grep -rl 'github.com/hashicorp/consul/consul' * | grep '\.go')
gsed -i -e 's|github.com/hashicorp/consul/consul|github.com/hashicorp/consul/agent/consul|' $f
goimports -w $f
goimports -w command/*.go main.go
)
8 years ago
"github.com/hashicorp/consul/agent/consul/structs"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/golang-lru"
"github.com/hashicorp/serf/serf"
)
// There's enough behavior difference with client-side ACLs that we've
// intentionally kept this code separate from the server-side ACL code in
// consul/acl.go. We may refactor some of the caching logic in the future,
// but for now we are developing this separately to see how things shake out.
// These must be kept in sync with the constants in consul/acl.go.
const (
// aclNotFound indicates there is no matching ACL.
aclNotFound = "ACL not found"
// rootDenied is returned when attempting to resolve a root ACL.
rootDenied = "Cannot resolve root ACL"
// permissionDenied is returned when an ACL based rejection happens.
permissionDenied = "Permission denied"
// aclDisabled is returned when ACL changes are not permitted since they
// are disabled.
aclDisabled = "ACL support disabled"
// anonymousToken is the token ID we re-write to if there is no token ID
// provided.
anonymousToken = "anonymous"
// Maximum number of cached ACL entries.
aclCacheSize = 10 * 1024
)
var errPermissionDenied = errors . New ( permissionDenied )
// aclCacheEntry is used to cache ACL tokens.
type aclCacheEntry struct {
// ACL is the cached ACL.
ACL acl . ACL
// Expires is set based on the TTL for the ACL.
Expires time . Time
// ETag is used as an optimization when fetching ACLs from servers to
// avoid transmitting data back when the agent has a good copy, which is
// usually the case when refreshing a TTL.
ETag string
}
// aclManager is used by the agent to keep track of state related to ACLs,
// including caching tokens from the servers. This has some internal state that
// we don't want to dump into the agent itself.
type aclManager struct {
// acls is a cache mapping ACL tokens to compiled policies.
acls * lru . TwoQueueCache
// master is the ACL to use when the agent master token is supplied.
master acl . ACL
// down is the ACL to use when the servers are down. This may be nil
// which means to try and use the cached policy if there is one (or
// deny if there isn't a policy in the cache).
down acl . ACL
// disabled is used to keep track of feedback from the servers that ACLs
// are disabled. If the manager discovers that ACLs are disabled, this
// will be set to the next time we should check to see if they have been
// enabled. This helps cut useless traffic, but allows us to turn on ACL
// support at the servers without having to restart the whole cluster.
disabled time . Time
disabledLock sync . RWMutex
}
// newACLManager returns an ACL manager based on the given config.
func newACLManager ( config * Config ) ( * aclManager , error ) {
// Set up the cache from ID to ACL (we don't cache policies like the
// servers; only one level).
acls , err := lru . New2Q ( aclCacheSize )
if err != nil {
return nil , err
}
// Build a policy for the agent master token.
policy := & acl . Policy {
Agents : [ ] * acl . AgentPolicy {
& acl . AgentPolicy {
Node : config . NodeName ,
Policy : acl . PolicyWrite ,
} ,
} ,
Nodes : [ ] * acl . NodePolicy {
& acl . NodePolicy {
Name : "" ,
Policy : acl . PolicyRead ,
} ,
} ,
}
master , err := acl . New ( acl . DenyAll ( ) , policy )
if err != nil {
return nil , err
}
var down acl . ACL
switch config . ACLDownPolicy {
case "allow" :
down = acl . AllowAll ( )
case "deny" :
down = acl . DenyAll ( )
case "extend-cache" :
// Leave the down policy as nil to signal this.
default :
return nil , fmt . Errorf ( "invalid ACL down policy %q" , config . ACLDownPolicy )
}
// Give back a manager.
return & aclManager {
acls : acls ,
master : master ,
down : down ,
} , nil
}
// isDisabled returns true if the manager has discovered that ACLs are disabled
// on the servers.
func ( m * aclManager ) isDisabled ( ) bool {
m . disabledLock . RLock ( )
defer m . disabledLock . RUnlock ( )
return time . Now ( ) . Before ( m . disabled )
}
// lookupACL attempts to locate the compiled policy associated with the given
// token. The agent may be used to perform RPC calls to the servers to fetch
// policies that aren't in the cache.
func ( m * aclManager ) lookupACL ( a * Agent , id string ) ( acl . ACL , error ) {
// Handle some special cases for the ID.
if len ( id ) == 0 {
id = anonymousToken
} else if acl . RootACL ( id ) != nil {
return nil , errors . New ( rootDenied )
} else if a . tokens . IsAgentMasterToken ( id ) {
return m . master , nil
}
// Try the cache first.
var cached * aclCacheEntry
if raw , ok := m . acls . Get ( id ) ; ok {
cached = raw . ( * aclCacheEntry )
}
if cached != nil && time . Now ( ) . Before ( cached . Expires ) {
metrics . IncrCounter ( [ ] string { "consul" , "acl" , "cache_hit" } , 1 )
return cached . ACL , nil
}
metrics . IncrCounter ( [ ] string { "consul" , "acl" , "cache_miss" } , 1 )
// At this point we might have a stale cached ACL, or none at all, so
// try to contact the servers.
args := structs . ACLPolicyRequest {
Datacenter : a . config . ACLDatacenter ,
ACL : id ,
}
if cached != nil {
args . ETag = cached . ETag
}
var reply structs . ACLPolicy
err := a . RPC ( "ACL.GetPolicy" , & args , & reply )
if err != nil {
if strings . Contains ( err . Error ( ) , aclDisabled ) {
a . logger . Printf ( "[DEBUG] agent: ACLs disabled on servers, will check again after %s" , a . config . ACLDisabledTTL )
m . disabledLock . Lock ( )
m . disabled = time . Now ( ) . Add ( a . config . ACLDisabledTTL )
m . disabledLock . Unlock ( )
return nil , nil
} else if strings . Contains ( err . Error ( ) , aclNotFound ) {
return nil , errors . New ( aclNotFound )
} else {
a . logger . Printf ( "[DEBUG] agent: Failed to get policy for ACL from servers: %v" , err )
if m . down != nil {
return m . down , nil
} else if cached != nil {
return cached . ACL , nil
} else {
return acl . DenyAll ( ) , nil
}
}
}
// Use the old cached compiled ACL if we can, otherwise compile it and
// resolve any parents.
var compiled acl . ACL
if cached != nil && cached . ETag == reply . ETag {
compiled = cached . ACL
} else {
parent := acl . RootACL ( reply . Parent )
if parent == nil {
parent , err = m . lookupACL ( a , reply . Parent )
if err != nil {
return nil , err
}
}
acl , err := acl . New ( parent , reply . Policy )
if err != nil {
return nil , err
}
compiled = acl
}
// Update the cache.
cached = & aclCacheEntry {
ACL : compiled ,
ETag : reply . ETag ,
}
if reply . TTL > 0 {
cached . Expires = time . Now ( ) . Add ( reply . TTL )
}
m . acls . Add ( id , cached )
return compiled , nil
}
// resolveToken is the primary interface used by ACL-checkers in the agent
// endpoints, which is the one place where we do some ACL enforcement on
// clients. Some of the enforcement is normative (e.g. self and monitor)
// and some is informative (e.g. catalog and health).
func ( a * Agent ) resolveToken ( id string ) ( acl . ACL , error ) {
// Disable ACLs if version 8 enforcement isn't enabled.
if ! ( * a . config . ACLEnforceVersion8 ) {
return nil , nil
}
// Bail if there's no ACL datacenter configured. This means that agent
// enforcement isn't on.
if a . config . ACLDatacenter == "" {
return nil , nil
}
// Bail if the ACL manager is disabled. This happens if it gets feedback
// from the servers that ACLs are disabled.
if a . acls . isDisabled ( ) {
return nil , nil
}
// This will look in the cache and fetch from the servers if necessary.
return a . acls . lookupACL ( a , id )
}
// vetServiceRegister makes sure the service registration action is allowed by
// the given token.
func ( a * Agent ) vetServiceRegister ( token string , service * structs . NodeService ) error {
// Resolve the token and bail if ACLs aren't enabled.
acl , err := a . resolveToken ( token )
if err != nil {
return err
}
if acl == nil {
return nil
}
// Vet the service itself.
if ! acl . ServiceWrite ( service . Service ) {
return errPermissionDenied
}
// Vet any service that might be getting overwritten.
services := a . state . Services ( )
if existing , ok := services [ service . ID ] ; ok {
if ! acl . ServiceWrite ( existing . Service ) {
return errPermissionDenied
}
}
return nil
}
// vetServiceUpdate makes sure the service update action is allowed by the given
// token.
func ( a * Agent ) vetServiceUpdate ( token string , serviceID string ) error {
// Resolve the token and bail if ACLs aren't enabled.
acl , err := a . resolveToken ( token )
if err != nil {
return err
}
if acl == nil {
return nil
}
// Vet any changes based on the existing services's info.
services := a . state . Services ( )
if existing , ok := services [ serviceID ] ; ok {
if ! acl . ServiceWrite ( existing . Service ) {
return errPermissionDenied
}
} else {
return fmt . Errorf ( "Unknown service %q" , serviceID )
}
return nil
}
// vetCheckRegister makes sure the check registration action is allowed by the
// given token.
func ( a * Agent ) vetCheckRegister ( token string , check * structs . HealthCheck ) error {
// Resolve the token and bail if ACLs aren't enabled.
acl , err := a . resolveToken ( token )
if err != nil {
return err
}
if acl == nil {
return nil
}
// Vet the check itself.
if len ( check . ServiceName ) > 0 {
if ! acl . ServiceWrite ( check . ServiceName ) {
return errPermissionDenied
}
} else {
if ! acl . NodeWrite ( a . config . NodeName ) {
return errPermissionDenied
}
}
// Vet any check that might be getting overwritten.
checks := a . state . Checks ( )
if existing , ok := checks [ check . CheckID ] ; ok {
if len ( existing . ServiceName ) > 0 {
if ! acl . ServiceWrite ( existing . ServiceName ) {
return errPermissionDenied
}
} else {
if ! acl . NodeWrite ( a . config . NodeName ) {
return errPermissionDenied
}
}
}
return nil
}
// vetCheckUpdate makes sure that a check update is allowed by the given token.
func ( a * Agent ) vetCheckUpdate ( token string , checkID types . CheckID ) error {
// Resolve the token and bail if ACLs aren't enabled.
acl , err := a . resolveToken ( token )
if err != nil {
return err
}
if acl == nil {
return nil
}
// Vet any changes based on the existing check's info.
checks := a . state . Checks ( )
if existing , ok := checks [ checkID ] ; ok {
if len ( existing . ServiceName ) > 0 {
if ! acl . ServiceWrite ( existing . ServiceName ) {
return errPermissionDenied
}
} else {
if ! acl . NodeWrite ( a . config . NodeName ) {
return errPermissionDenied
}
}
} else {
return fmt . Errorf ( "Unknown check %q" , checkID )
}
return nil
}
// filterMembers redacts members that the token doesn't have access to.
func ( a * Agent ) filterMembers ( token string , members * [ ] serf . Member ) error {
// Resolve the token and bail if ACLs aren't enabled.
acl , err := a . resolveToken ( token )
if err != nil {
return err
}
if acl == nil {
return nil
}
// Filter out members based on the node policy.
m := * members
for i := 0 ; i < len ( m ) ; i ++ {
node := m [ i ] . Name
if acl . NodeRead ( node ) {
continue
}
a . logger . Printf ( "[DEBUG] agent: dropping node %q from result due to ACLs" , node )
m = append ( m [ : i ] , m [ i + 1 : ] ... )
i --
}
* members = m
return nil
}
// filterServices redacts services that the token doesn't have access to.
func ( a * Agent ) filterServices ( token string , services * map [ string ] * structs . NodeService ) error {
// Resolve the token and bail if ACLs aren't enabled.
acl , err := a . resolveToken ( token )
if err != nil {
return err
}
if acl == nil {
return nil
}
// Filter out services based on the service policy.
for id , service := range * services {
if acl . ServiceRead ( service . Service ) {
continue
}
a . logger . Printf ( "[DEBUG] agent: dropping service %q from result due to ACLs" , id )
delete ( * services , id )
}
return nil
}
// filterChecks redacts checks that the token doesn't have access to.
func ( a * Agent ) filterChecks ( token string , checks * map [ types . CheckID ] * structs . HealthCheck ) error {
// Resolve the token and bail if ACLs aren't enabled.
acl , err := a . resolveToken ( token )
if err != nil {
return err
}
if acl == nil {
return nil
}
// Filter out checks based on the node or service policy.
for id , check := range * checks {
if len ( check . ServiceName ) > 0 {
if acl . ServiceRead ( check . ServiceName ) {
continue
}
} else {
if acl . NodeRead ( a . config . NodeName ) {
continue
}
}
a . logger . Printf ( "[DEBUG] agent: dropping check %q from result due to ACLs" , id )
delete ( * checks , id )
}
return nil
}