f9a43a1e2d
* 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
368 lines
10 KiB
Go
368 lines
10 KiB
Go
package consul
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/armon/go-metrics"
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
"github.com/hashicorp/consul/agent/consul/state"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/go-memdb"
|
|
"github.com/hashicorp/go-uuid"
|
|
)
|
|
|
|
var (
|
|
// ErrIntentionNotFound is returned if the intention lookup failed.
|
|
ErrIntentionNotFound = errors.New("Intention not found")
|
|
)
|
|
|
|
// Intention manages the Connect intentions.
|
|
type Intention struct {
|
|
// srv is a pointer back to the server.
|
|
srv *Server
|
|
}
|
|
|
|
// Apply creates or updates an intention in the data store.
|
|
func (s *Intention) Apply(
|
|
args *structs.IntentionRequest,
|
|
reply *string) error {
|
|
|
|
// Forward this request to the primary DC if we're a secondary that's replicating intentions.
|
|
if s.srv.intentionReplicationEnabled() {
|
|
args.Datacenter = s.srv.config.PrimaryDatacenter
|
|
}
|
|
|
|
if done, err := s.srv.forward("Intention.Apply", args, args, reply); done {
|
|
return err
|
|
}
|
|
defer metrics.MeasureSince([]string{"consul", "intention", "apply"}, time.Now())
|
|
defer metrics.MeasureSince([]string{"intention", "apply"}, time.Now())
|
|
|
|
// Always set a non-nil intention to avoid nil-access below
|
|
if args.Intention == nil {
|
|
args.Intention = &structs.Intention{}
|
|
}
|
|
|
|
// If no ID is provided, generate a new ID. This must be done prior to
|
|
// appending to the Raft log, because the ID is not deterministic. Once
|
|
// the entry is in the log, the state update MUST be deterministic or
|
|
// the followers will not converge.
|
|
if args.Op == structs.IntentionOpCreate {
|
|
if args.Intention.ID != "" {
|
|
return fmt.Errorf("ID must be empty when creating a new intention")
|
|
}
|
|
|
|
state := s.srv.fsm.State()
|
|
for {
|
|
var err error
|
|
args.Intention.ID, err = uuid.GenerateUUID()
|
|
if err != nil {
|
|
s.srv.logger.Printf("[ERR] consul.intention: UUID generation failed: %v", err)
|
|
return err
|
|
}
|
|
|
|
_, ixn, err := state.IntentionGet(nil, args.Intention.ID)
|
|
if err != nil {
|
|
s.srv.logger.Printf("[ERR] consul.intention: intention lookup failed: %v", err)
|
|
return err
|
|
}
|
|
if ixn == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Set the created at
|
|
args.Intention.CreatedAt = time.Now().UTC()
|
|
}
|
|
*reply = args.Intention.ID
|
|
|
|
// Get the ACL token for the request for the checks below.
|
|
rule, err := s.srv.ResolveToken(args.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Perform the ACL check
|
|
if prefix, ok := args.Intention.GetACLPrefix(); ok {
|
|
if rule != nil && rule.IntentionWrite(prefix, nil) != acl.Allow {
|
|
s.srv.logger.Printf("[WARN] consul.intention: Operation on intention '%s' denied due to ACLs", args.Intention.ID)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
}
|
|
|
|
// If this is not a create, then we have to verify the ID.
|
|
if args.Op != structs.IntentionOpCreate {
|
|
state := s.srv.fsm.State()
|
|
_, ixn, err := state.IntentionGet(nil, args.Intention.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("Intention lookup failed: %v", err)
|
|
}
|
|
if ixn == nil {
|
|
return fmt.Errorf("Cannot modify non-existent intention: '%s'", args.Intention.ID)
|
|
}
|
|
|
|
// Perform the ACL check that we have write to the old prefix too,
|
|
// which must be true to perform any rename.
|
|
if prefix, ok := ixn.GetACLPrefix(); ok {
|
|
if rule != nil && rule.IntentionWrite(prefix, nil) != acl.Allow {
|
|
s.srv.logger.Printf("[WARN] consul.intention: Operation on intention '%s' denied due to ACLs", args.Intention.ID)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
}
|
|
}
|
|
|
|
// We always update the updatedat field. This has no effect for deletion.
|
|
args.Intention.UpdatedAt = time.Now().UTC()
|
|
|
|
// Default source type
|
|
if args.Intention.SourceType == "" {
|
|
args.Intention.SourceType = structs.IntentionSourceConsul
|
|
}
|
|
|
|
// Until we support namespaces, we force all namespaces to be default
|
|
if args.Intention.SourceNS == "" {
|
|
args.Intention.SourceNS = structs.IntentionDefaultNamespace
|
|
}
|
|
if args.Intention.DestinationNS == "" {
|
|
args.Intention.DestinationNS = structs.IntentionDefaultNamespace
|
|
}
|
|
|
|
// Validate. We do not validate on delete since it is valid to only
|
|
// send an ID in that case.
|
|
if args.Op != structs.IntentionOpDelete {
|
|
// Set the precedence
|
|
args.Intention.UpdatePrecedence()
|
|
|
|
if err := args.Intention.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// make sure we set the hash prior to raft application
|
|
args.Intention.SetHash(true)
|
|
|
|
// Commit
|
|
resp, err := s.srv.raftApply(structs.IntentionRequestType, args)
|
|
if err != nil {
|
|
s.srv.logger.Printf("[ERR] consul.intention: Apply failed %v", err)
|
|
return err
|
|
}
|
|
if respErr, ok := resp.(error); ok {
|
|
return respErr
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get returns a single intention by ID.
|
|
func (s *Intention) Get(
|
|
args *structs.IntentionQueryRequest,
|
|
reply *structs.IndexedIntentions) error {
|
|
// Forward if necessary
|
|
if done, err := s.srv.forward("Intention.Get", args, args, reply); done {
|
|
return err
|
|
}
|
|
|
|
return s.srv.blockingQuery(
|
|
&args.QueryOptions,
|
|
&reply.QueryMeta,
|
|
func(ws memdb.WatchSet, state *state.Store) error {
|
|
index, ixn, err := state.IntentionGet(ws, args.IntentionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ixn == nil {
|
|
return ErrIntentionNotFound
|
|
}
|
|
|
|
reply.Index = index
|
|
reply.Intentions = structs.Intentions{ixn}
|
|
|
|
// Filter
|
|
if err := s.srv.filterACL(args.Token, reply); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If ACLs prevented any responses, error
|
|
if len(reply.Intentions) == 0 {
|
|
s.srv.logger.Printf("[WARN] consul.intention: Request to get intention '%s' denied due to ACLs", args.IntentionID)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
}
|
|
|
|
// List returns all the intentions.
|
|
func (s *Intention) List(
|
|
args *structs.DCSpecificRequest,
|
|
reply *structs.IndexedIntentions) error {
|
|
// Forward if necessary
|
|
if done, err := s.srv.forward("Intention.List", args, args, reply); done {
|
|
return err
|
|
}
|
|
|
|
return s.srv.blockingQuery(
|
|
&args.QueryOptions, &reply.QueryMeta,
|
|
func(ws memdb.WatchSet, state *state.Store) error {
|
|
index, ixns, err := state.Intentions(ws)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reply.Index, reply.Intentions = index, ixns
|
|
if reply.Intentions == nil {
|
|
reply.Intentions = make(structs.Intentions, 0)
|
|
}
|
|
|
|
return s.srv.filterACL(args.Token, reply)
|
|
},
|
|
)
|
|
}
|
|
|
|
// Match returns the set of intentions that match the given source/destination.
|
|
func (s *Intention) Match(
|
|
args *structs.IntentionQueryRequest,
|
|
reply *structs.IndexedIntentionMatches) error {
|
|
// Forward if necessary
|
|
if done, err := s.srv.forward("Intention.Match", args, args, reply); done {
|
|
return err
|
|
}
|
|
|
|
// Get the ACL token for the request for the checks below.
|
|
rule, err := s.srv.ResolveToken(args.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rule != nil {
|
|
// We go through each entry and test the destination to check if it
|
|
// matches.
|
|
for _, entry := range args.Match.Entries {
|
|
if prefix := entry.Name; prefix != "" && rule.IntentionRead(prefix, nil) != acl.Allow {
|
|
s.srv.logger.Printf("[WARN] consul.intention: Operation on intention prefix '%s' denied due to ACLs", prefix)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
}
|
|
}
|
|
|
|
return s.srv.blockingQuery(
|
|
&args.QueryOptions,
|
|
&reply.QueryMeta,
|
|
func(ws memdb.WatchSet, state *state.Store) error {
|
|
index, matches, err := state.IntentionMatch(ws, args.Match)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reply.Index = index
|
|
reply.Matches = matches
|
|
return nil
|
|
},
|
|
)
|
|
}
|
|
|
|
// Check tests a source/destination and returns whether it would be allowed
|
|
// or denied based on the current ACL configuration.
|
|
//
|
|
// Note: Whenever the logic for this method is changed, you should take
|
|
// a look at the agent authorize endpoint (agent/agent_endpoint.go) since
|
|
// the logic there is similar.
|
|
func (s *Intention) Check(
|
|
args *structs.IntentionQueryRequest,
|
|
reply *structs.IntentionQueryCheckResponse) error {
|
|
// Forward maybe
|
|
if done, err := s.srv.forward("Intention.Check", args, args, reply); done {
|
|
return err
|
|
}
|
|
|
|
// Get the test args, and defensively guard against nil
|
|
query := args.Check
|
|
if query == nil {
|
|
return errors.New("Check must be specified on args")
|
|
}
|
|
|
|
// Build the URI
|
|
var uri connect.CertURI
|
|
switch query.SourceType {
|
|
case structs.IntentionSourceConsul:
|
|
uri = &connect.SpiffeIDService{
|
|
Namespace: query.SourceNS,
|
|
Service: query.SourceName,
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported SourceType: %q", query.SourceType)
|
|
}
|
|
|
|
// Get the ACL token for the request for the checks below.
|
|
rule, err := s.srv.ResolveToken(args.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Perform the ACL check. For Check we only require ServiceRead and
|
|
// NOT IntentionRead because the Check API only returns pass/fail and
|
|
// returns no other information about the intentions used.
|
|
if prefix, ok := query.GetACLPrefix(); ok {
|
|
if rule != nil && rule.ServiceRead(prefix, nil) != acl.Allow {
|
|
s.srv.logger.Printf("[WARN] consul.intention: test on intention '%s' denied due to ACLs", prefix)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
}
|
|
|
|
// Get the matches for this destination
|
|
state := s.srv.fsm.State()
|
|
_, matches, err := state.IntentionMatch(nil, &structs.IntentionQueryMatch{
|
|
Type: structs.IntentionMatchDestination,
|
|
Entries: []structs.IntentionMatchEntry{
|
|
structs.IntentionMatchEntry{
|
|
Namespace: query.DestinationNS,
|
|
Name: query.DestinationName,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(matches) != 1 {
|
|
// This should never happen since the documented behavior of the
|
|
// Match call is that it'll always return exactly the number of results
|
|
// as entries passed in. But we guard against misbehavior.
|
|
return errors.New("internal error loading matches")
|
|
}
|
|
|
|
// Check the authorization for each match
|
|
for _, ixn := range matches[0] {
|
|
if auth, ok := uri.Authorize(ixn); ok {
|
|
reply.Allowed = auth
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// No match, we need to determine the default behavior. We do this by
|
|
// specifying the anonymous token token, which will get that behavior.
|
|
// The default behavior if ACLs are disabled is to allow connections
|
|
// to mimic the behavior of Consul itself: everything is allowed if
|
|
// ACLs are disabled.
|
|
//
|
|
// NOTE(mitchellh): This is the same behavior as the agent authorize
|
|
// endpoint. If this behavior is incorrect, we should also change it there
|
|
// which is much more important.
|
|
rule, err = s.srv.ResolveToken("")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reply.Allowed = true
|
|
if rule != nil {
|
|
reply.Allowed = rule.IntentionDefaultAllow(nil) == acl.Allow
|
|
}
|
|
|
|
return nil
|
|
}
|