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-bexpr" "github.com/hashicorp/go-hclog" "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 logger hclog.Logger } 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.logger.Warn("Intention creation denied due to ACLs", "intention", args.Intention.ID, "accessorID", 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) if err := s.validateEnterpriseIntention(args.Intention); err != nil { return err } // 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() 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.logger.Warn("Update operation on intention denied due to ACLs", "intention", args.Intention.ID, "accessorID", 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.logger.Warn("Update operation on intention denied due to ACLs", "intention", args.Intention.ID, "accessorID", 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) if err := s.validateEnterpriseIntention(args.Intention); err != nil { return err } // Set the precedence args.Intention.UpdatePrecedence() // Validate. We do not validate on delete since it is valid to only // send an ID in that case. if err := args.Intention.Validate(); err != nil { return err } // make sure we set the hash prior to raft application args.Intention.SetHash() 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, 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.logger.Warn("Deletion operation on intention denied due to ACLs", "intention", args.Intention.ID, "accessorID", 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.ForwardRPC("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, 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.logger.Error("Raft apply failed", "error", 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.ForwardRPC("Intention.Get", args, args, reply); done { return err } // Get the ACL token for the request for the checks below. var entMeta structs.EnterpriseMeta if _, err := s.srv.ResolveTokenAndDefaultMeta(args.Token, &entMeta, nil); err != nil { return err } if args.Exact != nil { // // Finish defaulting the namespace fields. if args.Exact.SourceNS == "" { args.Exact.SourceNS = entMeta.NamespaceOrDefault() } if err := s.srv.validateEnterpriseIntentionNamespace(args.Exact.SourceNS, true); err != nil { return fmt.Errorf("Invalid SourceNS %q: %v", args.Exact.SourceNS, err) } if args.Exact.DestinationNS == "" { args.Exact.DestinationNS = entMeta.NamespaceOrDefault() } if err := s.srv.validateEnterpriseIntentionNamespace(args.Exact.DestinationNS, true); err != nil { return fmt.Errorf("Invalid DestinationNS %q: %v", args.Exact.DestinationNS, err) } } return s.srv.blockingQuery( &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { var ( index uint64 ixn *structs.Intention err error ) if args.IntentionID != "" { index, ixn, err = state.IntentionGet(ws, args.IntentionID) } else if args.Exact != nil { index, ixn, err = state.IntentionGetExact(ws, args.Exact) } 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.logger.Warn("Request to get intention denied due to ACLs", "intention", args.IntentionID, "accessorID", 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.ForwardRPC("Intention.List", args, args, reply); done { return err } filter, err := bexpr.CreateFilter(args.Filter, nil, reply.Intentions) if err != nil { return err } var authzContext acl.AuthorizerContext if _, err := s.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil { return err } if err := s.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil { return err } return s.srv.blockingQuery( &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { index, ixns, err := state.Intentions(ws, &args.EnterpriseMeta) if err != nil { return err } reply.Index, reply.Intentions = index, ixns if reply.Intentions == nil { reply.Intentions = make(structs.Intentions, 0) } if err := s.srv.filterACL(args.Token, reply); err != nil { return err } raw, err := filter.Execute(reply.Intentions) if err != nil { return err } reply.Intentions = raw.(structs.Intentions) return nil }, ) } // 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.ForwardRPC("Intention.Match", args, args, reply); done { return err } // Get the ACL token for the request for the checks below. var entMeta structs.EnterpriseMeta authz, err := s.srv.ResolveTokenAndDefaultMeta(args.Token, &entMeta, nil) if err != nil { return err } // Finish defaulting the namespace fields. for i := range args.Match.Entries { if args.Match.Entries[i].Namespace == "" { args.Match.Entries[i].Namespace = entMeta.NamespaceOrDefault() } if err := s.srv.validateEnterpriseIntentionNamespace(args.Match.Entries[i].Namespace, true); err != nil { return fmt.Errorf("Invalid match entry namespace %q: %v", args.Match.Entries[i].Namespace, err) } } if authz != 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 != "" && authz.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.logger.Warn("Operation on intention prefix denied due to ACLs", "prefix", prefix, "accessorID", 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.ForwardRPC("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") } // Get the ACL token for the request for the checks below. var entMeta structs.EnterpriseMeta authz, err := s.srv.ResolveTokenAndDefaultMeta(args.Token, &entMeta, nil) if err != nil { return err } // Finish defaulting the namespace fields. if query.SourceNS == "" { query.SourceNS = entMeta.NamespaceOrDefault() } if query.DestinationNS == "" { query.DestinationNS = entMeta.NamespaceOrDefault() } if err := s.srv.validateEnterpriseIntentionNamespace(query.SourceNS, false); err != nil { return fmt.Errorf("Invalid source namespace %q: %v", query.SourceNS, err) } if err := s.srv.validateEnterpriseIntentionNamespace(query.DestinationNS, false); err != nil { return fmt.Errorf("Invalid destination namespace %q: %v", query.DestinationNS, err) } // 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) } // 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 authz != nil && authz.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.logger.Warn("test on intention denied due to ACLs", "prefix", prefix, "accessorID", 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{ { 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. authz, err = s.srv.ResolveToken("") if err != nil { return err } reply.Allowed = true if authz != nil { reply.Allowed = authz.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 acl.IsErrNotFound(err) { return "" } if err != nil { s.logger.Debug("non-critical error resolving acl token accessor for logging", "error", err) return "" } if ident == nil { return "" } return ident.ID() } func (s *Intention) validateEnterpriseIntention(ixn *structs.Intention) error { if err := s.srv.validateEnterpriseIntentionNamespace(ixn.SourceNS, true); err != nil { return fmt.Errorf("Invalid source namespace %q: %v", ixn.SourceNS, err) } if err := s.srv.validateEnterpriseIntentionNamespace(ixn.DestinationNS, true); err != nil { return fmt.Errorf("Invalid destination namespace %q: %v", ixn.DestinationNS, err) } return nil }