2018-02-28 18:04:27 +00:00
package consul
import (
2018-02-28 23:54:48 +00:00
"errors"
2018-03-01 05:11:35 +00:00
"fmt"
2018-02-28 18:28:07 +00:00
"time"
"github.com/armon/go-metrics"
2018-03-04 08:39:56 +00:00
"github.com/hashicorp/consul/acl"
2018-05-11 05:35:47 +00:00
"github.com/hashicorp/consul/agent/connect"
2018-02-28 18:04:27 +00:00
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
2020-01-13 20:51:40 +00:00
"github.com/hashicorp/consul/lib"
2020-04-07 15:48:44 +00:00
"github.com/hashicorp/go-bexpr"
2020-01-28 23:50:41 +00:00
"github.com/hashicorp/go-hclog"
2018-02-28 18:04:27 +00:00
"github.com/hashicorp/go-memdb"
)
2018-02-28 23:54:48 +00:00
var (
// ErrIntentionNotFound is returned if the intention lookup failed.
ErrIntentionNotFound = errors . New ( "Intention not found" )
)
2020-10-06 18:24:05 +00:00
// NewIntentionEndpoint returns a new Intention endpoint.
func NewIntentionEndpoint ( srv * Server , logger hclog . Logger ) * Intention {
return & Intention {
srv : srv ,
logger : logger ,
configEntryEndpoint : & ConfigEntry { srv } ,
}
}
2018-02-28 18:04:27 +00:00
// Intention manages the Connect intentions.
type Intention struct {
// srv is a pointer back to the server.
2020-01-28 23:50:41 +00:00
srv * Server
logger hclog . Logger
2020-10-06 18:24:05 +00:00
configEntryEndpoint * ConfigEntry
2018-02-28 18:04:27 +00:00
}
2020-01-13 20:51:40 +00:00
func ( s * Intention ) checkIntentionID ( id string ) ( bool , error ) {
state := s . srv . fsm . State ( )
2020-10-06 18:24:05 +00:00
if _ , _ , ixn , err := state . IntentionGet ( nil , id ) ; err != nil {
2020-01-13 20:51:40 +00:00
return false , err
} else if ixn != nil {
return false , nil
2019-01-22 19:29:13 +00:00
}
2020-01-13 20:51:40 +00:00
return true , nil
}
2018-02-28 18:28:07 +00:00
2020-10-06 18:24:05 +00:00
// 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
//
// Returns an existing service-intentions config entry for this destination if
// one exists.
func ( s * Intention ) prepareApplyCreate (
ident structs . ACLIdentity ,
authz acl . Authorizer ,
entMeta * structs . EnterpriseMeta ,
args * structs . IntentionRequest ,
) ( * structs . ServiceIntentionsConfigEntry , error ) {
2020-01-13 20:51:40 +00:00
if ! args . Intention . CanWrite ( authz ) {
2020-01-27 19:54:32 +00:00
var accessorID string
if ident != nil {
accessorID = ident . ID ( )
}
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
2020-01-28 23:50:41 +00:00
s . logger . Warn ( "Intention creation denied due to ACLs" , "intention" , args . Intention . ID , "accessorID" , accessorID )
2020-10-06 18:24:05 +00:00
return nil , acl . ErrPermissionDenied
2018-03-03 16:43:19 +00:00
}
2018-02-28 18:28:07 +00:00
// 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.
2020-01-13 20:51:40 +00:00
if args . Intention . ID != "" {
2020-10-06 18:24:05 +00:00
return nil , fmt . Errorf ( "ID must be empty when creating a new intention" )
2020-01-13 20:51:40 +00:00
}
2018-02-28 18:28:07 +00:00
2020-01-13 20:51:40 +00:00
var err error
args . Intention . ID , err = lib . GenerateUUID ( s . checkIntentionID )
if err != nil {
2020-10-06 18:24:05 +00:00
return nil , err
2020-01-13 20:51:40 +00:00
}
// Set the created at
args . Intention . CreatedAt = time . Now ( ) . UTC ( )
args . Intention . UpdatedAt = args . Intention . CreatedAt
2018-03-03 16:43:19 +00:00
2020-01-13 20:51:40 +00:00
// Default source type
if args . Intention . SourceType == "" {
args . Intention . SourceType = structs . IntentionSourceConsul
2018-02-28 18:28:07 +00:00
}
2020-01-13 20:51:40 +00:00
args . Intention . DefaultNamespaces ( entMeta )
2020-06-26 21:59:15 +00:00
if err := s . validateEnterpriseIntention ( args . Intention ) ; err != nil {
2020-10-06 18:24:05 +00:00
return nil , err
2020-06-26 21:59:15 +00:00
}
2020-10-06 18:24:05 +00:00
//nolint:staticcheck
2020-01-13 20:51:40 +00:00
if err := args . Intention . Validate ( ) ; err != nil {
2020-10-06 18:24:05 +00:00
return nil , err
2018-03-04 08:39:56 +00:00
}
2020-10-06 18:24:05 +00:00
_ , configEntry , err := s . srv . fsm . State ( ) . ConfigEntry ( nil , structs . ServiceIntentions , args . Intention . DestinationName , args . Intention . DestinationEnterpriseMeta ( ) )
if err != nil {
return nil , fmt . Errorf ( "service-intentions config entry lookup failed: %v" , err )
} else if configEntry == nil {
return nil , nil
}
2020-01-13 20:51:40 +00:00
2020-10-06 18:24:05 +00:00
return configEntry . ( * structs . ServiceIntentionsConfigEntry ) , nil
2020-01-13 20:51:40 +00:00
}
2020-10-06 18:24:05 +00:00
// prepareApplyUpdateLegacy validates that the requester has permissions on both the updated and existing
2020-01-13 20:51:40 +00:00
// intention as well as generally validating that the request is well-formed
2020-10-06 18:24:05 +00:00
//
// Returns an existing service-intentions config entry for this destination if
// one exists.
func ( s * Intention ) prepareApplyUpdateLegacy (
ident structs . ACLIdentity ,
authz acl . Authorizer ,
entMeta * structs . EnterpriseMeta ,
args * structs . IntentionRequest ,
) ( * structs . ServiceIntentionsConfigEntry , error ) {
2020-01-13 20:51:40 +00:00
if ! args . Intention . CanWrite ( authz ) {
2020-01-27 19:54:32 +00:00
var accessorID string
if ident != nil {
accessorID = ident . ID ( )
}
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
2020-01-28 23:50:41 +00:00
s . logger . Warn ( "Update operation on intention denied due to ACLs" , "intention" , args . Intention . ID , "accessorID" , accessorID )
2020-10-06 18:24:05 +00:00
return nil , acl . ErrPermissionDenied
2018-03-04 08:39:56 +00:00
}
2020-10-06 18:24:05 +00:00
_ , configEntry , ixn , err := s . srv . fsm . State ( ) . IntentionGet ( nil , args . Intention . ID )
2020-01-13 20:51:40 +00:00
if err != nil {
2020-10-06 18:24:05 +00:00
return nil , fmt . Errorf ( "Intention lookup failed: %v" , err )
2020-01-13 20:51:40 +00:00
}
2020-10-06 18:24:05 +00:00
if ixn == nil || configEntry == nil {
return nil , fmt . Errorf ( "Cannot modify non-existent intention: '%s'" , args . Intention . ID )
2020-01-13 20:51:40 +00:00
}
2018-03-04 19:35:39 +00:00
2020-01-13 20:51:40 +00:00
// 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 ) {
2020-01-27 19:54:32 +00:00
var accessorID string
if ident != nil {
accessorID = ident . ID ( )
}
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
2020-01-28 23:50:41 +00:00
s . logger . Warn ( "Update operation on intention denied due to ACLs" , "intention" , args . Intention . ID , "accessorID" , accessorID )
2020-10-06 18:24:05 +00:00
return nil , acl . ErrPermissionDenied
}
// Prior to v1.9.0 renames of the destination side of an intention were
// allowed, but that behavior doesn't work anymore.
if ixn . DestinationServiceName ( ) != args . Intention . DestinationServiceName ( ) {
return nil , fmt . Errorf ( "Cannot modify DestinationNS or DestinationName for an intention once it exists." )
2018-03-01 05:11:35 +00:00
}
2020-01-13 20:51:40 +00:00
// We always update the updatedat field.
2018-03-06 17:04:44 +00:00
args . Intention . UpdatedAt = time . Now ( ) . UTC ( )
2018-03-03 16:43:19 +00:00
2018-03-03 17:55:27 +00:00
// Default source type
if args . Intention . SourceType == "" {
args . Intention . SourceType = structs . IntentionSourceConsul
}
2020-01-13 20:51:40 +00:00
args . Intention . DefaultNamespaces ( entMeta )
2018-03-03 16:51:40 +00:00
2020-06-26 21:59:15 +00:00
if err := s . validateEnterpriseIntention ( args . Intention ) ; err != nil {
2020-10-06 18:24:05 +00:00
return nil , err
2020-06-26 21:59:15 +00:00
}
// Validate. We do not validate on delete since it is valid to only
// send an ID in that case.
2020-10-06 18:24:05 +00:00
//nolint:staticcheck
2020-01-13 20:51:40 +00:00
if err := args . Intention . Validate ( ) ; err != nil {
2020-10-06 18:24:05 +00:00
return nil , err
2018-03-03 17:43:37 +00:00
}
2020-10-06 18:24:05 +00:00
return configEntry , nil
2020-01-13 20:51:40 +00:00
}
2020-10-06 18:24:05 +00:00
// prepareApplyDeleteLegacy ensures that the intention specified by the ID in the request exists
2020-01-13 20:51:40 +00:00
// and that the requester is authorized to delete it
2020-10-06 18:24:05 +00:00
//
// Returns an existing service-intentions config entry for this destination if
// one exists.
func ( s * Intention ) prepareApplyDeleteLegacy (
ident structs . ACLIdentity ,
authz acl . Authorizer ,
args * structs . IntentionRequest ,
) ( * structs . ServiceIntentionsConfigEntry , error ) {
2020-01-13 20:51:40 +00:00
// If this is not a create, then we have to verify the ID.
2020-10-06 18:24:05 +00:00
_ , configEntry , ixn , err := s . srv . fsm . State ( ) . IntentionGet ( nil , args . Intention . ID )
2020-01-13 20:51:40 +00:00
if err != nil {
2020-10-06 18:24:05 +00:00
return nil , fmt . Errorf ( "Intention lookup failed: %v" , err )
2020-01-13 20:51:40 +00:00
}
2020-10-06 18:24:05 +00:00
if ixn == nil || configEntry == nil {
return nil , fmt . Errorf ( "Cannot delete non-existent intention: '%s'" , args . Intention . ID )
2020-01-13 20:51:40 +00:00
}
2020-10-06 18:24:05 +00:00
// Perform the ACL check that we have write to the old intention. This is
// the only ACL enforcement done for deletions and a secondary enforcement
// for updates.
2020-01-13 20:51:40 +00:00
if ! ixn . CanWrite ( authz ) {
2020-01-27 19:54:32 +00:00
var accessorID string
if ident != nil {
accessorID = ident . ID ( )
}
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
2020-01-28 23:50:41 +00:00
s . logger . Warn ( "Deletion operation on intention denied due to ACLs" , "intention" , args . Intention . ID , "accessorID" , accessorID )
2020-10-06 18:24:05 +00:00
return nil , acl . ErrPermissionDenied
2020-01-13 20:51:40 +00:00
}
2020-10-06 18:24:05 +00:00
return configEntry , nil
}
var ErrIntentionsNotUpgradedYet = errors . New ( "Intentions are read only while being upgraded to config entries" )
// legacyUpgradeCheck fast fails a write request using the legacy intention
// RPCs if the system is known to be mid-upgrade. This is purely a perf
// optimization and the actual real enforcement happens in the FSM. It would be
// wasteful to round trip all the way through raft to have it fail for
// known-up-front reasons, hence why we check it twice.
func ( s * Intention ) legacyUpgradeCheck ( ) error {
usingConfigEntries , err := s . srv . fsm . State ( ) . AreIntentionsInConfigEntries ( )
if err != nil {
return fmt . Errorf ( "system metadata lookup failed: %v" , err )
}
if ! usingConfigEntries {
return ErrIntentionsNotUpgradedYet
}
2020-01-13 20:51:40 +00:00
return nil
}
// Apply creates or updates an intention in the data store.
func ( s * Intention ) Apply (
args * structs . IntentionRequest ,
reply * string ) error {
2020-10-06 18:24:05 +00:00
// Ensure that all service-intentions config entry writes go to the primary
// datacenter. These will then be replicated to all the other datacenters.
args . Datacenter = s . srv . config . PrimaryDatacenter
2020-01-13 20:51:40 +00:00
2020-07-07 19:45:08 +00:00
if done , err := s . srv . ForwardRPC ( "Intention.Apply" , args , args , reply ) ; done {
2020-01-13 20:51:40 +00:00
return err
}
defer metrics . MeasureSince ( [ ] string { "consul" , "intention" , "apply" } , time . Now ( ) )
defer metrics . MeasureSince ( [ ] string { "intention" , "apply" } , time . Now ( ) )
2020-10-06 18:24:05 +00:00
if err := s . legacyUpgradeCheck ( ) ; err != nil {
return err
}
2020-01-13 20:51:40 +00:00
// 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
2020-01-27 19:54:32 +00:00
ident , authz , err := s . srv . ResolveTokenIdentityAndDefaultMeta ( args . Token , & entMeta , nil )
2020-01-13 20:51:40 +00:00
if err != nil {
return err
}
2020-10-06 18:24:05 +00:00
var (
prevEntry * structs . ServiceIntentionsConfigEntry
upsertEntry * structs . ServiceIntentionsConfigEntry
legacyWrite bool
noop bool
)
2020-01-13 20:51:40 +00:00
switch args . Op {
case structs . IntentionOpCreate :
2020-10-06 18:24:05 +00:00
legacyWrite = true
// This variant is just for legacy UUID-based intentions.
prevEntry , err = s . prepareApplyCreate ( ident , authz , & entMeta , args )
if err != nil {
2020-01-13 20:51:40 +00:00
return err
}
2020-10-06 18:24:05 +00:00
if prevEntry == nil {
2020-10-06 22:09:13 +00:00
upsertEntry = args . Intention . ToConfigEntry ( true )
2020-10-06 18:24:05 +00:00
} else {
upsertEntry = prevEntry . Clone ( )
2020-10-06 22:09:13 +00:00
upsertEntry . Sources = append ( upsertEntry . Sources , args . Intention . ToSourceIntention ( true ) )
2020-10-06 18:24:05 +00:00
}
2020-01-13 20:51:40 +00:00
case structs . IntentionOpUpdate :
2020-10-06 18:24:05 +00:00
// This variant is just for legacy UUID-based intentions.
legacyWrite = true
prevEntry , err = s . prepareApplyUpdateLegacy ( ident , authz , & entMeta , args )
if err != nil {
2020-01-13 20:51:40 +00:00
return err
}
2020-10-06 18:24:05 +00:00
upsertEntry = prevEntry . Clone ( )
for i , src := range upsertEntry . Sources {
if src . LegacyID == args . Intention . ID {
2020-10-06 22:09:13 +00:00
upsertEntry . Sources [ i ] = args . Intention . ToSourceIntention ( true )
2020-10-06 18:24:05 +00:00
break
}
}
case structs . IntentionOpUpsert :
// This variant is just for config-entry based intentions.
legacyWrite = false
if args . Intention . ID != "" {
// This is a new-style only endpoint
return fmt . Errorf ( "ID must not be specified" )
}
args . Intention . DefaultNamespaces ( & entMeta )
prevEntry , err = s . getServiceIntentionsConfigEntry ( args . Intention . DestinationName , args . Intention . DestinationEnterpriseMeta ( ) )
if err != nil {
2020-01-13 20:51:40 +00:00
return err
}
2020-10-06 18:24:05 +00:00
sn := args . Intention . SourceServiceName ( )
// TODO(intentions): have service-intentions validation functions
// return structured errors so that we can rewrite the field prefix
// here so that the validation errors are not misleading.
if prevEntry == nil {
// Meta is NOT permitted here, as it would need to be persisted on
// the enclosing config entry.
if len ( args . Intention . Meta ) > 0 {
return fmt . Errorf ( "Meta must not be specified" )
}
2020-10-06 22:09:13 +00:00
upsertEntry = args . Intention . ToConfigEntry ( false )
2020-10-06 18:24:05 +00:00
} else {
upsertEntry = prevEntry . Clone ( )
if len ( args . Intention . Meta ) > 0 {
// Meta is NOT permitted here, but there is one exception. If
// you are updating a previous record, but that record lives
// within a config entry that itself has Meta, then you may
// incidentally ship the Meta right back to consul.
//
// In that case if Meta is provided, it has to be a perfect
// match for what is already on the enclosing config entry so
// it's safe to discard.
if ! equalStringMaps ( upsertEntry . Meta , args . Intention . Meta ) {
return fmt . Errorf ( "Meta must not be specified, or should be unchanged during an update." )
}
// Now it is safe to discard
args . Intention . Meta = nil
}
found := false
for i , src := range upsertEntry . Sources {
if src . SourceServiceName ( ) == sn {
2020-10-06 22:09:13 +00:00
upsertEntry . Sources [ i ] = args . Intention . ToSourceIntention ( false )
2020-10-06 18:24:05 +00:00
found = true
break
}
}
if ! found {
2020-10-06 22:09:13 +00:00
upsertEntry . Sources = append ( upsertEntry . Sources , args . Intention . ToSourceIntention ( false ) )
2020-10-06 18:24:05 +00:00
}
}
case structs . IntentionOpDelete :
// There are two ways to get this request:
//
// 1) legacy: the ID field is populated
// 2) config-entry: the ID field is NOT populated
if args . Intention . ID == "" {
// config-entry style: no LegacyID
legacyWrite = false
args . Intention . DefaultNamespaces ( & entMeta )
prevEntry , err = s . getServiceIntentionsConfigEntry ( args . Intention . DestinationName , args . Intention . DestinationEnterpriseMeta ( ) )
if err != nil {
return err
}
// NOTE: validation errors may be misleading!
noop = true
if prevEntry != nil {
sn := args . Intention . SourceServiceName ( )
upsertEntry = prevEntry . Clone ( )
for i , src := range upsertEntry . Sources {
if src . SourceServiceName ( ) == sn {
// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
// a = append(a[:i], a[i+1:]...)
upsertEntry . Sources = append ( upsertEntry . Sources [ : i ] , upsertEntry . Sources [ i + 1 : ] ... )
if len ( upsertEntry . Sources ) == 0 {
upsertEntry . Sources = nil
}
noop = false
break
}
}
}
} else {
// legacy style: LegacyID required
legacyWrite = true
prevEntry , err = s . prepareApplyDeleteLegacy ( ident , authz , args )
if err != nil {
return err
}
upsertEntry = prevEntry . Clone ( )
for i , src := range upsertEntry . Sources {
if src . LegacyID == args . Intention . ID {
// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
// a = append(a[:i], a[i+1:]...)
upsertEntry . Sources = append ( upsertEntry . Sources [ : i ] , upsertEntry . Sources [ i + 1 : ] ... )
if len ( upsertEntry . Sources ) == 0 {
upsertEntry . Sources = nil
}
break
}
}
}
case structs . IntentionOpDeleteAll :
// This is an internal operation initiated by the leader and is not
// exposed for general RPC use.
fallthrough
2020-01-13 20:51:40 +00:00
default :
return fmt . Errorf ( "Invalid Intention operation: %v" , args . Op )
}
2020-10-06 18:24:05 +00:00
if ! noop && prevEntry != nil && legacyWrite && ! prevEntry . LegacyIDFieldsAreAllSet ( ) {
sn := prevEntry . DestinationServiceName ( )
return fmt . Errorf ( "cannot use legacy intention API to edit intentions with a destination of %q after editing them via a service-intentions config entry" , sn . String ( ) )
}
2020-01-13 20:51:40 +00:00
2020-10-06 18:24:05 +00:00
// setup the reply which will have been filled in by one of the preparedApply* funcs
if legacyWrite {
* reply = args . Intention . ID
} else {
* reply = ""
2018-02-28 18:28:07 +00:00
}
2020-10-06 18:24:05 +00:00
if noop {
return nil
2018-02-28 18:28:07 +00:00
}
2020-10-06 18:24:05 +00:00
// Commit indirectly by invoking the other RPC handler directly.
configReq := & structs . ConfigEntryRequest {
Datacenter : args . Datacenter ,
WriteRequest : args . WriteRequest ,
}
if upsertEntry == nil || len ( upsertEntry . Sources ) == 0 {
configReq . Op = structs . ConfigEntryDelete
configReq . Entry = & structs . ServiceIntentionsConfigEntry {
Kind : structs . ServiceIntentions ,
Name : prevEntry . Name ,
EnterpriseMeta : prevEntry . EnterpriseMeta ,
}
var ignored struct { }
return s . configEntryEndpoint . Delete ( configReq , & ignored )
} else {
// Update config entry CAS
configReq . Op = structs . ConfigEntryUpsertCAS
configReq . Entry = upsertEntry
var normalizeAndValidateFn func ( raw structs . ConfigEntry ) error
if legacyWrite {
normalizeAndValidateFn = func ( raw structs . ConfigEntry ) error {
entry := raw . ( * structs . ServiceIntentionsConfigEntry )
if err := entry . LegacyNormalize ( ) ; err != nil {
return err
}
return entry . LegacyValidate ( )
}
}
var applied bool
err := s . configEntryEndpoint . applyInternal ( configReq , & applied , normalizeAndValidateFn )
if err != nil {
return err
}
if ! applied {
return fmt . Errorf ( "config entry failed to persist due to CAS failure: kind=%q, name=%q" , upsertEntry . Kind , upsertEntry . Name )
}
return nil
}
2018-02-28 18:28:07 +00:00
}
2018-02-28 18:44:49 +00:00
// Get returns a single intention by ID.
func ( s * Intention ) Get (
args * structs . IntentionQueryRequest ,
reply * structs . IndexedIntentions ) error {
// Forward if necessary
2020-07-07 19:45:08 +00:00
if done , err := s . srv . ForwardRPC ( "Intention.Get" , args , args , reply ) ; done {
2018-02-28 18:44:49 +00:00
return err
}
2020-06-26 21:59:15 +00:00
// 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 )
}
}
2018-02-28 18:44:49 +00:00
return s . srv . blockingQuery (
& args . QueryOptions ,
& reply . QueryMeta ,
func ( ws memdb . WatchSet , state * state . Store ) error {
2020-06-26 21:59:15 +00:00
var (
index uint64
ixn * structs . Intention
err error
)
if args . IntentionID != "" {
2020-10-06 18:24:05 +00:00
index , _ , ixn , err = state . IntentionGet ( ws , args . IntentionID )
2020-06-26 21:59:15 +00:00
} else if args . Exact != nil {
2020-10-06 18:24:05 +00:00
index , _ , ixn , err = state . IntentionGetExact ( ws , args . Exact )
2020-06-26 21:59:15 +00:00
}
2018-02-28 18:44:49 +00:00
if err != nil {
return err
}
if ixn == nil {
2018-02-28 23:54:48 +00:00
return ErrIntentionNotFound
2018-02-28 18:44:49 +00:00
}
reply . Index = index
reply . Intentions = structs . Intentions { ixn }
2018-03-04 19:53:52 +00:00
// Filter
if err := s . srv . filterACL ( args . Token , reply ) ; err != nil {
return err
}
// If ACLs prevented any responses, error
if len ( reply . Intentions ) == 0 {
2020-01-27 19:54:32 +00:00
accessorID := s . aclAccessorID ( args . Token )
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
2020-01-28 23:50:41 +00:00
s . logger . Warn ( "Request to get intention denied due to ACLs" , "intention" , args . IntentionID , "accessorID" , accessorID )
2018-03-04 19:53:52 +00:00
return acl . ErrPermissionDenied
}
2018-02-28 18:44:49 +00:00
return nil
} ,
)
}
2018-02-28 18:28:07 +00:00
// List returns all the intentions.
2018-02-28 18:04:27 +00:00
func ( s * Intention ) List (
2020-10-06 18:24:05 +00:00
args * structs . IntentionListRequest ,
2018-02-28 18:04:27 +00:00
reply * structs . IndexedIntentions ) error {
// Forward if necessary
2020-07-07 19:45:08 +00:00
if done , err := s . srv . ForwardRPC ( "Intention.List" , args , args , reply ) ; done {
2018-02-28 18:04:27 +00:00
return err
}
2020-04-07 15:48:44 +00:00
filter , err := bexpr . CreateFilter ( args . Filter , nil , reply . Intentions )
if err != nil {
return err
}
2020-06-26 21:59:15 +00:00
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
}
2018-02-28 18:04:27 +00:00
return s . srv . blockingQuery (
& args . QueryOptions , & reply . QueryMeta ,
func ( ws memdb . WatchSet , state * state . Store ) error {
2020-10-06 18:24:05 +00:00
var (
index uint64
ixns structs . Intentions
fromConfig bool
err error
)
if args . Legacy {
index , ixns , err = state . LegacyIntentions ( ws , & args . EnterpriseMeta )
} else {
index , ixns , fromConfig , err = state . Intentions ( ws , & args . EnterpriseMeta )
}
2018-02-28 18:04:27 +00:00
if err != nil {
return err
}
reply . Index , reply . Intentions = index , ixns
2018-03-06 17:04:44 +00:00
if reply . Intentions == nil {
reply . Intentions = make ( structs . Intentions , 0 )
}
2020-10-06 18:24:05 +00:00
if fromConfig {
reply . DataOrigin = structs . IntentionDataOriginConfigEntries
} else {
reply . DataOrigin = structs . IntentionDataOriginLegacy
}
2020-04-07 15:48:44 +00:00
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
2018-02-28 18:04:27 +00:00
} ,
)
}
2018-03-02 21:40:03 +00:00
// 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
2020-07-07 19:45:08 +00:00
if done , err := s . srv . ForwardRPC ( "Intention.Match" , args , args , reply ) ; done {
2018-03-02 21:40:03 +00:00
return err
}
2018-03-05 02:32:28 +00:00
// Get the ACL token for the request for the checks below.
2020-06-26 21:59:15 +00:00
var entMeta structs . EnterpriseMeta
authz , err := s . srv . ResolveTokenAndDefaultMeta ( args . Token , & entMeta , nil )
2018-03-05 02:32:28 +00:00
if err != nil {
return err
}
2020-06-26 21:59:15 +00:00
// 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 {
2020-01-13 20:51:40 +00:00
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.
2018-03-05 02:32:28 +00:00
for _ , entry := range args . Match . Entries {
2020-01-13 20:51:40 +00:00
entry . FillAuthzContext ( & authzContext )
2020-06-26 21:59:15 +00:00
if prefix := entry . Name ; prefix != "" && authz . IntentionRead ( prefix , & authzContext ) != acl . Allow {
2020-01-27 19:54:32 +00:00
accessorID := s . aclAccessorID ( args . Token )
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
2020-01-28 23:50:41 +00:00
s . logger . Warn ( "Operation on intention prefix denied due to ACLs" , "prefix" , prefix , "accessorID" , accessorID )
2018-03-05 02:32:28 +00:00
return acl . ErrPermissionDenied
}
}
}
2018-03-02 21:40:03 +00:00
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
} ,
)
}
2018-05-11 05:35:47 +00:00
2018-05-11 16:19:22 +00:00
// Check tests a source/destination and returns whether it would be allowed
2018-05-11 05:35:47 +00:00
// or denied based on the current ACL configuration.
2018-05-11 05:37:02 +00:00
//
2020-10-06 22:09:13 +00:00
// NOTE: This endpoint treats any L7 intentions as DENY.
//
2018-05-11 05:37:02 +00:00
// 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.
2018-05-11 16:19:22 +00:00
func ( s * Intention ) Check (
2018-05-11 05:35:47 +00:00
args * structs . IntentionQueryRequest ,
2018-05-11 16:19:22 +00:00
reply * structs . IntentionQueryCheckResponse ) error {
2018-05-11 05:38:13 +00:00
// Forward maybe
2020-07-07 19:45:08 +00:00
if done , err := s . srv . ForwardRPC ( "Intention.Check" , args , args , reply ) ; done {
2018-05-11 05:38:13 +00:00
return err
}
2018-05-11 05:35:47 +00:00
// Get the test args, and defensively guard against nil
2018-05-11 16:19:22 +00:00
query := args . Check
2018-05-11 05:35:47 +00:00
if query == nil {
2018-05-11 16:19:22 +00:00
return errors . New ( "Check must be specified on args" )
2018-05-11 05:35:47 +00:00
}
2020-06-26 21:59:15 +00:00
// 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 )
}
2018-05-11 05:35:47 +00:00
// 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 )
}
2018-05-19 04:03:10 +00:00
// Perform the ACL check. For Check we only require ServiceRead and
// NOT IntentionRead because the Check API only returns pass/fail and
2020-01-13 20:51:40 +00:00
// 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.
2018-05-11 05:35:47 +00:00
if prefix , ok := query . GetACLPrefix ( ) ; ok {
2020-01-13 20:51:40 +00:00
var authzContext acl . AuthorizerContext
query . FillAuthzContext ( & authzContext )
2020-06-26 21:59:15 +00:00
if authz != nil && authz . ServiceRead ( prefix , & authzContext ) != acl . Allow {
2020-01-27 19:54:32 +00:00
accessorID := s . aclAccessorID ( args . Token )
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
2020-01-28 23:50:41 +00:00
s . logger . Warn ( "test on intention denied due to ACLs" , "prefix" , prefix , "accessorID" , accessorID )
2018-05-11 05:35:47 +00:00
return acl . ErrPermissionDenied
}
}
2020-10-06 22:09:13 +00:00
// Note: the default intention policy is like an intention with a
// wildcarded destination in that it is limited to L4-only.
2018-05-11 05:35:47 +00:00
// No match, we need to determine the default behavior. We do this by
2020-10-08 00:35:34 +00:00
// fetching the default intention behavior from the resolved authorizer.
2018-05-11 05:35:47 +00:00
// 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.
2020-10-08 00:35:34 +00:00
defaultDecision := acl . Allow
if authz != nil {
defaultDecision = authz . IntentionDefaultAllow ( nil )
2018-05-11 05:35:47 +00:00
}
2020-10-08 00:35:34 +00:00
state := s . srv . fsm . State ( )
decision , err := state . IntentionDecision ( uri , query . DestinationName , query . DestinationNS , defaultDecision )
if err != nil {
return fmt . Errorf ( "failed to get intention decision from (%s/%s) to (%s/%s): %v" ,
query . SourceNS , query . SourceName , query . DestinationNS , query . DestinationName , err )
2018-05-11 05:35:47 +00:00
}
2020-10-08 00:35:34 +00:00
reply . Allowed = decision . Allowed
2018-05-11 05:35:47 +00:00
return nil
}
2020-01-27 19:54:32 +00:00
// 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 )
2020-01-29 17:16:08 +00:00
if acl . IsErrNotFound ( err ) {
return ""
}
2020-01-27 19:54:32 +00:00
if err != nil {
2020-01-29 17:16:08 +00:00
s . logger . Debug ( "non-critical error resolving acl token accessor for logging" , "error" , err )
2020-01-27 19:54:32 +00:00
return ""
}
if ident == nil {
return ""
}
return ident . ID ( )
}
2020-06-26 21:59:15 +00:00
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
}
2020-10-06 18:24:05 +00:00
func ( s * Intention ) getServiceIntentionsConfigEntry ( name string , entMeta * structs . EnterpriseMeta ) ( * structs . ServiceIntentionsConfigEntry , error ) {
_ , raw , err := s . srv . fsm . State ( ) . ConfigEntry ( nil , structs . ServiceIntentions , name , entMeta )
if err != nil {
return nil , fmt . Errorf ( "Intention lookup failed: %v" , err )
}
if raw == nil {
return nil , nil
}
configEntry , ok := raw . ( * structs . ServiceIntentionsConfigEntry )
if ! ok {
return nil , fmt . Errorf ( "invalid service config type %T" , raw )
}
return configEntry , nil
}
func equalStringMaps ( a , b map [ string ] string ) bool {
if len ( a ) != len ( b ) {
return false
}
for k := range a {
v , ok := b [ k ]
if ! ok || a [ k ] != v {
return false
}
}
return true
}