consul/agent/consul/intention_endpoint.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) {
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) {
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) {
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) {
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()
}
return nil
}