49e9bbbdf9
* agent: add and edit doc comments * agent: add ACL token accessorID to debugging traces * agent: polish acl debugging * agent: minor fix + string fmt over value interp * agent: undo export & fix logging field names * agent: remove note and migrate up to code review * Update agent/consul/acl.go Co-Authored-By: Matt Keeler <mkeeler@users.noreply.github.com> * agent: incorporate review feedback * Update agent/acl.go Co-Authored-By: R.B. Boyer <public@richardboyer.net> Co-authored-by: Matt Keeler <mkeeler@users.noreply.github.com> Co-authored-by: R.B. Boyer <public@richardboyer.net>
482 lines
15 KiB
Go
482 lines
15 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/consul/lib"
|
|
"github.com/hashicorp/go-memdb"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func (s *Intention) checkIntentionID(id string) (bool, error) {
|
|
state := s.srv.fsm.State()
|
|
if _, ixn, err := state.IntentionGet(nil, id); err != nil {
|
|
return false, err
|
|
} else if ixn != nil {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// prepareApplyCreate validates that the requester has permissions to create the new intention,
|
|
// generates a new uuid for the intention and generally validates that the request is well-formed
|
|
func (s *Intention) prepareApplyCreate(ident structs.ACLIdentity, authz acl.Authorizer, entMeta *structs.EnterpriseMeta, args *structs.IntentionRequest) error {
|
|
if !args.Intention.CanWrite(authz) {
|
|
var accessorID string
|
|
if ident != nil {
|
|
accessorID = ident.ID()
|
|
}
|
|
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
|
|
s.srv.logger.Printf("[WARN] consul.intention: Intention creation denied due to ACLs, accessorID=%q", accessorID)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
|
|
// 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.Intention.ID != "" {
|
|
return fmt.Errorf("ID must be empty when creating a new intention")
|
|
}
|
|
|
|
var err error
|
|
args.Intention.ID, err = lib.GenerateUUID(s.checkIntentionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Set the created at
|
|
args.Intention.CreatedAt = time.Now().UTC()
|
|
args.Intention.UpdatedAt = args.Intention.CreatedAt
|
|
|
|
// Default source type
|
|
if args.Intention.SourceType == "" {
|
|
args.Intention.SourceType = structs.IntentionSourceConsul
|
|
}
|
|
|
|
args.Intention.DefaultNamespaces(entMeta)
|
|
|
|
// Validate. We do not validate on delete since it is valid to only
|
|
// send an ID in that case.
|
|
// 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)
|
|
|
|
return nil
|
|
}
|
|
|
|
// prepareApplyUpdate validates that the requester has permissions on both the updated and existing
|
|
// intention as well as generally validating that the request is well-formed
|
|
func (s *Intention) prepareApplyUpdate(ident structs.ACLIdentity, authz acl.Authorizer, entMeta *structs.EnterpriseMeta, args *structs.IntentionRequest) error {
|
|
if !args.Intention.CanWrite(authz) {
|
|
var accessorID string
|
|
if ident != nil {
|
|
accessorID = ident.ID()
|
|
}
|
|
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
|
|
s.srv.logger.Printf("[WARN] consul.intention: Update operation on intention denied due to ACLs, intention=%q accessorID=%q", args.Intention.ID, accessorID)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
|
|
_, ixn, err := s.srv.fsm.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 intention too,
|
|
// which must be true to perform any rename. This is the only ACL enforcement
|
|
// done for deletions and a secondary enforcement for updates.
|
|
if !ixn.CanWrite(authz) {
|
|
var accessorID string
|
|
if ident != nil {
|
|
accessorID = ident.ID()
|
|
}
|
|
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
|
|
s.srv.logger.Printf("[WARN] consul.intention: Update operation on intention denied due to ACLs, intention=%q accessorID=%q", args.Intention.ID, accessorID)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
|
|
// We always update the updatedat field.
|
|
args.Intention.UpdatedAt = time.Now().UTC()
|
|
|
|
// Default source type
|
|
if args.Intention.SourceType == "" {
|
|
args.Intention.SourceType = structs.IntentionSourceConsul
|
|
}
|
|
|
|
args.Intention.DefaultNamespaces(entMeta)
|
|
|
|
// Validate. We do not validate on delete since it is valid to only
|
|
// send an ID in that case.
|
|
// 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)
|
|
|
|
return nil
|
|
}
|
|
|
|
// prepareApplyDelete ensures that the intention specified by the ID in the request exists
|
|
// and that the requester is authorized to delete it
|
|
func (s *Intention) prepareApplyDelete(ident structs.ACLIdentity, authz acl.Authorizer, entMeta *structs.EnterpriseMeta, args *structs.IntentionRequest) error {
|
|
// If this is not a create, then we have to verify the ID.
|
|
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 delete non-existent intention: '%s'", args.Intention.ID)
|
|
}
|
|
|
|
// Perform the ACL check that we have write to the old intention too,
|
|
// which must be true to perform any rename. This is the only ACL enforcement
|
|
// done for deletions and a secondary enforcement for updates.
|
|
if !ixn.CanWrite(authz) {
|
|
var accessorID string
|
|
if ident != nil {
|
|
accessorID = ident.ID()
|
|
}
|
|
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
|
|
s.srv.logger.Printf("[WARN] consul.intention: Deletion operation on intention denied due to ACLs, intention=%q accessorID=%q", args.Intention.ID, accessorID)
|
|
return acl.ErrPermissionDenied
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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{}
|
|
}
|
|
|
|
// Get the ACL token for the request for the checks below.
|
|
var entMeta structs.EnterpriseMeta
|
|
ident, authz, err := s.srv.ResolveTokenIdentityAndDefaultMeta(args.Token, &entMeta, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch args.Op {
|
|
case structs.IntentionOpCreate:
|
|
if err := s.prepareApplyCreate(ident, authz, &entMeta, args); err != nil {
|
|
return err
|
|
}
|
|
case structs.IntentionOpUpdate:
|
|
if err := s.prepareApplyUpdate(ident, authz, &entMeta, args); err != nil {
|
|
return err
|
|
}
|
|
case structs.IntentionOpDelete:
|
|
if err := s.prepareApplyDelete(ident, authz, &entMeta, args); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("Invalid Intention operation: %v", args.Op)
|
|
}
|
|
|
|
// setup the reply which will have been filled in by one of the 3 preparedApply* funcs
|
|
*reply = args.Intention.ID
|
|
|
|
// 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 {
|
|
accessorID := s.aclAccessorID(args.Token)
|
|
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
|
|
s.srv.logger.Printf("[WARN] consul.intention: Request to get intention denied due to ACLs, intention=%s accessorID=%q", args.IntentionID, accessorID)
|
|
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 {
|
|
var authzContext acl.AuthorizerContext
|
|
// Go through each entry to ensure we have intention:read for the resource.
|
|
|
|
// TODO - should we do this instead of filtering the result set? This will only allow
|
|
// queries for which the token has intention:read permissions on the requested side
|
|
// of the service. Should it instead return all matches that it would be able to list.
|
|
// if so we should remove this and call filterACL instead. Based on how this is used
|
|
// its probably fine. If you have intention read on the source just do a source type
|
|
// matching, if you have it on the dest then perform a dest type match.
|
|
for _, entry := range args.Match.Entries {
|
|
entry.FillAuthzContext(&authzContext)
|
|
if prefix := entry.Name; prefix != "" && rule.IntentionRead(prefix, &authzContext) != acl.Allow {
|
|
accessorID := s.aclAccessorID(args.Token)
|
|
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
|
|
s.srv.logger.Printf("[WARN] consul.intention: Operation on intention prefix denied due to ACLs, prefix=%s accessorID=%q", prefix, accessorID)
|
|
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. We could check
|
|
// both the source and dest side but only checking dest also has the nice
|
|
// benefit of only returning a passing status if the token would be able
|
|
// to discover the dest service and connect to it.
|
|
if prefix, ok := query.GetACLPrefix(); ok {
|
|
var authzContext acl.AuthorizerContext
|
|
query.FillAuthzContext(&authzContext)
|
|
if rule != nil && rule.ServiceRead(prefix, &authzContext) != acl.Allow {
|
|
accessorID := s.aclAccessorID(args.Token)
|
|
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
|
|
s.srv.logger.Printf("[WARN] consul.intention: test on intention denied due to ACLs, intention=%s accessorID=%q", prefix, accessorID)
|
|
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
|
|
}
|
|
|
|
// aclAccessorID is used to convert an ACLToken's secretID to its accessorID for non-
|
|
// critical purposes, such as logging. Therefore we interpret all errors as empty-string
|
|
// so we can safely log it without handling non-critical errors at the usage site.
|
|
func (s *Intention) aclAccessorID(secretID string) string {
|
|
_, ident, err := s.srv.ResolveIdentityFromToken(secretID)
|
|
if err != nil {
|
|
s.srv.logger.Printf("[DEBUG] consul.intention: %v", err)
|
|
return ""
|
|
}
|
|
if ident == nil {
|
|
return ""
|
|
}
|
|
return ident.ID()
|
|
}
|