open-consul/agent/acl.go
Matt Keeler f9a43a1e2d
ACL Authorizer overhaul (#6620)
* ACL Authorizer overhaul

To account for upcoming features every Authorization function can now take an extra *acl.EnterpriseAuthorizerContext. These are unused in OSS and will always be nil.

Additionally the acl package has received some thorough refactoring to enable all of the extra Consul Enterprise specific authorizations including moving sentinel enforcement into the stubbed structs. The Authorizer funcs now return an acl.EnforcementDecision instead of a boolean. This improves the overall interface as it makes multiple Authorizers easily chainable as they now indicate whether they had an authoritative decision or should use some other defaults. A ChainedAuthorizer was added to handle this Authorizer enforcement chain and will never itself return a non-authoritative decision.

* Include stub for extra enterprise rules in the global management policy

* Allow for an upgrade of the global-management policy
2019-10-15 16:58:50 -04:00

287 lines
7.9 KiB
Go

package agent
import (
"fmt"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/serf/serf"
)
// 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.Authorizer, error) {
// ACLs are disabled
if !a.delegate.ACLsEnabled() {
return nil, nil
}
// Disable ACLs if version 8 enforcement isn't enabled.
if !a.config.ACLEnforceVersion8 {
return nil, nil
}
if acl.RootAuthorizer(id) != nil {
return nil, acl.ErrRootDenied
}
if a.tokens.IsAgentMasterToken(id) {
return a.aclMasterAuthorizer, nil
}
return a.delegate.ResolveToken(id)
}
func (a *Agent) initializeACLs() error {
// Build a policy for the agent master token.
// The builtin agent master policy allows reading any node information
// and allows writes to the agent with the node name of the running agent
// only. This used to allow a prefix match on agent names but that seems
// entirely unnecessary so it is now using an exact match.
policy := &acl.Policy{
PolicyRules: acl.PolicyRules{
Agents: []*acl.AgentRule{
&acl.AgentRule{
Node: a.config.NodeName,
Policy: acl.PolicyWrite,
},
},
NodePrefixes: []*acl.NodeRule{
&acl.NodeRule{
Name: "",
Policy: acl.PolicyRead,
},
},
},
}
master, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
if err != nil {
return err
}
a.aclMasterAuthorizer = master
return nil
}
// 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.
rule, err := a.resolveToken(token)
if err != nil {
return err
}
if rule == nil {
return nil
}
// Vet the service itself.
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceWrite(service.Service, nil) != acl.Allow {
return acl.ErrPermissionDenied
}
// Vet any service that might be getting overwritten.
services := a.State.Services()
if existing, ok := services[service.ID]; ok {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceWrite(existing.Service, nil) != acl.Allow {
return acl.ErrPermissionDenied
}
}
// If the service is a proxy, ensure that it has write on the destination too
// since it can be discovered as an instance of that service.
if service.Kind == structs.ServiceKindConnectProxy {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceWrite(service.Proxy.DestinationServiceName, nil) != acl.Allow {
return acl.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.
rule, err := a.resolveToken(token)
if err != nil {
return err
}
if rule == nil {
return nil
}
// Vet any changes based on the existing services's info.
services := a.State.Services()
if existing, ok := services[serviceID]; ok {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceWrite(existing.Service, nil) != acl.Allow {
return acl.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.
rule, err := a.resolveToken(token)
if err != nil {
return err
}
if rule == nil {
return nil
}
// Vet the check itself.
if len(check.ServiceName) > 0 {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceWrite(check.ServiceName, nil) != acl.Allow {
return acl.ErrPermissionDenied
}
} else {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.NodeWrite(a.config.NodeName, nil) != acl.Allow {
return acl.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 {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceWrite(existing.ServiceName, nil) != acl.Allow {
return acl.ErrPermissionDenied
}
} else {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.NodeWrite(a.config.NodeName, nil) != acl.Allow {
return acl.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.
rule, err := a.resolveToken(token)
if err != nil {
return err
}
if rule == 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 {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceWrite(existing.ServiceName, nil) != acl.Allow {
return acl.ErrPermissionDenied
}
} else {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.NodeWrite(a.config.NodeName, nil) != acl.Allow {
return acl.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.
rule, err := a.resolveToken(token)
if err != nil {
return err
}
if rule == nil {
return nil
}
// Filter out members based on the node policy.
m := *members
for i := 0; i < len(m); i++ {
node := m[i].Name
// TODO (namespaces) - pass through a real ent authz ctx
if rule.NodeRead(node, nil) == acl.Allow {
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.
rule, err := a.resolveToken(token)
if err != nil {
return err
}
if rule == nil {
return nil
}
// Filter out services based on the service policy.
for id, service := range *services {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceRead(service.Service, nil) == acl.Allow {
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.
rule, err := a.resolveToken(token)
if err != nil {
return err
}
if rule == nil {
return nil
}
// Filter out checks based on the node or service policy.
for id, check := range *checks {
if len(check.ServiceName) > 0 {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.ServiceRead(check.ServiceName, nil) == acl.Allow {
continue
}
} else {
// TODO (namespaces) - pass through a real ent authz ctx
if rule.NodeRead(a.config.NodeName, nil) == acl.Allow {
continue
}
}
a.logger.Printf("[DEBUG] agent: dropping check %q from result due to ACLs", id)
delete(*checks, id)
}
return nil
}