connect: support defining intentions using layer 7 criteria (#8839)
Extend Consul’s intentions model to allow for request-based access control enforcement for HTTP-like protocols in addition to the existing connection-based enforcement for unspecified protocols (e.g. tcp).
This commit is contained in:
parent
a12056f57c
commit
35c4efd220
|
@ -1341,6 +1341,8 @@ func (s *HTTPHandlers) AgentConnectCALeafCert(resp http.ResponseWriter, req *htt
|
||||||
//
|
//
|
||||||
// POST /v1/agent/connect/authorize
|
// POST /v1/agent/connect/authorize
|
||||||
//
|
//
|
||||||
|
// NOTE: This endpoint treats any L7 intentions as DENY.
|
||||||
|
//
|
||||||
// Note: when this logic changes, consider if the Intention.Check RPC method
|
// Note: when this logic changes, consider if the Intention.Check RPC method
|
||||||
// also needs to be updated.
|
// also needs to be updated.
|
||||||
func (s *HTTPHandlers) AgentConnectAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
func (s *HTTPHandlers) AgentConnectAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
|
|
|
@ -11,10 +11,15 @@ import (
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO(rb/intentions): this should move back into the agent endpoint since
|
||||||
|
// there is no ext_authz implementation anymore.
|
||||||
|
//
|
||||||
// ConnectAuthorize implements the core authorization logic for Connect. It's in
|
// ConnectAuthorize implements the core authorization logic for Connect. It's in
|
||||||
// a separate agent method here because we need to re-use this both in our own
|
// a separate agent method here because we need to re-use this both in our own
|
||||||
// HTTP API authz endpoint and in the gRPX xDS/ext_authz API for envoy.
|
// HTTP API authz endpoint and in the gRPX xDS/ext_authz API for envoy.
|
||||||
//
|
//
|
||||||
|
// NOTE: This treats any L7 intentions as DENY.
|
||||||
|
//
|
||||||
// The ACL token and the auth request are provided and the auth decision (true
|
// The ACL token and the auth request are provided and the auth decision (true
|
||||||
// means authorized) and reason string are returned.
|
// means authorized) and reason string are returned.
|
||||||
//
|
//
|
||||||
|
@ -97,12 +102,26 @@ func (a *Agent) ConnectAuthorize(token string,
|
||||||
return returnErr(fmt.Errorf("Internal error loading matches"))
|
return returnErr(fmt.Errorf("Internal error loading matches"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the authorization for each match
|
// Figure out which source matches this request.
|
||||||
|
var ixnMatch *structs.Intention
|
||||||
for _, ixn := range reply.Matches[0] {
|
for _, ixn := range reply.Matches[0] {
|
||||||
if auth, ok := uriService.Authorize(ixn); ok {
|
if _, ok := uriService.Authorize(ixn); ok {
|
||||||
reason = fmt.Sprintf("Matched intention: %s", ixn.String())
|
ixnMatch = ixn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ixnMatch != nil {
|
||||||
|
if len(ixnMatch.Permissions) == 0 {
|
||||||
|
// This is an L4 intention.
|
||||||
|
reason = fmt.Sprintf("Matched L4 intention: %s", ixnMatch.String())
|
||||||
|
auth := ixnMatch.Action == structs.IntentionActionAllow
|
||||||
return auth, reason, &meta, nil
|
return auth, reason, &meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is an L7 intention, so DENY.
|
||||||
|
reason = fmt.Sprintf("Matched L7 intention: %s", ixnMatch.String())
|
||||||
|
return false, reason, &meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// No match, we need to determine the default behavior. We do this by
|
// No match, we need to determine the default behavior. We do this by
|
||||||
|
|
|
@ -288,10 +288,10 @@ func (s *Intention) Apply(
|
||||||
}
|
}
|
||||||
|
|
||||||
if prevEntry == nil {
|
if prevEntry == nil {
|
||||||
upsertEntry = args.Intention.ToConfigEntry()
|
upsertEntry = args.Intention.ToConfigEntry(true)
|
||||||
} else {
|
} else {
|
||||||
upsertEntry = prevEntry.Clone()
|
upsertEntry = prevEntry.Clone()
|
||||||
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention())
|
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
case structs.IntentionOpUpdate:
|
case structs.IntentionOpUpdate:
|
||||||
|
@ -306,7 +306,7 @@ func (s *Intention) Apply(
|
||||||
upsertEntry = prevEntry.Clone()
|
upsertEntry = prevEntry.Clone()
|
||||||
for i, src := range upsertEntry.Sources {
|
for i, src := range upsertEntry.Sources {
|
||||||
if src.LegacyID == args.Intention.ID {
|
if src.LegacyID == args.Intention.ID {
|
||||||
upsertEntry.Sources[i] = args.Intention.ToSourceIntention()
|
upsertEntry.Sources[i] = args.Intention.ToSourceIntention(true)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -339,7 +339,7 @@ func (s *Intention) Apply(
|
||||||
return fmt.Errorf("Meta must not be specified")
|
return fmt.Errorf("Meta must not be specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
upsertEntry = args.Intention.ToConfigEntry()
|
upsertEntry = args.Intention.ToConfigEntry(false)
|
||||||
} else {
|
} else {
|
||||||
upsertEntry = prevEntry.Clone()
|
upsertEntry = prevEntry.Clone()
|
||||||
|
|
||||||
|
@ -363,13 +363,13 @@ func (s *Intention) Apply(
|
||||||
found := false
|
found := false
|
||||||
for i, src := range upsertEntry.Sources {
|
for i, src := range upsertEntry.Sources {
|
||||||
if src.SourceServiceName() == sn {
|
if src.SourceServiceName() == sn {
|
||||||
upsertEntry.Sources[i] = args.Intention.ToSourceIntention()
|
upsertEntry.Sources[i] = args.Intention.ToSourceIntention(false)
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention())
|
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention(false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -712,6 +712,8 @@ func (s *Intention) Match(
|
||||||
// Check tests a source/destination and returns whether it would be allowed
|
// Check tests a source/destination and returns whether it would be allowed
|
||||||
// or denied based on the current ACL configuration.
|
// or denied based on the current ACL configuration.
|
||||||
//
|
//
|
||||||
|
// NOTE: This endpoint treats any L7 intentions as DENY.
|
||||||
|
//
|
||||||
// Note: Whenever the logic for this method is changed, you should take
|
// Note: Whenever the logic for this method is changed, you should take
|
||||||
// a look at the agent authorize endpoint (agent/agent_endpoint.go) since
|
// a look at the agent authorize endpoint (agent/agent_endpoint.go) since
|
||||||
// the logic there is similar.
|
// the logic there is similar.
|
||||||
|
@ -802,14 +804,30 @@ func (s *Intention) Check(
|
||||||
return errors.New("internal error loading matches")
|
return errors.New("internal error loading matches")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the authorization for each match
|
// Figure out which source matches this request.
|
||||||
|
var ixnMatch *structs.Intention
|
||||||
for _, ixn := range matches[0] {
|
for _, ixn := range matches[0] {
|
||||||
if auth, ok := uri.Authorize(ixn); ok {
|
if _, ok := uri.Authorize(ixn); ok {
|
||||||
reply.Allowed = auth
|
ixnMatch = ixn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ixnMatch != nil {
|
||||||
|
if len(ixnMatch.Permissions) == 0 {
|
||||||
|
// This is an L4 intention.
|
||||||
|
reply.Allowed = ixnMatch.Action == structs.IntentionActionAllow
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is an L7 intention, so DENY.
|
||||||
|
reply.Allowed = false
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: the default intention policy is like an intention with a
|
||||||
|
// wildcarded destination in that it is limited to L4-only.
|
||||||
|
|
||||||
// No match, we need to determine the default behavior. We do this by
|
// No match, we need to determine the default behavior. We do this by
|
||||||
// specifying the anonymous token token, which will get that behavior.
|
// specifying the anonymous token token, which will get that behavior.
|
||||||
// The default behavior if ACLs are disabled is to allow connections
|
// The default behavior if ACLs are disabled is to allow connections
|
||||||
|
|
|
@ -343,6 +343,22 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
||||||
|
|
||||||
defaultEntMeta := structs.DefaultEnterpriseMeta()
|
defaultEntMeta := structs.DefaultEnterpriseMeta()
|
||||||
|
|
||||||
|
// Force "test" to be L7-capable.
|
||||||
|
{
|
||||||
|
args := structs.ConfigEntryRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "test",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bool
|
||||||
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &args, &out))
|
||||||
|
require.True(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
opApply := func(req *structs.IntentionRequest) error {
|
opApply := func(req *structs.IntentionRequest) error {
|
||||||
req.Datacenter = "dc1"
|
req.Datacenter = "dc1"
|
||||||
var ignored string
|
var ignored string
|
||||||
|
@ -419,6 +435,10 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
||||||
got := resp.Intentions[0]
|
got := resp.Intentions[0]
|
||||||
require.Equal(t, "original", got.Description)
|
require.Equal(t, "original", got.Description)
|
||||||
|
|
||||||
|
// L4
|
||||||
|
require.Equal(t, structs.IntentionActionAllow, got.Action)
|
||||||
|
require.Empty(t, got.Permissions)
|
||||||
|
|
||||||
// Verify it is in the new-style.
|
// Verify it is in the new-style.
|
||||||
require.Empty(t, got.ID)
|
require.Empty(t, got.ID)
|
||||||
require.True(t, got.CreatedAt.IsZero())
|
require.True(t, got.CreatedAt.IsZero())
|
||||||
|
@ -483,6 +503,10 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
||||||
got := resp.Intentions[0]
|
got := resp.Intentions[0]
|
||||||
require.Equal(t, "updated", got.Description)
|
require.Equal(t, "updated", got.Description)
|
||||||
|
|
||||||
|
// L4
|
||||||
|
require.Equal(t, structs.IntentionActionAllow, got.Action)
|
||||||
|
require.Empty(t, got.Permissions)
|
||||||
|
|
||||||
// Verify it is in the new-style.
|
// Verify it is in the new-style.
|
||||||
require.Empty(t, got.ID)
|
require.Empty(t, got.ID)
|
||||||
require.True(t, got.CreatedAt.IsZero())
|
require.True(t, got.CreatedAt.IsZero())
|
||||||
|
@ -502,8 +526,15 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
||||||
Intention: &structs.Intention{
|
Intention: &structs.Intention{
|
||||||
SourceName: "assay",
|
SourceName: "assay",
|
||||||
DestinationName: "test",
|
DestinationName: "test",
|
||||||
Action: structs.IntentionActionDeny,
|
|
||||||
Description: "original-2",
|
Description: "original-2",
|
||||||
|
Permissions: []*structs.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -521,6 +552,17 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
||||||
got := resp.Intentions[0]
|
got := resp.Intentions[0]
|
||||||
require.Equal(t, "original-2", got.Description)
|
require.Equal(t, "original-2", got.Description)
|
||||||
|
|
||||||
|
// L7
|
||||||
|
require.Empty(t, got.Action)
|
||||||
|
require.Equal(t, []*structs.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, got.Permissions)
|
||||||
|
|
||||||
// Verify it is in the new-style.
|
// Verify it is in the new-style.
|
||||||
require.Empty(t, got.ID)
|
require.Empty(t, got.ID)
|
||||||
require.True(t, got.CreatedAt.IsZero())
|
require.True(t, got.CreatedAt.IsZero())
|
||||||
|
@ -556,10 +598,17 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "assay",
|
Name: "assay",
|
||||||
EnterpriseMeta: *defaultEntMeta,
|
EnterpriseMeta: *defaultEntMeta,
|
||||||
Action: structs.IntentionActionDeny,
|
|
||||||
Description: "original-2",
|
Description: "original-2",
|
||||||
Precedence: 9,
|
Precedence: 9,
|
||||||
Type: structs.IntentionSourceConsul,
|
Type: structs.IntentionSourceConsul,
|
||||||
|
Permissions: []*structs.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
RaftIndex: entry.RaftIndex,
|
RaftIndex: entry.RaftIndex,
|
||||||
|
@ -609,10 +658,17 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "assay",
|
Name: "assay",
|
||||||
EnterpriseMeta: *defaultEntMeta,
|
EnterpriseMeta: *defaultEntMeta,
|
||||||
Action: structs.IntentionActionDeny,
|
|
||||||
Description: "original-2",
|
Description: "original-2",
|
||||||
Precedence: 9,
|
Precedence: 9,
|
||||||
Type: structs.IntentionSourceConsul,
|
Type: structs.IntentionSourceConsul,
|
||||||
|
Permissions: []*structs.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
RaftIndex: entry.RaftIndex,
|
RaftIndex: entry.RaftIndex,
|
||||||
|
|
|
@ -383,6 +383,16 @@ func (s *Server) replicateLegacyIntentionsOnce(ctx context.Context, lastFetchInd
|
||||||
return 0, false, err
|
return 0, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do a quick sanity check that somehow Permissions didn't slip through.
|
||||||
|
// This shouldn't be necessary, but one extra check isn't going to hurt
|
||||||
|
// anything.
|
||||||
|
for _, ixn := range local {
|
||||||
|
if len(ixn.Permissions) > 0 {
|
||||||
|
// Assume that the data origin has switched to config entries.
|
||||||
|
return 0, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compute the diff between the remote and local intentions.
|
// Compute the diff between the remote and local intentions.
|
||||||
deletes, updates := diffIntentions(local, remote.Intentions)
|
deletes, updates := diffIntentions(local, remote.Intentions)
|
||||||
txnOpSets := batchLegacyIntentionUpdates(deletes, updates)
|
txnOpSets := batchLegacyIntentionUpdates(deletes, updates)
|
||||||
|
|
|
@ -324,7 +324,7 @@ func insertConfigEntryWithTxn(tx *txn, idx uint64, conf structs.ConfigEntry) err
|
||||||
func validateProposedConfigEntryInGraph(
|
func validateProposedConfigEntryInGraph(
|
||||||
tx ReadTxn,
|
tx ReadTxn,
|
||||||
kind, name string,
|
kind, name string,
|
||||||
next structs.ConfigEntry,
|
proposedEntry structs.ConfigEntry,
|
||||||
entMeta *structs.EnterpriseMeta,
|
entMeta *structs.EnterpriseMeta,
|
||||||
) error {
|
) error {
|
||||||
validateAllChains := false
|
validateAllChains := false
|
||||||
|
@ -350,14 +350,11 @@ func validateProposedConfigEntryInGraph(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case structs.ServiceIntentions:
|
case structs.ServiceIntentions:
|
||||||
// TODO(rb): should this validate protocols?
|
|
||||||
return nil
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unhandled kind %q during validation of %q", kind, name)
|
return fmt.Errorf("unhandled kind %q during validation of %q", kind, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return validateProposedConfigEntryInServiceGraph(tx, kind, name, next, validateAllChains, entMeta)
|
return validateProposedConfigEntryInServiceGraph(tx, kind, name, proposedEntry, validateAllChains, entMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGatewayClash(
|
func checkGatewayClash(
|
||||||
|
@ -475,7 +472,7 @@ func (s *Store) discoveryChainSourcesTxn(tx ReadTxn, ws memdb.WatchSet, dc strin
|
||||||
func validateProposedConfigEntryInServiceGraph(
|
func validateProposedConfigEntryInServiceGraph(
|
||||||
tx ReadTxn,
|
tx ReadTxn,
|
||||||
kind, name string,
|
kind, name string,
|
||||||
next structs.ConfigEntry,
|
proposedEntry structs.ConfigEntry,
|
||||||
validateAllChains bool,
|
validateAllChains bool,
|
||||||
entMeta *structs.EnterpriseMeta,
|
entMeta *structs.EnterpriseMeta,
|
||||||
) error {
|
) error {
|
||||||
|
@ -484,6 +481,7 @@ func validateProposedConfigEntryInServiceGraph(
|
||||||
var (
|
var (
|
||||||
checkChains = make(map[structs.ServiceID]struct{})
|
checkChains = make(map[structs.ServiceID]struct{})
|
||||||
checkIngress []*structs.IngressGatewayConfigEntry
|
checkIngress []*structs.IngressGatewayConfigEntry
|
||||||
|
checkIntentions []*structs.ServiceIntentionsConfigEntry
|
||||||
enforceIngressProtocolsMatch bool
|
enforceIngressProtocolsMatch bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -503,11 +501,11 @@ func validateProposedConfigEntryInServiceGraph(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entries, err := configEntriesByKindTxn(tx, nil, structs.IngressGateway, structs.WildcardEnterpriseMeta())
|
_, ingressEntries, err := configEntriesByKindTxn(tx, nil, structs.IngressGateway, structs.WildcardEnterpriseMeta())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range ingressEntries {
|
||||||
ingress, ok := entry.(*structs.IngressGatewayConfigEntry)
|
ingress, ok := entry.(*structs.IngressGatewayConfigEntry)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("type %T is not an ingress gateway config entry", entry)
|
return fmt.Errorf("type %T is not an ingress gateway config entry", entry)
|
||||||
|
@ -515,17 +513,43 @@ func validateProposedConfigEntryInServiceGraph(
|
||||||
checkIngress = append(checkIngress, ingress)
|
checkIngress = append(checkIngress, ingress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, ixnEntries, err := configEntriesByKindTxn(tx, nil, structs.ServiceIntentions, structs.WildcardEnterpriseMeta())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range ixnEntries {
|
||||||
|
ixn, ok := entry.(*structs.ServiceIntentionsConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("type %T is not a service intentions config entry", entry)
|
||||||
|
}
|
||||||
|
checkIntentions = append(checkIntentions, ixn)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if kind == structs.ServiceIntentions {
|
||||||
|
// Check that the protocols match.
|
||||||
|
|
||||||
|
// This is the case for deleting a config entry
|
||||||
|
if proposedEntry == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ixn, ok := proposedEntry.(*structs.ServiceIntentionsConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("type %T is not a service intentions config entry", proposedEntry)
|
||||||
|
}
|
||||||
|
checkIntentions = append(checkIntentions, ixn)
|
||||||
|
|
||||||
} else if kind == structs.IngressGateway {
|
} else if kind == structs.IngressGateway {
|
||||||
// Checking an ingress pointing to multiple chains.
|
// Checking an ingress pointing to multiple chains.
|
||||||
|
|
||||||
// This is the case for deleting a config entry
|
// This is the case for deleting a config entry
|
||||||
if next == nil {
|
if proposedEntry == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ingress, ok := next.(*structs.IngressGatewayConfigEntry)
|
ingress, ok := proposedEntry.(*structs.IngressGatewayConfigEntry)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("type %T is not an ingress gateway config entry", next)
|
return fmt.Errorf("type %T is not an ingress gateway config entry", proposedEntry)
|
||||||
}
|
}
|
||||||
checkIngress = append(checkIngress, ingress)
|
checkIngress = append(checkIngress, ingress)
|
||||||
|
|
||||||
|
@ -536,6 +560,28 @@ func validateProposedConfigEntryInServiceGraph(
|
||||||
} else {
|
} else {
|
||||||
// Must be a single chain.
|
// Must be a single chain.
|
||||||
|
|
||||||
|
// Check to see if we should ensure L7 intentions have an L7 protocol.
|
||||||
|
_, ixn, err := getServiceIntentionsConfigEntryTxn(
|
||||||
|
tx, nil, name, nil, entMeta,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if ixn != nil {
|
||||||
|
checkIntentions = append(checkIntentions, ixn)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ixnEntries, err := configEntriesByKindTxn(tx, nil, structs.ServiceIntentions, structs.WildcardEnterpriseMeta())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range ixnEntries {
|
||||||
|
ixn, ok := entry.(*structs.ServiceIntentionsConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("type %T is not a service intentions config entry", entry)
|
||||||
|
}
|
||||||
|
checkIntentions = append(checkIntentions, ixn)
|
||||||
|
}
|
||||||
|
|
||||||
sid := structs.NewServiceID(name, entMeta)
|
sid := structs.NewServiceID(name, entMeta)
|
||||||
checkChains[sid] = struct{}{}
|
checkChains[sid] = struct{}{}
|
||||||
|
|
||||||
|
@ -559,16 +605,20 @@ func validateProposedConfigEntryInServiceGraph(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure if any ingress is affected that we fetch all of the chains needed
|
// Ensure if any ingress or intention is affected that we fetch all of the
|
||||||
// to fully validate that ingress.
|
// chains needed to fully validate them.
|
||||||
for _, ingress := range checkIngress {
|
for _, ingress := range checkIngress {
|
||||||
for _, svcID := range ingress.ListRelatedServices() {
|
for _, svcID := range ingress.ListRelatedServices() {
|
||||||
checkChains[svcID] = struct{}{}
|
checkChains[svcID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, ixn := range checkIntentions {
|
||||||
|
sn := ixn.DestinationServiceName()
|
||||||
|
checkChains[sn.ToServiceID()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
overrides := map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
overrides := map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
structs.NewConfigEntryKindName(kind, name, entMeta): next,
|
structs.NewConfigEntryKindName(kind, name, entMeta): proposedEntry,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -619,6 +669,25 @@ func validateProposedConfigEntryInServiceGraph(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now validate that intentions with L7 permissions reference HTTP services
|
||||||
|
for _, e := range checkIntentions {
|
||||||
|
// We only have to double check things that try to use permissions
|
||||||
|
if e.HasWildcardDestination() || !e.HasAnyPermissions() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sn := e.DestinationServiceName()
|
||||||
|
svcID := sn.ToServiceID()
|
||||||
|
|
||||||
|
svcProto := svcProtocols[svcID]
|
||||||
|
if !structs.IsProtocolHTTPLike(svcProto) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"service %q has protocol %q, which is incompatible with L7 intentions permissions",
|
||||||
|
svcID.String(),
|
||||||
|
svcProto,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2038,3 +2038,235 @@ func TestTargetsForSource(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStore_ValidateServiceIntentionsErrorOnIncompatibleProtocols(t *testing.T) {
|
||||||
|
l7perms := []*structs.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/v2/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceDefaults := func(service, protocol string) *structs.ServiceConfigEntry {
|
||||||
|
return &structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: service,
|
||||||
|
Protocol: protocol,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyDefaults := func(protocol string) *structs.ProxyConfigEntry {
|
||||||
|
return &structs.ProxyConfigEntry{
|
||||||
|
Kind: structs.ProxyDefaults,
|
||||||
|
Name: structs.ProxyConfigGlobal,
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"protocol": protocol,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type operation struct {
|
||||||
|
entry structs.ConfigEntry
|
||||||
|
deletion bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type testcase struct {
|
||||||
|
ops []operation
|
||||||
|
expectLastErr string
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := map[string]testcase{
|
||||||
|
"L4 intention cannot upgrade to L7 when tcp": {
|
||||||
|
ops: []operation{
|
||||||
|
{ // set the target service as tcp
|
||||||
|
entry: serviceDefaults("api", "tcp"),
|
||||||
|
},
|
||||||
|
{ // create an L4 intention
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Action: structs.IntentionActionAllow},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // Should fail if converted to L7
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Permissions: l7perms},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectLastErr: `has protocol "tcp"`,
|
||||||
|
},
|
||||||
|
"L4 intention can upgrade to L7 when made http via service-defaults": {
|
||||||
|
ops: []operation{
|
||||||
|
{ // set the target service as tcp
|
||||||
|
entry: serviceDefaults("api", "tcp"),
|
||||||
|
},
|
||||||
|
{ // create an L4 intention
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Action: structs.IntentionActionAllow},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // set the target service as http
|
||||||
|
entry: serviceDefaults("api", "http"),
|
||||||
|
},
|
||||||
|
{ // Should succeed if converted to L7
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Permissions: l7perms},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"L4 intention can upgrade to L7 when made http via proxy-defaults": {
|
||||||
|
ops: []operation{
|
||||||
|
{ // set the target service as tcp
|
||||||
|
entry: proxyDefaults("tcp"),
|
||||||
|
},
|
||||||
|
{ // create an L4 intention
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Action: structs.IntentionActionAllow},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // set the target service as http
|
||||||
|
entry: proxyDefaults("http"),
|
||||||
|
},
|
||||||
|
{ // Should succeed if converted to L7
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Permissions: l7perms},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"L7 intention cannot have protocol downgraded to tcp via modification via service-defaults": {
|
||||||
|
ops: []operation{
|
||||||
|
{ // set the target service as http
|
||||||
|
entry: serviceDefaults("api", "http"),
|
||||||
|
},
|
||||||
|
{ // create an L7 intention
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Permissions: l7perms},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // setting the target service as tcp should fail
|
||||||
|
entry: serviceDefaults("api", "tcp"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectLastErr: `has protocol "tcp"`,
|
||||||
|
},
|
||||||
|
"L7 intention cannot have protocol downgraded to tcp via modification via proxy-defaults": {
|
||||||
|
ops: []operation{
|
||||||
|
{ // set the target service as http
|
||||||
|
entry: proxyDefaults("http"),
|
||||||
|
},
|
||||||
|
{ // create an L7 intention
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Permissions: l7perms},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // setting the target service as tcp should fail
|
||||||
|
entry: proxyDefaults("tcp"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectLastErr: `has protocol "tcp"`,
|
||||||
|
},
|
||||||
|
"L7 intention cannot have protocol downgraded to tcp via deletion of service-defaults": {
|
||||||
|
ops: []operation{
|
||||||
|
{ // set the target service as http
|
||||||
|
entry: serviceDefaults("api", "http"),
|
||||||
|
},
|
||||||
|
{ // create an L7 intention
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Permissions: l7perms},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // setting the target service as tcp should fail
|
||||||
|
entry: serviceDefaults("api", "tcp"),
|
||||||
|
deletion: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectLastErr: `has protocol "tcp"`,
|
||||||
|
},
|
||||||
|
"L7 intention cannot have protocol downgraded to tcp via deletion of proxy-defaults": {
|
||||||
|
ops: []operation{
|
||||||
|
{ // set the target service as http
|
||||||
|
entry: proxyDefaults("http"),
|
||||||
|
},
|
||||||
|
{ // create an L7 intention
|
||||||
|
entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "api",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{Name: "web", Permissions: l7perms},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // setting the target service as tcp should fail
|
||||||
|
entry: proxyDefaults("tcp"),
|
||||||
|
deletion: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectLastErr: `has protocol "tcp"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
s := testStateStore(t)
|
||||||
|
|
||||||
|
var nextIndex = uint64(1)
|
||||||
|
|
||||||
|
for i, op := range tc.ops {
|
||||||
|
isLast := (i == len(tc.ops)-1)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if op.deletion {
|
||||||
|
err = s.DeleteConfigEntry(nextIndex, op.entry.GetKind(), op.entry.GetName(), nil)
|
||||||
|
} else {
|
||||||
|
err = s.EnsureConfigEntry(nextIndex, op.entry, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLast && tc.expectLastErr != "" {
|
||||||
|
testutil.RequireErrorContains(t, err, `has protocol "tcp"`)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -108,15 +109,12 @@ func (e *ServiceRouterConfigEntry) Normalize() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
httpMatch := route.Match.HTTP
|
httpMatch := route.Match.HTTP
|
||||||
if len(httpMatch.Methods) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for j := 0; j < len(httpMatch.Methods); j++ {
|
for j := 0; j < len(httpMatch.Methods); j++ {
|
||||||
httpMatch.Methods[j] = strings.ToUpper(httpMatch.Methods[j])
|
httpMatch.Methods[j] = strings.ToUpper(httpMatch.Methods[j])
|
||||||
}
|
}
|
||||||
|
|
||||||
if route.Destination != nil && route.Destination.Namespace == "" {
|
if route.Destination != nil && route.Destination.Namespace == "" {
|
||||||
route.Destination.Namespace = e.EnterpriseMeta.NamespaceOrDefault()
|
route.Destination.Namespace = e.EnterpriseMeta.NamespaceOrEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,6 +207,9 @@ func (e *ServiceRouterConfigEntry) Validate() error {
|
||||||
if len(route.Match.HTTP.Methods) > 0 {
|
if len(route.Match.HTTP.Methods) > 0 {
|
||||||
found := make(map[string]struct{})
|
found := make(map[string]struct{})
|
||||||
for _, m := range route.Match.HTTP.Methods {
|
for _, m := range route.Match.HTTP.Methods {
|
||||||
|
if !isValidHTTPMethod(m) {
|
||||||
|
return fmt.Errorf("Route[%d] Methods contains an invalid method %q", i, m)
|
||||||
|
}
|
||||||
if _, ok := found[m]; ok {
|
if _, ok := found[m]; ok {
|
||||||
return fmt.Errorf("Route[%d] Methods contains %q more than once", i, m)
|
return fmt.Errorf("Route[%d] Methods contains %q more than once", i, m)
|
||||||
}
|
}
|
||||||
|
@ -227,6 +228,23 @@ func (e *ServiceRouterConfigEntry) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidHTTPMethod(method string) bool {
|
||||||
|
switch method {
|
||||||
|
case http.MethodGet,
|
||||||
|
http.MethodHead,
|
||||||
|
http.MethodPost,
|
||||||
|
http.MethodPut,
|
||||||
|
http.MethodPatch,
|
||||||
|
http.MethodDelete,
|
||||||
|
http.MethodConnect,
|
||||||
|
http.MethodOptions,
|
||||||
|
http.MethodTrace:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *ServiceRouterConfigEntry) CanRead(rule acl.Authorizer) bool {
|
func (e *ServiceRouterConfigEntry) CanRead(rule acl.Authorizer) bool {
|
||||||
return canReadDiscoveryChain(e, rule)
|
return canReadDiscoveryChain(e, rule)
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,7 @@ func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intent
|
||||||
SourceName: src.Name,
|
SourceName: src.Name,
|
||||||
SourceType: src.Type,
|
SourceType: src.Type,
|
||||||
Action: src.Action,
|
Action: src.Action,
|
||||||
|
Permissions: src.Permissions,
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
Precedence: src.Precedence,
|
Precedence: src.Precedence,
|
||||||
DestinationNS: e.NamespaceOrDefault(),
|
DestinationNS: e.NamespaceOrDefault(),
|
||||||
|
@ -135,7 +136,27 @@ type SourceIntention struct {
|
||||||
// Action is whether this is an allowlist or denylist intention.
|
// Action is whether this is an allowlist or denylist intention.
|
||||||
//
|
//
|
||||||
// formerly Intention.Action
|
// formerly Intention.Action
|
||||||
Action IntentionAction
|
//
|
||||||
|
// NOTE: this is mutually exclusive with the Permissions field.
|
||||||
|
Action IntentionAction `json:",omitempty"`
|
||||||
|
|
||||||
|
// Permissions is the list of additional L7 attributes that extend the
|
||||||
|
// intention definition.
|
||||||
|
//
|
||||||
|
// Permissions are interpreted in the order represented in the slice. In
|
||||||
|
// default-deny mode, deny permissions are logically subtracted from all
|
||||||
|
// following allow permissions. Multiple allow permissions are then ORed
|
||||||
|
// together.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
// ["deny /v2/admin", "allow /v2/*", "allow GET /healthz"]
|
||||||
|
//
|
||||||
|
// Is logically interpreted as:
|
||||||
|
// allow: [
|
||||||
|
// "(/v2/*) AND NOT (/v2/admin)",
|
||||||
|
// "(GET /healthz) AND NOT (/v2/admin)"
|
||||||
|
// ]
|
||||||
|
Permissions []*IntentionPermission `json:",omitempty"`
|
||||||
|
|
||||||
// Precedence is the order that the intention will be applied, with
|
// Precedence is the order that the intention will be applied, with
|
||||||
// larger numbers being applied first. This is a read-only field, on
|
// larger numbers being applied first. This is a read-only field, on
|
||||||
|
@ -182,6 +203,65 @@ type SourceIntention struct {
|
||||||
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IntentionPermission struct {
|
||||||
|
Action IntentionAction // required: allow|deny
|
||||||
|
|
||||||
|
HTTP *IntentionHTTPPermission `json:",omitempty"`
|
||||||
|
|
||||||
|
// If we have non-http match criteria for other protocols
|
||||||
|
// in the future (gRPC, redis, etc) they can go here.
|
||||||
|
|
||||||
|
// Support for edge-decoded JWTs would likely be configured
|
||||||
|
// in a new top level section here.
|
||||||
|
|
||||||
|
// If we ever add Sentinel support, this is one place we may
|
||||||
|
// wish to add it.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IntentionPermission) Clone() *IntentionPermission {
|
||||||
|
p2 := *p
|
||||||
|
if p.HTTP != nil {
|
||||||
|
p2.HTTP = p.HTTP.Clone()
|
||||||
|
}
|
||||||
|
return &p2
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntentionHTTPPermission struct {
|
||||||
|
// PathExact, PathPrefix, and PathRegex are mutually exclusive.
|
||||||
|
PathExact string `json:",omitempty" alias:"path_exact"`
|
||||||
|
PathPrefix string `json:",omitempty" alias:"path_prefix"`
|
||||||
|
PathRegex string `json:",omitempty" alias:"path_regex"`
|
||||||
|
|
||||||
|
Header []IntentionHTTPHeaderPermission `json:",omitempty"`
|
||||||
|
|
||||||
|
Methods []string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IntentionHTTPPermission) Clone() *IntentionHTTPPermission {
|
||||||
|
p2 := *p
|
||||||
|
|
||||||
|
if len(p.Header) > 0 {
|
||||||
|
p2.Header = make([]IntentionHTTPHeaderPermission, 0, len(p.Header))
|
||||||
|
for _, hdr := range p.Header {
|
||||||
|
p2.Header = append(p2.Header, hdr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p2.Methods = cloneStringSlice(p.Methods)
|
||||||
|
|
||||||
|
return &p2
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntentionHTTPHeaderPermission struct {
|
||||||
|
Name string
|
||||||
|
Present bool `json:",omitempty"`
|
||||||
|
Exact string `json:",omitempty"`
|
||||||
|
Prefix string `json:",omitempty"`
|
||||||
|
Suffix string `json:",omitempty"`
|
||||||
|
Regex string `json:",omitempty"`
|
||||||
|
Invert bool `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func cloneStringStringMap(m map[string]string) map[string]string {
|
func cloneStringStringMap(m map[string]string) map[string]string {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -202,6 +282,13 @@ func (x *SourceIntention) Clone() *SourceIntention {
|
||||||
|
|
||||||
x2.LegacyMeta = cloneStringStringMap(x.LegacyMeta)
|
x2.LegacyMeta = cloneStringStringMap(x.LegacyMeta)
|
||||||
|
|
||||||
|
if len(x.Permissions) > 0 {
|
||||||
|
x2.Permissions = make([]*IntentionPermission, 0, len(x.Permissions))
|
||||||
|
for _, perm := range x.Permissions {
|
||||||
|
x2.Permissions = append(x2.Permissions, perm.Clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &x2
|
return &x2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,6 +390,16 @@ func (e *ServiceIntentionsConfigEntry) normalize(legacyWrite bool) error {
|
||||||
src.LegacyCreateTime = nil
|
src.LegacyCreateTime = nil
|
||||||
src.LegacyUpdateTime = nil
|
src.LegacyUpdateTime = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, perm := range src.Permissions {
|
||||||
|
if perm.HTTP == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := 0; j < len(perm.HTTP.Methods); j++ {
|
||||||
|
perm.HTTP.Methods[j] = strings.ToUpper(perm.HTTP.Methods[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The source intentions closer to the head of the list have higher
|
// The source intentions closer to the head of the list have higher
|
||||||
|
@ -370,6 +467,20 @@ func (e *ServiceIntentionsConfigEntry) LegacyValidate() error {
|
||||||
return e.validate(true)
|
return e.validate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *ServiceIntentionsConfigEntry) HasWildcardDestination() bool {
|
||||||
|
dstNS := e.EnterpriseMeta.NamespaceOrDefault()
|
||||||
|
return dstNS == WildcardSpecifier || e.Name == WildcardSpecifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ServiceIntentionsConfigEntry) HasAnyPermissions() bool {
|
||||||
|
for _, src := range e.Sources {
|
||||||
|
if len(src.Permissions) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
||||||
if e.Name == "" {
|
if e.Name == "" {
|
||||||
return fmt.Errorf("Name is required")
|
return fmt.Errorf("Name is required")
|
||||||
|
@ -379,6 +490,8 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destIsWild := e.HasWildcardDestination()
|
||||||
|
|
||||||
if legacyWrite {
|
if legacyWrite {
|
||||||
if len(e.Meta) > 0 {
|
if len(e.Meta) > 0 {
|
||||||
return fmt.Errorf("Meta must be omitted for legacy intention writes")
|
return fmt.Errorf("Meta must be omitted for legacy intention writes")
|
||||||
|
@ -445,11 +558,21 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if legacyWrite || len(src.Permissions) == 0 {
|
||||||
switch src.Action {
|
switch src.Action {
|
||||||
case IntentionActionAllow, IntentionActionDeny:
|
case IntentionActionAllow, IntentionActionDeny:
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Sources[%d].Action must be set to 'allow' or 'deny'", i)
|
return fmt.Errorf("Sources[%d].Action must be set to 'allow' or 'deny'", i)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(src.Permissions) > 0 && src.Action != "" {
|
||||||
|
return fmt.Errorf("Sources[%d].Action must be omitted if Permissions are specified", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if destIsWild && len(src.Permissions) > 0 {
|
||||||
|
return fmt.Errorf("Sources[%d].Permissions cannot be specified on intentions with wildcarded destinations", i)
|
||||||
|
}
|
||||||
|
|
||||||
switch src.Type {
|
switch src.Type {
|
||||||
case IntentionSourceConsul:
|
case IntentionSourceConsul:
|
||||||
|
@ -457,6 +580,94 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
||||||
return fmt.Errorf("Sources[%d].Type must be set to 'consul'", i)
|
return fmt.Errorf("Sources[%d].Type must be set to 'consul'", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for j, perm := range src.Permissions {
|
||||||
|
switch perm.Action {
|
||||||
|
case IntentionActionAllow, IntentionActionDeny:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Sources[%d].Permissions[%d].Action must be set to 'allow' or 'deny'", i, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
errorPrefix := "Sources[%d].Permissions[%d].HTTP"
|
||||||
|
if perm.HTTP == nil {
|
||||||
|
return fmt.Errorf(errorPrefix+" is required", i, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathParts := 0
|
||||||
|
if perm.HTTP.PathExact != "" {
|
||||||
|
pathParts++
|
||||||
|
if !strings.HasPrefix(perm.HTTP.PathExact, "/") {
|
||||||
|
return fmt.Errorf(
|
||||||
|
errorPrefix+".PathExact doesn't start with '/': %q",
|
||||||
|
i, j, perm.HTTP.PathExact,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if perm.HTTP.PathPrefix != "" {
|
||||||
|
pathParts++
|
||||||
|
if !strings.HasPrefix(perm.HTTP.PathPrefix, "/") {
|
||||||
|
return fmt.Errorf(
|
||||||
|
errorPrefix+".PathPrefix doesn't start with '/': %q",
|
||||||
|
i, j, perm.HTTP.PathPrefix,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if perm.HTTP.PathRegex != "" {
|
||||||
|
pathParts++
|
||||||
|
}
|
||||||
|
if pathParts > 1 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
errorPrefix+" should only contain at most one of PathExact, PathPrefix, or PathRegex",
|
||||||
|
i, j,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
permParts := pathParts
|
||||||
|
|
||||||
|
for k, hdr := range perm.HTTP.Header {
|
||||||
|
if hdr.Name == "" {
|
||||||
|
return fmt.Errorf(errorPrefix+".Header[%d] missing required Name field", i, j, k)
|
||||||
|
}
|
||||||
|
hdrParts := 0
|
||||||
|
if hdr.Present {
|
||||||
|
hdrParts++
|
||||||
|
}
|
||||||
|
if hdr.Exact != "" {
|
||||||
|
hdrParts++
|
||||||
|
}
|
||||||
|
if hdr.Regex != "" {
|
||||||
|
hdrParts++
|
||||||
|
}
|
||||||
|
if hdr.Prefix != "" {
|
||||||
|
hdrParts++
|
||||||
|
}
|
||||||
|
if hdr.Suffix != "" {
|
||||||
|
hdrParts++
|
||||||
|
}
|
||||||
|
if hdrParts != 1 {
|
||||||
|
return fmt.Errorf(errorPrefix+".Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex", i, j, k)
|
||||||
|
}
|
||||||
|
permParts++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(perm.HTTP.Methods) > 0 {
|
||||||
|
found := make(map[string]struct{})
|
||||||
|
for _, m := range perm.HTTP.Methods {
|
||||||
|
if !isValidHTTPMethod(m) {
|
||||||
|
return fmt.Errorf(errorPrefix+".Methods contains an invalid method %q", i, j, m)
|
||||||
|
}
|
||||||
|
if _, ok := found[m]; ok {
|
||||||
|
return fmt.Errorf(errorPrefix+".Methods contains %q more than once", i, j, m)
|
||||||
|
}
|
||||||
|
found[m] = struct{}{}
|
||||||
|
}
|
||||||
|
permParts++
|
||||||
|
}
|
||||||
|
|
||||||
|
if permParts == 0 {
|
||||||
|
return fmt.Errorf(errorPrefix+" should not be empty", i, j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
serviceName := src.SourceServiceName()
|
serviceName := src.SourceServiceName()
|
||||||
if _, exists := seenSources[serviceName]; exists {
|
if _, exists := seenSources[serviceName]; exists {
|
||||||
return fmt.Errorf("Sources[%d] defines %q more than once", i, serviceName.String())
|
return fmt.Errorf("Sources[%d] defines %q more than once", i, serviceName.String())
|
||||||
|
@ -521,7 +732,7 @@ func MigrateIntentions(ixns Intentions) []*ServiceIntentionsConfigEntry {
|
||||||
}
|
}
|
||||||
collated := make(map[ServiceName]*ServiceIntentionsConfigEntry)
|
collated := make(map[ServiceName]*ServiceIntentionsConfigEntry)
|
||||||
for _, ixn := range ixns {
|
for _, ixn := range ixns {
|
||||||
thisEntry := ixn.ToConfigEntry()
|
thisEntry := ixn.ToConfigEntry(true)
|
||||||
sn := thisEntry.DestinationServiceName()
|
sn := thisEntry.DestinationServiceName()
|
||||||
|
|
||||||
if entry, ok := collated[sn]; ok {
|
if entry, ok := collated[sn]; ok {
|
||||||
|
|
|
@ -315,6 +315,688 @@ func TestServiceIntentionsConfigEntry(t *testing.T) {
|
||||||
},
|
},
|
||||||
validateErr: `Sources[0].Action must be set to 'allow' or 'deny'`,
|
validateErr: `Sources[0].Action must be set to 'allow' or 'deny'`,
|
||||||
},
|
},
|
||||||
|
"action must not be set for L7": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{PathExact: "/"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Action must be omitted if Permissions are specified`,
|
||||||
|
},
|
||||||
|
"permission action must be set": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
HTTP: &IntentionHTTPPermission{PathExact: "/"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].Action must be set to 'allow' or 'deny'`,
|
||||||
|
},
|
||||||
|
"permission action must allow or deny": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: "blah",
|
||||||
|
HTTP: &IntentionHTTPPermission{PathExact: "/"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].Action must be set to 'allow' or 'deny'`,
|
||||||
|
},
|
||||||
|
"permission missing http": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP is required`,
|
||||||
|
},
|
||||||
|
"permission has too many path components (1)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathExact: "/",
|
||||||
|
PathPrefix: "/a",
|
||||||
|
// PathRegex: "/b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`,
|
||||||
|
},
|
||||||
|
"permission has too many path components (2)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathExact: "/",
|
||||||
|
// PathPrefix: "/a",
|
||||||
|
PathRegex: "/b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`,
|
||||||
|
},
|
||||||
|
"permission has too many path components (3)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
// PathExact: "/",
|
||||||
|
PathPrefix: "/a",
|
||||||
|
PathRegex: "/b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`,
|
||||||
|
},
|
||||||
|
"permission has too many path components (4)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathExact: "/",
|
||||||
|
PathPrefix: "/a",
|
||||||
|
PathRegex: "/b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`,
|
||||||
|
},
|
||||||
|
"permission has invalid path exact": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathExact: "x",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.PathExact doesn't start with '/': "x"`,
|
||||||
|
},
|
||||||
|
"permission has invalid path prefix": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathPrefix: "x",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.PathPrefix doesn't start with '/': "x"`,
|
||||||
|
},
|
||||||
|
"permission header missing name": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Description: strings.Repeat("x", 512),
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{Exact: "foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] missing required Name field`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (1)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
Present: true,
|
||||||
|
Exact: "foo",
|
||||||
|
// Regex: "foo",
|
||||||
|
// Prefix: "foo",
|
||||||
|
// Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (2)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
Present: true,
|
||||||
|
// Exact: "foo",
|
||||||
|
Regex: "foo",
|
||||||
|
// Prefix: "foo",
|
||||||
|
// Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (3)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
Present: true,
|
||||||
|
// Exact: "foo",
|
||||||
|
// Regex: "foo",
|
||||||
|
Prefix: "foo",
|
||||||
|
// Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (4)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
Present: true,
|
||||||
|
// Exact: "foo",
|
||||||
|
// Regex: "foo",
|
||||||
|
// Prefix: "foo",
|
||||||
|
Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (5)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
// Present: true,
|
||||||
|
Exact: "foo",
|
||||||
|
Regex: "foo",
|
||||||
|
// Prefix: "foo",
|
||||||
|
// Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (6)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
// Present: true,
|
||||||
|
Exact: "foo",
|
||||||
|
// Regex: "foo",
|
||||||
|
Prefix: "foo",
|
||||||
|
// Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (7)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
// Present: true,
|
||||||
|
Exact: "foo",
|
||||||
|
// Regex: "foo",
|
||||||
|
// Prefix: "foo",
|
||||||
|
Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (8)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
// Present: true,
|
||||||
|
// Exact: "foo",
|
||||||
|
Regex: "foo",
|
||||||
|
Prefix: "foo",
|
||||||
|
// Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (9)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
// Present: true,
|
||||||
|
// Exact: "foo",
|
||||||
|
Regex: "foo",
|
||||||
|
// Prefix: "foo",
|
||||||
|
Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (10)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
// Present: true,
|
||||||
|
// Exact: "foo",
|
||||||
|
// Regex: "foo",
|
||||||
|
Prefix: "foo",
|
||||||
|
Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission header has too many parts (11)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
Present: true,
|
||||||
|
Exact: "foo",
|
||||||
|
Regex: "foo",
|
||||||
|
Prefix: "foo",
|
||||||
|
Suffix: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
|
||||||
|
},
|
||||||
|
"permission invalid method": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Methods: []string{"YOINK"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Methods contains an invalid method "YOINK"`,
|
||||||
|
},
|
||||||
|
"permission repeated method": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Methods: []string{"POST", "PUT", "POST"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP.Methods contains "POST" more than once`,
|
||||||
|
},
|
||||||
|
"permission should not be empty (1)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Header: []IntentionHTTPHeaderPermission{},
|
||||||
|
Methods: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP should not be empty`,
|
||||||
|
},
|
||||||
|
"permission should not be empty (2)": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions[0].HTTP should not be empty`,
|
||||||
|
},
|
||||||
|
"permission kitchen sink": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/foo",
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "x-abc",
|
||||||
|
Exact: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "x-xyz",
|
||||||
|
Present: true,
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Methods: []string{"POST", "PUT", "GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"permissions not allowed on wildcarded destinations": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
// TODO: ent
|
||||||
|
Name: WildcardSpecifier,
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErr: `Sources[0].Permissions cannot be specified on intentions with wildcarded destinations`,
|
||||||
|
},
|
||||||
"L4 normalize": {
|
"L4 normalize": {
|
||||||
entry: &ServiceIntentionsConfigEntry{
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
Kind: ServiceIntentions,
|
Kind: ServiceIntentions,
|
||||||
|
@ -472,6 +1154,47 @@ func TestServiceIntentionsConfigEntry(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"L7 normalize": {
|
||||||
|
entry: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: ServiceIntentions,
|
||||||
|
Name: "test",
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "bar",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionDeny,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Methods: []string{
|
||||||
|
"get", "post",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, entry *ServiceIntentionsConfigEntry) {
|
||||||
|
assert.Equal(t, []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "bar",
|
||||||
|
EnterpriseMeta: *defaultMeta,
|
||||||
|
Precedence: 9,
|
||||||
|
Type: IntentionSourceConsul,
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionDeny,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
Methods: []string{
|
||||||
|
"GET", "POST",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, entry.Sources)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, tc := range cases {
|
for name, tc := range cases {
|
||||||
tc := tc
|
tc := tc
|
||||||
|
|
|
@ -973,6 +973,57 @@ func TestDecodeConfigEntry(t *testing.T) {
|
||||||
name = "bar"
|
name = "bar"
|
||||||
action = "allow"
|
action = "allow"
|
||||||
description = "bar desc"
|
description = "bar desc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "l7"
|
||||||
|
permissions = [
|
||||||
|
{
|
||||||
|
action = "deny"
|
||||||
|
http {
|
||||||
|
path_exact = "/admin"
|
||||||
|
header = [
|
||||||
|
{
|
||||||
|
name = "hdr-present"
|
||||||
|
present = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-exact"
|
||||||
|
exact = "exact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-prefix"
|
||||||
|
prefix = "prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-suffix"
|
||||||
|
suffix = "suffix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-regex"
|
||||||
|
regex = "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-absent"
|
||||||
|
present = true
|
||||||
|
invert = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_prefix = "/v3/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_regex = "/v[12]/.*"
|
||||||
|
methods = ["GET", "POST"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
sources {
|
sources {
|
||||||
|
@ -999,6 +1050,57 @@ func TestDecodeConfigEntry(t *testing.T) {
|
||||||
Name = "bar"
|
Name = "bar"
|
||||||
Action = "allow"
|
Action = "allow"
|
||||||
Description = "bar desc"
|
Description = "bar desc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "l7"
|
||||||
|
Permissions = [
|
||||||
|
{
|
||||||
|
Action = "deny"
|
||||||
|
HTTP {
|
||||||
|
PathExact = "/admin"
|
||||||
|
Header = [
|
||||||
|
{
|
||||||
|
Name = "hdr-present"
|
||||||
|
Present = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-exact"
|
||||||
|
Exact = "exact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-prefix"
|
||||||
|
Prefix = "prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-suffix"
|
||||||
|
Suffix = "suffix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-regex"
|
||||||
|
Regex = "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-absent"
|
||||||
|
Present = true
|
||||||
|
Invert = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action = "allow"
|
||||||
|
HTTP {
|
||||||
|
PathPrefix = "/v3/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action = "allow"
|
||||||
|
HTTP {
|
||||||
|
PathRegex = "/v[12]/.*"
|
||||||
|
Methods = ["GET", "POST"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Sources {
|
Sources {
|
||||||
|
@ -1026,6 +1128,57 @@ func TestDecodeConfigEntry(t *testing.T) {
|
||||||
Action: "allow",
|
Action: "allow",
|
||||||
Description: "bar desc",
|
Description: "bar desc",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "l7",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: "deny",
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathExact: "/admin",
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "hdr-present",
|
||||||
|
Present: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-exact",
|
||||||
|
Exact: "exact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-prefix",
|
||||||
|
Prefix: "prefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-suffix",
|
||||||
|
Suffix: "suffix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-regex",
|
||||||
|
Regex: "regex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-absent",
|
||||||
|
Present: true,
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "allow",
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/v3/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "allow",
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathRegex: "/v[12]/.*",
|
||||||
|
Methods: []string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "*",
|
Name: "*",
|
||||||
Action: "deny",
|
Action: "deny",
|
||||||
|
|
|
@ -55,7 +55,14 @@ type Intention struct {
|
||||||
SourceType IntentionSourceType
|
SourceType IntentionSourceType
|
||||||
|
|
||||||
// Action is whether this is an allowlist or denylist intention.
|
// Action is whether this is an allowlist or denylist intention.
|
||||||
Action IntentionAction
|
Action IntentionAction `json:",omitempty"`
|
||||||
|
|
||||||
|
// Permissions is the list of additional L7 attributes that extend the
|
||||||
|
// intention definition.
|
||||||
|
//
|
||||||
|
// NOTE: This field is not editable unless editing the underlying
|
||||||
|
// service-intentions config entry directly.
|
||||||
|
Permissions []*IntentionPermission `bexpr:"-" json:",omitempty"`
|
||||||
|
|
||||||
// DefaultAddr is not used.
|
// DefaultAddr is not used.
|
||||||
// Deprecated: DefaultAddr is not used and may be removed in a future version.
|
// Deprecated: DefaultAddr is not used and may be removed in a future version.
|
||||||
|
@ -77,10 +84,11 @@ type Intention struct {
|
||||||
// or modified.
|
// or modified.
|
||||||
CreatedAt, UpdatedAt time.Time `mapstructure:"-" bexpr:"-"`
|
CreatedAt, UpdatedAt time.Time `mapstructure:"-" bexpr:"-"`
|
||||||
|
|
||||||
// Hash of the contents of the intention
|
// Hash of the contents of the intention. This is only necessary for legacy
|
||||||
|
// intention replication purposes.
|
||||||
//
|
//
|
||||||
// This is needed mainly for replication purposes. When replicating from
|
// This is needed mainly for legacy replication purposes. When replicating
|
||||||
// one DC to another keeping the content Hash will allow us to detect
|
// from one DC to another keeping the content Hash will allow us to detect
|
||||||
// content changes more efficiently than checking every single field
|
// content changes more efficiently than checking every single field
|
||||||
Hash []byte `bexpr:"-" json:",omitempty"`
|
Hash []byte `bexpr:"-" json:",omitempty"`
|
||||||
|
|
||||||
|
@ -89,6 +97,12 @@ type Intention struct {
|
||||||
|
|
||||||
func (t *Intention) Clone() *Intention {
|
func (t *Intention) Clone() *Intention {
|
||||||
t2 := *t
|
t2 := *t
|
||||||
|
if len(t.Permissions) > 0 {
|
||||||
|
t2.Permissions = make([]*IntentionPermission, 0, len(t.Permissions))
|
||||||
|
for _, perm := range t.Permissions {
|
||||||
|
t2.Permissions = append(t2.Permissions, perm.Clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
t2.Meta = cloneStringStringMap(t.Meta)
|
t2.Meta = cloneStringStringMap(t.Meta)
|
||||||
t2.Hash = nil
|
t2.Hash = nil
|
||||||
return &t2
|
return &t2
|
||||||
|
@ -267,6 +281,11 @@ func (x *Intention) Validate() error {
|
||||||
"Action must be set to 'allow' or 'deny'"))
|
"Action must be set to 'allow' or 'deny'"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(x.Permissions) > 0 {
|
||||||
|
result = multierror.Append(result, fmt.Errorf(
|
||||||
|
"Permissions must not be set when using the legacy APIs"))
|
||||||
|
}
|
||||||
|
|
||||||
switch x.SourceType {
|
switch x.SourceType {
|
||||||
case IntentionSourceConsul:
|
case IntentionSourceConsul:
|
||||||
default:
|
default:
|
||||||
|
@ -365,11 +384,25 @@ func (x *Intention) countExact(ns, n string) int {
|
||||||
|
|
||||||
// String returns a human-friendly string for this intention.
|
// String returns a human-friendly string for this intention.
|
||||||
func (x *Intention) String() string {
|
func (x *Intention) String() string {
|
||||||
return fmt.Sprintf("%s %s/%s => %s/%s (ID: %s, Precedence: %d)",
|
var idPart string
|
||||||
strings.ToUpper(string(x.Action)),
|
if x.ID != "" {
|
||||||
|
idPart = "ID: " + x.ID + ", "
|
||||||
|
}
|
||||||
|
|
||||||
|
var detailPart string
|
||||||
|
if len(x.Permissions) > 0 {
|
||||||
|
detailPart = fmt.Sprintf("Permissions: %d", len(x.Permissions))
|
||||||
|
} else {
|
||||||
|
detailPart = "Action: " + strings.ToUpper(string(x.Action))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s => %s/%s (%sPrecedence: %d, %s)",
|
||||||
x.SourceNS, x.SourceName,
|
x.SourceNS, x.SourceName,
|
||||||
x.DestinationNS, x.DestinationName,
|
x.DestinationNS, x.DestinationName,
|
||||||
x.ID, x.Precedence)
|
idPart,
|
||||||
|
x.Precedence,
|
||||||
|
detailPart,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LegacyEstimateSize returns an estimate (in bytes) of the size of this structure when encoded.
|
// LegacyEstimateSize returns an estimate (in bytes) of the size of this structure when encoded.
|
||||||
|
@ -397,20 +430,21 @@ func (x *Intention) DestinationServiceName() ServiceName {
|
||||||
|
|
||||||
// NOTE this is just used to manipulate user-provided data before an insert
|
// NOTE this is just used to manipulate user-provided data before an insert
|
||||||
// The RPC execution will do Normalize + Validate for us.
|
// The RPC execution will do Normalize + Validate for us.
|
||||||
func (x *Intention) ToConfigEntry() *ServiceIntentionsConfigEntry {
|
func (x *Intention) ToConfigEntry(legacy bool) *ServiceIntentionsConfigEntry {
|
||||||
return &ServiceIntentionsConfigEntry{
|
return &ServiceIntentionsConfigEntry{
|
||||||
Kind: ServiceIntentions,
|
Kind: ServiceIntentions,
|
||||||
Name: x.DestinationName,
|
Name: x.DestinationName,
|
||||||
EnterpriseMeta: *x.DestinationEnterpriseMeta(),
|
EnterpriseMeta: *x.DestinationEnterpriseMeta(),
|
||||||
Sources: []*SourceIntention{x.ToSourceIntention()},
|
Sources: []*SourceIntention{x.ToSourceIntention(legacy)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Intention) ToSourceIntention() *SourceIntention {
|
func (x *Intention) ToSourceIntention(legacy bool) *SourceIntention {
|
||||||
return &SourceIntention{
|
src := &SourceIntention{
|
||||||
Name: x.SourceName,
|
Name: x.SourceName,
|
||||||
EnterpriseMeta: *x.SourceEnterpriseMeta(),
|
EnterpriseMeta: *x.SourceEnterpriseMeta(),
|
||||||
Action: x.Action,
|
Action: x.Action,
|
||||||
|
Permissions: nil, // explicitly not symmetric with the old APIs
|
||||||
Precedence: 0, // Ignore, let it be computed.
|
Precedence: 0, // Ignore, let it be computed.
|
||||||
LegacyID: x.ID,
|
LegacyID: x.ID,
|
||||||
Type: x.SourceType,
|
Type: x.SourceType,
|
||||||
|
@ -419,6 +453,10 @@ func (x *Intention) ToSourceIntention() *SourceIntention {
|
||||||
LegacyCreateTime: nil, // Ignore
|
LegacyCreateTime: nil, // Ignore
|
||||||
LegacyUpdateTime: nil, // Ignore
|
LegacyUpdateTime: nil, // Ignore
|
||||||
}
|
}
|
||||||
|
if !legacy {
|
||||||
|
src.Permissions = x.Permissions
|
||||||
|
}
|
||||||
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntentionAction is the action that the intention represents. This
|
// IntentionAction is the action that the intention represents. This
|
||||||
|
|
|
@ -359,3 +359,97 @@ func TestIntention_SetHash(t *testing.T) {
|
||||||
}
|
}
|
||||||
require.Equal(t, expected, i.Hash)
|
require.Equal(t, expected, i.Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntention_String(t *testing.T) {
|
||||||
|
type testcase struct {
|
||||||
|
ixn *Intention
|
||||||
|
expect string
|
||||||
|
}
|
||||||
|
|
||||||
|
testID := generateUUID()
|
||||||
|
|
||||||
|
cases := map[string]testcase{
|
||||||
|
"legacy allow": {
|
||||||
|
&Intention{
|
||||||
|
ID: testID,
|
||||||
|
SourceName: "foo",
|
||||||
|
DestinationName: "bar",
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
},
|
||||||
|
`default/foo => default/bar (ID: ` + testID + `, Precedence: 9, Action: ALLOW)`,
|
||||||
|
},
|
||||||
|
"legacy deny": {
|
||||||
|
&Intention{
|
||||||
|
ID: testID,
|
||||||
|
SourceName: "foo",
|
||||||
|
DestinationName: "bar",
|
||||||
|
Action: IntentionActionDeny,
|
||||||
|
},
|
||||||
|
`default/foo => default/bar (ID: ` + testID + `, Precedence: 9, Action: DENY)`,
|
||||||
|
},
|
||||||
|
"L4 allow": {
|
||||||
|
&Intention{
|
||||||
|
SourceName: "foo",
|
||||||
|
DestinationName: "bar",
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
},
|
||||||
|
`default/foo => default/bar (Precedence: 9, Action: ALLOW)`,
|
||||||
|
},
|
||||||
|
"L4 deny": {
|
||||||
|
&Intention{
|
||||||
|
SourceName: "foo",
|
||||||
|
DestinationName: "bar",
|
||||||
|
Action: IntentionActionDeny,
|
||||||
|
},
|
||||||
|
`default/foo => default/bar (Precedence: 9, Action: DENY)`,
|
||||||
|
},
|
||||||
|
"L7 one perm": {
|
||||||
|
&Intention{
|
||||||
|
SourceName: "foo",
|
||||||
|
DestinationName: "bar",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`default/foo => default/bar (Precedence: 9, Permissions: 1)`,
|
||||||
|
},
|
||||||
|
"L7 two perms": {
|
||||||
|
&Intention{
|
||||||
|
SourceName: "foo",
|
||||||
|
DestinationName: "bar",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: IntentionActionDeny,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathExact: "/foo/admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: IntentionActionAllow,
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`default/foo => default/bar (Precedence: 9, Permissions: 2)`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
// Add a bunch of required fields.
|
||||||
|
tc.ixn.DefaultNamespaces(DefaultEnterpriseMeta())
|
||||||
|
tc.ixn.UpdatePrecedence()
|
||||||
|
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := tc.ixn.String()
|
||||||
|
require.Equal(t, tc.expect, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,8 +12,37 @@ var (
|
||||||
// minSupportedVersion is the oldest mainline version we support. This should always be
|
// minSupportedVersion is the oldest mainline version we support. This should always be
|
||||||
// the zero'th point release of the last element of proxysupport.EnvoyVersions.
|
// the zero'th point release of the last element of proxysupport.EnvoyVersions.
|
||||||
minSupportedVersion = version.Must(version.NewVersion("1.12.0"))
|
minSupportedVersion = version.Must(version.NewVersion("1.12.0"))
|
||||||
|
|
||||||
|
specificUnsupportedVersions = []unsupportedVersion{
|
||||||
|
{
|
||||||
|
Version: version.Must(version.NewVersion("1.12.0")),
|
||||||
|
UpgradeTo: "1.12.3+",
|
||||||
|
Why: "does not support RBAC rules using url_path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: version.Must(version.NewVersion("1.12.1")),
|
||||||
|
UpgradeTo: "1.12.3+",
|
||||||
|
Why: "does not support RBAC rules using url_path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: version.Must(version.NewVersion("1.12.2")),
|
||||||
|
UpgradeTo: "1.12.3+",
|
||||||
|
Why: "does not support RBAC rules using url_path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: version.Must(version.NewVersion("1.13.0")),
|
||||||
|
UpgradeTo: "1.13.1+",
|
||||||
|
Why: "does not support RBAC rules using url_path",
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type unsupportedVersion struct {
|
||||||
|
Version *version.Version
|
||||||
|
UpgradeTo string
|
||||||
|
Why string
|
||||||
|
}
|
||||||
|
|
||||||
type supportedProxyFeatures struct {
|
type supportedProxyFeatures struct {
|
||||||
// add version dependent feature flags here
|
// add version dependent feature flags here
|
||||||
}
|
}
|
||||||
|
@ -39,6 +68,18 @@ func determineSupportedProxyFeaturesFromVersion(version *version.Version) (suppo
|
||||||
return supportedProxyFeatures{}, fmt.Errorf("Envoy %s is too old and is not supported by Consul", version)
|
return supportedProxyFeatures{}, fmt.Errorf("Envoy %s is too old and is not supported by Consul", version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, uv := range specificUnsupportedVersions {
|
||||||
|
if version.Equal(uv.Version) {
|
||||||
|
return supportedProxyFeatures{}, fmt.Errorf(
|
||||||
|
"Envoy %s is too old of a point release and is not supported by Consul because it %s. "+
|
||||||
|
"Please upgrade to version %s.",
|
||||||
|
version,
|
||||||
|
uv.Why,
|
||||||
|
uv.UpgradeTo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return supportedProxyFeatures{}, nil
|
return supportedProxyFeatures{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
envoycore "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
envoycore "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||||
envoytype "github.com/envoyproxy/go-control-plane/envoy/type"
|
envoytype "github.com/envoyproxy/go-control-plane/envoy/type"
|
||||||
|
"github.com/hashicorp/consul/sdk/testutil"
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -68,3 +69,49 @@ func TestDetermineEnvoyVersionFromNode(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDetermineSupportedProxyFeaturesFromString(t *testing.T) {
|
||||||
|
const (
|
||||||
|
err1_12 = "is too old of a point release and is not supported by Consul because it does not support RBAC rules using url_path. Please upgrade to version 1.12.3+."
|
||||||
|
err1_13 = "is too old of a point release and is not supported by Consul because it does not support RBAC rules using url_path. Please upgrade to version 1.13.1+."
|
||||||
|
errTooOld = "is too old and is not supported by Consul"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testcase struct {
|
||||||
|
expect supportedProxyFeatures
|
||||||
|
expectErr string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just the bad versions
|
||||||
|
cases := map[string]testcase{
|
||||||
|
"1.9.0": {expectErr: "Envoy 1.9.0 " + errTooOld},
|
||||||
|
"1.10.0": {expectErr: "Envoy 1.10.0 " + errTooOld},
|
||||||
|
"1.11.0": {expectErr: "Envoy 1.11.0 " + errTooOld},
|
||||||
|
"1.12.0": {expectErr: "Envoy 1.12.0 " + err1_12},
|
||||||
|
"1.12.1": {expectErr: "Envoy 1.12.1 " + err1_12},
|
||||||
|
"1.12.2": {expectErr: "Envoy 1.12.2 " + err1_12},
|
||||||
|
"1.13.0": {expectErr: "Envoy 1.13.0 " + err1_13},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a bunch of valid versions.
|
||||||
|
for _, v := range []string{
|
||||||
|
"1.12.3", "1.12.4", "1.12.5", "1.12.6", "1.12.7",
|
||||||
|
"1.13.1", "1.13.2", "1.13.3", "1.13.4", "1.13.6", "1.14.1",
|
||||||
|
"1.14.2", "1.14.3", "1.14.4", "1.14.5",
|
||||||
|
"1.15.0", "1.15.1", "1.15.2",
|
||||||
|
} {
|
||||||
|
cases[v] = testcase{expect: supportedProxyFeatures{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
sf, err := determineSupportedProxyFeaturesFromString(name)
|
||||||
|
if tc.expectErr == "" {
|
||||||
|
require.Equal(t, tc.expect, sf)
|
||||||
|
} else {
|
||||||
|
testutil.RequireErrorContains(t, err, tc.expectErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,10 @@ package xds
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
envoylistener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
|
envoylistener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
|
||||||
|
envoyroute "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
|
||||||
envoyhttprbac "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/rbac/v2"
|
envoyhttprbac "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/rbac/v2"
|
||||||
envoyhttp "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
|
envoyhttp "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
|
||||||
envoynetrbac "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/rbac/v2"
|
envoynetrbac "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/rbac/v2"
|
||||||
|
@ -14,7 +16,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeRBACNetworkFilter(intentions structs.Intentions, intentionDefaultAllow bool) (*envoylistener.Filter, error) {
|
func makeRBACNetworkFilter(intentions structs.Intentions, intentionDefaultAllow bool) (*envoylistener.Filter, error) {
|
||||||
rules, err := makeRBACRules(intentions, intentionDefaultAllow)
|
rules, err := makeRBACRules(intentions, intentionDefaultAllow, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -27,7 +29,7 @@ func makeRBACNetworkFilter(intentions structs.Intentions, intentionDefaultAllow
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeRBACHTTPFilter(intentions structs.Intentions, intentionDefaultAllow bool) (*envoyhttp.HttpFilter, error) {
|
func makeRBACHTTPFilter(intentions structs.Intentions, intentionDefaultAllow bool) (*envoyhttp.HttpFilter, error) {
|
||||||
rules, err := makeRBACRules(intentions, intentionDefaultAllow)
|
rules, err := makeRBACRules(intentions, intentionDefaultAllow, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -38,16 +40,238 @@ func makeRBACHTTPFilter(intentions structs.Intentions, intentionDefaultAllow boo
|
||||||
return makeEnvoyHTTPFilter("envoy.filters.http.rbac", cfg)
|
return makeEnvoyHTTPFilter("envoy.filters.http.rbac", cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func intentionListToIntermediateRBACForm(intentions structs.Intentions, isHTTP bool) []*rbacIntention {
|
||||||
|
sort.Sort(structs.IntentionPrecedenceSorter(intentions))
|
||||||
|
|
||||||
|
// Omit any lower-precedence intentions that share the same source.
|
||||||
|
intentions = removeSameSourceIntentions(intentions)
|
||||||
|
|
||||||
|
rbacIxns := make([]*rbacIntention, 0, len(intentions))
|
||||||
|
for _, ixn := range intentions {
|
||||||
|
rixn := intentionToIntermediateRBACForm(ixn, isHTTP)
|
||||||
|
rbacIxns = append(rbacIxns, rixn)
|
||||||
|
}
|
||||||
|
return rbacIxns
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSourcePrecedence(rbacIxns []*rbacIntention, intentionDefaultAction intentionAction) []*rbacIntention {
|
||||||
|
if len(rbacIxns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove source precedence:
|
||||||
|
//
|
||||||
|
// First walk backwards and add each intention to all subsequent statements
|
||||||
|
// (via AND NOT $x).
|
||||||
|
//
|
||||||
|
// If it is L4 and has the same action as the default intention action then
|
||||||
|
// mark the rule itself for erasure.
|
||||||
|
numRetained := 0
|
||||||
|
for i := len(rbacIxns) - 1; i >= 0; i-- {
|
||||||
|
for j := i + 1; j < len(rbacIxns); j++ {
|
||||||
|
if rbacIxns[j].Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// [i] is the intention candidate that we are distributing
|
||||||
|
// [j] is the thing to maybe NOT [i] from
|
||||||
|
if ixnSourceMatches(rbacIxns[i].Source, rbacIxns[j].Source) {
|
||||||
|
rbacIxns[j].NotSources = append(rbacIxns[j].NotSources, rbacIxns[i].Source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rbacIxns[i].Action == intentionDefaultAction {
|
||||||
|
// Lower precedence intentions that match the default intention
|
||||||
|
// action are skipped, since they're handled by the default
|
||||||
|
// catch-all.
|
||||||
|
rbacIxns[i].Skip = true // mark for deletion
|
||||||
|
} else {
|
||||||
|
numRetained++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At this point precedence doesn't matter for the source element.
|
||||||
|
|
||||||
|
// Remove skipped intentions and also compute the final Principals for each
|
||||||
|
// intention.
|
||||||
|
out := make([]*rbacIntention, 0, numRetained)
|
||||||
|
for _, rixn := range rbacIxns {
|
||||||
|
if rixn.Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rixn.ComputedPrincipal = rixn.FlattenPrincipal()
|
||||||
|
out = append(out, rixn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeIntentionPrecedence(rbacIxns []*rbacIntention, intentionDefaultAction intentionAction) []*rbacIntention {
|
||||||
|
// Remove source precedence. After this completes precedence doesn't matter
|
||||||
|
// between any two intentions.
|
||||||
|
rbacIxns = removeSourcePrecedence(rbacIxns, intentionDefaultAction)
|
||||||
|
|
||||||
|
for _, rbacIxn := range rbacIxns {
|
||||||
|
// Remove permission precedence. After this completes precedence
|
||||||
|
// doesn't matter between any two permissions on this intention.
|
||||||
|
rbacIxn.Permissions = removePermissionPrecedence(rbacIxn.Permissions, intentionDefaultAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rbacIxns
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePermissionPrecedence(perms []*rbacPermission, intentionDefaultAction intentionAction) []*rbacPermission {
|
||||||
|
if len(perms) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// First walk backwards and add each permission to all subsequent
|
||||||
|
// statements (via AND NOT $x).
|
||||||
|
//
|
||||||
|
// If it has the same action as the default intention action then mark the
|
||||||
|
// permission itself for erasure.
|
||||||
|
numRetained := 0
|
||||||
|
for i := len(perms) - 1; i >= 0; i-- {
|
||||||
|
for j := i + 1; j < len(perms); j++ {
|
||||||
|
if perms[j].Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// [i] is the permission candidate that we are distributing
|
||||||
|
// [j] is the thing to maybe NOT [i] from
|
||||||
|
perms[j].NotPerms = append(
|
||||||
|
perms[j].NotPerms,
|
||||||
|
perms[i].Perm,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if perms[i].Action == intentionDefaultAction {
|
||||||
|
// Lower precedence permissions that match the default intention
|
||||||
|
// action are skipped, since they're handled by the default
|
||||||
|
// catch-all.
|
||||||
|
perms[i].Skip = true // mark for deletion
|
||||||
|
} else {
|
||||||
|
numRetained++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove skipped permissions and also compute the final Permissions for each item.
|
||||||
|
out := make([]*rbacPermission, 0, numRetained)
|
||||||
|
for _, perm := range perms {
|
||||||
|
if perm.Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
perm.ComputedPermission = perm.Flatten()
|
||||||
|
out = append(out, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func intentionToIntermediateRBACForm(ixn *structs.Intention, isHTTP bool) *rbacIntention {
|
||||||
|
rixn := &rbacIntention{
|
||||||
|
Source: ixn.SourceServiceName(),
|
||||||
|
Precedence: ixn.Precedence,
|
||||||
|
}
|
||||||
|
if len(ixn.Permissions) > 0 {
|
||||||
|
if isHTTP {
|
||||||
|
rixn.Action = intentionActionLayer7
|
||||||
|
rixn.Permissions = make([]*rbacPermission, 0, len(ixn.Permissions))
|
||||||
|
for _, perm := range ixn.Permissions {
|
||||||
|
rixn.Permissions = append(rixn.Permissions, &rbacPermission{
|
||||||
|
Definition: perm,
|
||||||
|
Action: intentionActionFromString(perm.Action),
|
||||||
|
Perm: convertPermission(perm),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In case L7 intentions slip through to here, treat them as deny intentions.
|
||||||
|
rixn.Action = intentionActionDeny
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rixn.Action = intentionActionFromString(ixn.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rixn
|
||||||
|
}
|
||||||
|
|
||||||
|
type intentionAction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
intentionActionDeny intentionAction = iota
|
||||||
|
intentionActionAllow
|
||||||
|
intentionActionLayer7
|
||||||
|
)
|
||||||
|
|
||||||
|
func intentionActionFromBool(v bool) intentionAction {
|
||||||
|
if v {
|
||||||
|
return intentionActionAllow
|
||||||
|
} else {
|
||||||
|
return intentionActionDeny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func intentionActionFromString(s structs.IntentionAction) intentionAction {
|
||||||
|
if s == structs.IntentionActionAllow {
|
||||||
|
return intentionActionAllow
|
||||||
|
}
|
||||||
|
return intentionActionDeny
|
||||||
|
}
|
||||||
|
|
||||||
type rbacIntention struct {
|
type rbacIntention struct {
|
||||||
Source structs.ServiceName
|
Source structs.ServiceName
|
||||||
NotSources []structs.ServiceName
|
NotSources []structs.ServiceName
|
||||||
Allow bool
|
Action intentionAction
|
||||||
|
Permissions []*rbacPermission
|
||||||
Precedence int
|
Precedence int
|
||||||
|
|
||||||
|
// Skip is field used to indicate that this intention can be deleted in the
|
||||||
|
// final pass. Items marked as true should generally not escape the method
|
||||||
|
// that marked them.
|
||||||
Skip bool
|
Skip bool
|
||||||
|
|
||||||
|
ComputedPrincipal *envoyrbac.Principal
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rbacIntention) Simplify() {
|
func (r *rbacIntention) FlattenPrincipal() *envoyrbac.Principal {
|
||||||
r.NotSources = simplifyNotSourceSlice(r.NotSources)
|
r.NotSources = simplifyNotSourceSlice(r.NotSources)
|
||||||
|
|
||||||
|
if len(r.NotSources) == 0 {
|
||||||
|
return idPrincipal(r.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
andIDs := make([]*envoyrbac.Principal, 0, len(r.NotSources)+1)
|
||||||
|
andIDs = append(andIDs, idPrincipal(r.Source))
|
||||||
|
for _, src := range r.NotSources {
|
||||||
|
andIDs = append(andIDs, notPrincipal(
|
||||||
|
idPrincipal(src),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return andPrincipals(andIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rbacPermission struct {
|
||||||
|
Definition *structs.IntentionPermission
|
||||||
|
|
||||||
|
Action intentionAction
|
||||||
|
Perm *envoyrbac.Permission
|
||||||
|
NotPerms []*envoyrbac.Permission
|
||||||
|
|
||||||
|
// Skip is field used to indicate that this permission can be deleted in
|
||||||
|
// the final pass. Items marked as true should generally not escape the
|
||||||
|
// method that marked them.
|
||||||
|
Skip bool
|
||||||
|
|
||||||
|
ComputedPermission *envoyrbac.Permission
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *rbacPermission) Flatten() *envoyrbac.Permission {
|
||||||
|
if len(p.NotPerms) == 0 {
|
||||||
|
return p.Perm
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]*envoyrbac.Permission, 0, len(p.NotPerms)+1)
|
||||||
|
parts = append(parts, p.Perm)
|
||||||
|
for _, notPerm := range p.NotPerms {
|
||||||
|
parts = append(parts, notPermission(notPerm))
|
||||||
|
}
|
||||||
|
return andPermissions(parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func simplifyNotSourceSlice(notSources []structs.ServiceName) []structs.ServiceName {
|
func simplifyNotSourceSlice(notSources []structs.ServiceName) []structs.ServiceName {
|
||||||
|
@ -132,7 +356,7 @@ func simplifyNotSourceSlice(notSources []structs.ServiceName) []structs.ServiceN
|
||||||
// <default> : DENY
|
// <default> : DENY
|
||||||
//
|
//
|
||||||
// Which really is just an allow-list of [A, C AND NOT(B)]
|
// Which really is just an allow-list of [A, C AND NOT(B)]
|
||||||
func makeRBACRules(intentions structs.Intentions, intentionDefaultAllow bool) (*envoyrbac.RBAC, error) {
|
func makeRBACRules(intentions structs.Intentions, intentionDefaultAllow bool, isHTTP bool) (*envoyrbac.RBAC, error) {
|
||||||
// Note that we DON'T explicitly validate the trust-domain matches ours.
|
// Note that we DON'T explicitly validate the trust-domain matches ours.
|
||||||
//
|
//
|
||||||
// For now we don't validate the trust domain of the _destination_ at all.
|
// For now we don't validate the trust domain of the _destination_ at all.
|
||||||
|
@ -147,20 +371,11 @@ func makeRBACRules(intentions structs.Intentions, intentionDefaultAllow bool) (*
|
||||||
|
|
||||||
// TODO(banks,rb): Implement revocation list checking?
|
// TODO(banks,rb): Implement revocation list checking?
|
||||||
|
|
||||||
// Omit any lower-precedence intentions that share the same source.
|
|
||||||
intentions = removeSameSourceIntentions(intentions)
|
|
||||||
|
|
||||||
// First build up just the basic principal matches.
|
// First build up just the basic principal matches.
|
||||||
rbacIxns := make([]*rbacIntention, 0, len(intentions))
|
rbacIxns := intentionListToIntermediateRBACForm(intentions, isHTTP)
|
||||||
for _, ixn := range intentions {
|
|
||||||
rbacIxns = append(rbacIxns, &rbacIntention{
|
|
||||||
Source: ixn.SourceServiceName(),
|
|
||||||
Allow: (ixn.Action == structs.IntentionActionAllow),
|
|
||||||
Precedence: ixn.Precedence,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize: if we are in default-deny then all intentions must be allows and vice versa
|
// Normalize: if we are in default-deny then all intentions must be allows and vice versa
|
||||||
|
intentionDefaultAction := intentionActionFromBool(intentionDefaultAllow)
|
||||||
|
|
||||||
var rbacAction envoyrbac.RBAC_Action
|
var rbacAction envoyrbac.RBAC_Action
|
||||||
if intentionDefaultAllow {
|
if intentionDefaultAllow {
|
||||||
|
@ -173,69 +388,46 @@ func makeRBACRules(intentions structs.Intentions, intentionDefaultAllow bool) (*
|
||||||
rbacAction = envoyrbac.RBAC_ALLOW
|
rbacAction = envoyrbac.RBAC_ALLOW
|
||||||
}
|
}
|
||||||
|
|
||||||
// First walk backwards and if we encounter an intention with an action
|
// Remove source and permissions precedence.
|
||||||
// that is the same as the default intention action, add it to all
|
rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction)
|
||||||
// subsequent statements (via AND NOT $x) and mark the rule itself for
|
|
||||||
// erasure.
|
|
||||||
//
|
|
||||||
// i.e. for a default-deny setup we look for denies.
|
|
||||||
if len(rbacIxns) > 0 {
|
|
||||||
for i := len(rbacIxns) - 1; i >= 0; i-- {
|
|
||||||
if rbacIxns[i].Allow == intentionDefaultAllow {
|
|
||||||
for j := i + 1; j < len(rbacIxns); j++ {
|
|
||||||
if rbacIxns[j].Skip {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// [i] is the intention candidate that we are distributing
|
|
||||||
// [j] is the thing to maybe NOT [i] from
|
|
||||||
if ixnSourceMatches(rbacIxns[i].Source, rbacIxns[j].Source) {
|
|
||||||
rbacIxns[j].NotSources = append(rbacIxns[j].NotSources, rbacIxns[i].Source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// since this is default-FOO, any trailing FOO intentions will just evaporate
|
|
||||||
rbacIxns[i].Skip = true // mark for deletion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// At this point precedence doesn't matter since all roads lead to the same action.
|
|
||||||
|
|
||||||
var principals []*envoyrbac.Principal
|
|
||||||
for _, rbacIxn := range rbacIxns {
|
|
||||||
if rbacIxn.Skip {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: at this point "rbacIxn.Allow != intentionDefaultAllow"
|
|
||||||
|
|
||||||
rbacIxn.Simplify()
|
|
||||||
|
|
||||||
if len(rbacIxn.NotSources) > 0 {
|
|
||||||
andIDs := make([]*envoyrbac.Principal, 0, len(rbacIxn.NotSources)+1)
|
|
||||||
andIDs = append(andIDs, idPrincipal(rbacIxn.Source))
|
|
||||||
for _, src := range rbacIxn.NotSources {
|
|
||||||
andIDs = append(andIDs, notPrincipal(
|
|
||||||
idPrincipal(src),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
principals = append(principals, andPrincipals(andIDs))
|
|
||||||
} else {
|
|
||||||
principals = append(principals, idPrincipal(rbacIxn.Source))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// For L4: we should generate one big Policy listing all Principals
|
||||||
|
// For L7: we should generate one Policy per Principal and list all of the Permissions
|
||||||
rbac := &envoyrbac.RBAC{
|
rbac := &envoyrbac.RBAC{
|
||||||
Action: rbacAction,
|
Action: rbacAction,
|
||||||
|
Policies: make(map[string]*envoyrbac.Policy),
|
||||||
}
|
}
|
||||||
if len(principals) > 0 {
|
|
||||||
|
var principalsL4 []*envoyrbac.Principal
|
||||||
|
for i, rbacIxn := range rbacIxns {
|
||||||
|
if len(rbacIxn.Permissions) > 0 {
|
||||||
|
if !isHTTP {
|
||||||
|
panic("invalid state: L7 permissions present for TCP service")
|
||||||
|
}
|
||||||
|
// For L7: we should generate one Policy per Principal and list all of the Permissions
|
||||||
policy := &envoyrbac.Policy{
|
policy := &envoyrbac.Policy{
|
||||||
Principals: principals,
|
Principals: []*envoyrbac.Principal{rbacIxn.ComputedPrincipal},
|
||||||
Permissions: []*envoyrbac.Permission{anyPermission()},
|
Permissions: make([]*envoyrbac.Permission, 0, len(rbacIxn.Permissions)),
|
||||||
}
|
}
|
||||||
rbac.Policies = map[string]*envoyrbac.Policy{
|
for _, perm := range rbacIxn.Permissions {
|
||||||
"consul-intentions": policy,
|
policy.Permissions = append(policy.Permissions, perm.ComputedPermission)
|
||||||
|
}
|
||||||
|
rbac.Policies[fmt.Sprintf("consul-intentions-layer7-%d", i)] = policy
|
||||||
|
} else {
|
||||||
|
// For L4: we should generate one big Policy listing all Principals
|
||||||
|
principalsL4 = append(principalsL4, rbacIxn.ComputedPrincipal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(principalsL4) > 0 {
|
||||||
|
rbac.Policies["consul-intentions-layer4"] = &envoyrbac.Policy{
|
||||||
|
Principals: principalsL4,
|
||||||
|
Permissions: []*envoyrbac.Permission{anyPermission()},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(rbac.Policies) == 0 {
|
||||||
|
rbac.Policies = nil
|
||||||
|
}
|
||||||
return rbac, nil
|
return rbac, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,14 +459,6 @@ func removeSameSourceIntentions(intentions structs.Intentions) structs.Intention
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
type sourceMatch int
|
|
||||||
|
|
||||||
const (
|
|
||||||
sourceMatchIgnore sourceMatch = 0
|
|
||||||
sourceMatchSuperset sourceMatch = 1
|
|
||||||
matchSameSubset sourceMatch = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// ixnSourceMatches deterines if the 'tester' service name is matched by the
|
// ixnSourceMatches deterines if the 'tester' service name is matched by the
|
||||||
// 'against' service name via wildcard rules.
|
// 'against' service name via wildcard rules.
|
||||||
//
|
//
|
||||||
|
@ -372,3 +556,142 @@ func anyPermission() *envoyrbac.Permission {
|
||||||
Rule: &envoyrbac.Permission_Any{Any: true},
|
Rule: &envoyrbac.Permission_Any{Any: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertPermission(perm *structs.IntentionPermission) *envoyrbac.Permission {
|
||||||
|
// NOTE: this does not do anything with perm.Action
|
||||||
|
if perm.HTTP == nil {
|
||||||
|
return anyPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []*envoyrbac.Permission
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case perm.HTTP.PathExact != "":
|
||||||
|
parts = append(parts, &envoyrbac.Permission{
|
||||||
|
Rule: &envoyrbac.Permission_UrlPath{
|
||||||
|
UrlPath: &envoymatcher.PathMatcher{
|
||||||
|
Rule: &envoymatcher.PathMatcher_Path{
|
||||||
|
Path: &envoymatcher.StringMatcher{
|
||||||
|
MatchPattern: &envoymatcher.StringMatcher_Exact{
|
||||||
|
Exact: perm.HTTP.PathExact,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
case perm.HTTP.PathPrefix != "":
|
||||||
|
parts = append(parts, &envoyrbac.Permission{
|
||||||
|
Rule: &envoyrbac.Permission_UrlPath{
|
||||||
|
UrlPath: &envoymatcher.PathMatcher{
|
||||||
|
Rule: &envoymatcher.PathMatcher_Path{
|
||||||
|
Path: &envoymatcher.StringMatcher{
|
||||||
|
MatchPattern: &envoymatcher.StringMatcher_Prefix{
|
||||||
|
Prefix: perm.HTTP.PathPrefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
case perm.HTTP.PathRegex != "":
|
||||||
|
parts = append(parts, &envoyrbac.Permission{
|
||||||
|
Rule: &envoyrbac.Permission_UrlPath{
|
||||||
|
UrlPath: &envoymatcher.PathMatcher{
|
||||||
|
Rule: &envoymatcher.PathMatcher_Path{
|
||||||
|
Path: &envoymatcher.StringMatcher{
|
||||||
|
MatchPattern: &envoymatcher.StringMatcher_SafeRegex{
|
||||||
|
SafeRegex: makeEnvoyRegexMatch(perm.HTTP.PathRegex),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hdr := range perm.HTTP.Header {
|
||||||
|
eh := &envoyroute.HeaderMatcher{
|
||||||
|
Name: hdr.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case hdr.Exact != "":
|
||||||
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_ExactMatch{
|
||||||
|
ExactMatch: hdr.Exact,
|
||||||
|
}
|
||||||
|
case hdr.Regex != "":
|
||||||
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_SafeRegexMatch{
|
||||||
|
SafeRegexMatch: makeEnvoyRegexMatch(hdr.Regex),
|
||||||
|
}
|
||||||
|
case hdr.Prefix != "":
|
||||||
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PrefixMatch{
|
||||||
|
PrefixMatch: hdr.Prefix,
|
||||||
|
}
|
||||||
|
case hdr.Suffix != "":
|
||||||
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_SuffixMatch{
|
||||||
|
SuffixMatch: hdr.Suffix,
|
||||||
|
}
|
||||||
|
case hdr.Present:
|
||||||
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PresentMatch{
|
||||||
|
PresentMatch: true,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue // skip this impossible situation
|
||||||
|
}
|
||||||
|
|
||||||
|
if hdr.Invert {
|
||||||
|
eh.InvertMatch = true
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(parts, &envoyrbac.Permission{
|
||||||
|
Rule: &envoyrbac.Permission_Header{
|
||||||
|
Header: eh,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(perm.HTTP.Methods) > 0 {
|
||||||
|
methodHeaderRegex := strings.Join(perm.HTTP.Methods, "|")
|
||||||
|
|
||||||
|
eh := &envoyroute.HeaderMatcher{
|
||||||
|
Name: ":method",
|
||||||
|
HeaderMatchSpecifier: &envoyroute.HeaderMatcher_SafeRegexMatch{
|
||||||
|
SafeRegexMatch: makeEnvoyRegexMatch(methodHeaderRegex),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(parts, &envoyrbac.Permission{
|
||||||
|
Rule: &envoyrbac.Permission_Header{
|
||||||
|
Header: eh,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: if for some reason we errantly allow a permission to be defined
|
||||||
|
// with a body of "http{}" then we'll end up treating that like "ANY" here.
|
||||||
|
return andPermissions(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func notPermission(perm *envoyrbac.Permission) *envoyrbac.Permission {
|
||||||
|
return &envoyrbac.Permission{
|
||||||
|
Rule: &envoyrbac.Permission_NotRule{NotRule: perm},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func andPermissions(perms []*envoyrbac.Permission) *envoyrbac.Permission {
|
||||||
|
switch len(perms) {
|
||||||
|
case 0:
|
||||||
|
return anyPermission()
|
||||||
|
case 1:
|
||||||
|
return perms[0]
|
||||||
|
default:
|
||||||
|
return &envoyrbac.Permission{
|
||||||
|
Rule: &envoyrbac.Permission_AndRules{
|
||||||
|
AndRules: &envoyrbac.Permission_Set{
|
||||||
|
Rules: perms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMakeRBACNetworkFilter(t *testing.T) {
|
func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
|
||||||
testIntention := func(t *testing.T, src, dst string, action structs.IntentionAction) *structs.Intention {
|
testIntention := func(t *testing.T, src, dst string, action structs.IntentionAction) *structs.Intention {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ixn := structs.TestIntention(t)
|
ixn := structs.TestIntention(t)
|
||||||
|
@ -25,6 +25,11 @@ func TestMakeRBACNetworkFilter(t *testing.T) {
|
||||||
testSourceIntention := func(src string, action structs.IntentionAction) *structs.Intention {
|
testSourceIntention := func(src string, action structs.IntentionAction) *structs.Intention {
|
||||||
return testIntention(t, src, "api", action)
|
return testIntention(t, src, "api", action)
|
||||||
}
|
}
|
||||||
|
testSourcePermIntention := func(src string, perms ...*structs.IntentionPermission) *structs.Intention {
|
||||||
|
ixn := testIntention(t, src, "api", "")
|
||||||
|
ixn.Permissions = perms
|
||||||
|
return ixn
|
||||||
|
}
|
||||||
sorted := func(ixns ...*structs.Intention) structs.Intentions {
|
sorted := func(ixns ...*structs.Intention) structs.Intentions {
|
||||||
sort.SliceStable(ixns, func(i, j int) bool {
|
sort.SliceStable(ixns, func(i, j int) bool {
|
||||||
return ixns[j].Precedence < ixns[i].Precedence
|
return ixns[j].Precedence < ixns[i].Precedence
|
||||||
|
@ -97,11 +102,148 @@ func TestMakeRBACNetworkFilter(t *testing.T) {
|
||||||
testSourceIntention("*", structs.IntentionActionDeny),
|
testSourceIntention("*", structs.IntentionActionDeny),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"default-deny-two-path-deny-and-path-allow": {
|
||||||
|
intentionDefaultAllow: false,
|
||||||
|
intentions: sorted(
|
||||||
|
testSourcePermIntention("web",
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/v1/secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/v1/admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"default-allow-two-path-deny-and-path-allow": {
|
||||||
|
intentionDefaultAllow: true,
|
||||||
|
intentions: sorted(
|
||||||
|
testSourcePermIntention("web",
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/v1/secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/v1/admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"default-deny-single-intention-with-kitchen-sink-perms": {
|
||||||
|
intentionDefaultAllow: false,
|
||||||
|
intentions: sorted(
|
||||||
|
testSourcePermIntention("web",
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/v1/secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathRegex: "/v[123]",
|
||||||
|
Methods: []string{"GET", "HEAD", "OPTIONS"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
Header: []structs.IntentionHTTPHeaderPermission{
|
||||||
|
{Name: "x-foo", Present: true},
|
||||||
|
{Name: "x-bar", Exact: "xyz"},
|
||||||
|
{Name: "x-dib", Prefix: "gaz"},
|
||||||
|
{Name: "x-gir", Suffix: "zim"},
|
||||||
|
{Name: "x-zim", Regex: "gi[rR]"},
|
||||||
|
{Name: "z-foo", Present: true, Invert: true},
|
||||||
|
{Name: "z-bar", Exact: "xyz", Invert: true},
|
||||||
|
{Name: "z-dib", Prefix: "gaz", Invert: true},
|
||||||
|
{Name: "z-gir", Suffix: "zim", Invert: true},
|
||||||
|
{Name: "z-zim", Regex: "gi[rR]", Invert: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"default-allow-single-intention-with-kitchen-sink-perms": {
|
||||||
|
intentionDefaultAllow: true,
|
||||||
|
intentions: sorted(
|
||||||
|
testSourcePermIntention("web",
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathExact: "/v1/secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
PathRegex: "/v[123]",
|
||||||
|
Methods: []string{"GET", "HEAD", "OPTIONS"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.IntentionPermission{
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
Header: []structs.IntentionHTTPHeaderPermission{
|
||||||
|
{Name: "x-foo", Present: true},
|
||||||
|
{Name: "x-bar", Exact: "xyz"},
|
||||||
|
{Name: "x-dib", Prefix: "gaz"},
|
||||||
|
{Name: "x-gir", Suffix: "zim"},
|
||||||
|
{Name: "x-zim", Regex: "gi[rR]"},
|
||||||
|
{Name: "z-foo", Present: true, Invert: true},
|
||||||
|
{Name: "z-bar", Exact: "xyz", Invert: true},
|
||||||
|
{Name: "z-dib", Prefix: "gaz", Invert: true},
|
||||||
|
{Name: "z-gir", Suffix: "zim", Invert: true},
|
||||||
|
{Name: "z-zim", Regex: "gi[rR]", Invert: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tt := range tests {
|
for name, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Run("network filter", func(t *testing.T) {
|
||||||
filter, err := makeRBACNetworkFilter(tt.intentions, tt.intentionDefaultAllow)
|
filter, err := makeRBACNetworkFilter(tt.intentions, tt.intentionDefaultAllow)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -109,6 +251,15 @@ func TestMakeRBACNetworkFilter(t *testing.T) {
|
||||||
|
|
||||||
require.JSONEq(t, golden(t, filepath.Join("rbac", name), "", gotJSON), gotJSON)
|
require.JSONEq(t, golden(t, filepath.Join("rbac", name), "", gotJSON), gotJSON)
|
||||||
})
|
})
|
||||||
|
t.Run("http filter", func(t *testing.T) {
|
||||||
|
filter, err := makeRBACHTTPFilter(tt.intentions, tt.intentionDefaultAllow)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gotJSON := protoToJSON(t, filter)
|
||||||
|
|
||||||
|
require.JSONEq(t, golden(t, filepath.Join("rbac", name+"--httpfilter"), "", gotJSON), gotJSON)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"action": "DENY",
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"and_ids": {
|
||||||
|
"ids": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/unsafe$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"action": "DENY",
|
"action": "DENY",
|
||||||
"policies": {
|
"policies": {
|
||||||
"consul-intentions": {
|
"consul-intentions-layer4": {
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"any": true
|
"any": true
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
"safe_regex": {
|
"safe_regex": {
|
||||||
"google_re2": {
|
"google_re2": {
|
||||||
},
|
},
|
||||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
"safe_regex": {
|
"safe_regex": {
|
||||||
"google_re2": {
|
"google_re2": {
|
||||||
},
|
},
|
||||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"not_id": {
|
"not_id": {
|
||||||
"authenticated": {
|
"authenticated": {
|
||||||
|
@ -59,6 +72,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"action": "DENY",
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"action": "DENY",
|
"action": "DENY",
|
||||||
"policies": {
|
"policies": {
|
||||||
"consul-intentions": {
|
"consul-intentions-layer4": {
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"any": true
|
"any": true
|
||||||
|
|
30
agent/xds/testdata/rbac/default-allow-service-wildcard-deny--httpfilter.golden
vendored
Normal file
30
agent/xds/testdata/rbac/default-allow-service-wildcard-deny--httpfilter.golden
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"action": "DENY",
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"action": "DENY",
|
"action": "DENY",
|
||||||
"policies": {
|
"policies": {
|
||||||
"consul-intentions": {
|
"consul-intentions-layer4": {
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"any": true
|
"any": true
|
||||||
|
|
232
agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms--httpfilter.golden
vendored
Normal file
232
agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms--httpfilter.golden
vendored
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"action": "DENY",
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer7-0": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"prefix": "/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "/v[123]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": ":method",
|
||||||
|
"safe_regex_match": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "GET|HEAD|OPTIONS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"prefix": "/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": "x-foo",
|
||||||
|
"present_match": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"exact_match": "xyz",
|
||||||
|
"name": "x-bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": "x-dib",
|
||||||
|
"prefix_match": "gaz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": "x-gir",
|
||||||
|
"suffix_match": "zim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": "x-zim",
|
||||||
|
"safe_regex_match": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "gi[rR]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-foo",
|
||||||
|
"present_match": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"exact_match": "xyz",
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-dib",
|
||||||
|
"prefix_match": "gaz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-gir",
|
||||||
|
"suffix_match": "zim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-zim",
|
||||||
|
"safe_regex_match": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "gi[rR]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "/v[123]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": ":method",
|
||||||
|
"safe_regex_match": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "GET|HEAD|OPTIONS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"prefix": "/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms.golden
vendored
Normal file
31
agent/xds/testdata/rbac/default-allow-single-intention-with-kitchen-sink-perms.golden
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.network.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"action": "DENY",
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stat_prefix": "connect_authz"
|
||||||
|
}
|
||||||
|
}
|
56
agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow--httpfilter.golden
vendored
Normal file
56
agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow--httpfilter.golden
vendored
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"action": "DENY",
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer7-0": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.network.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"action": "DENY",
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stat_prefix": "connect_authz"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"and_ids": {
|
||||||
|
"ids": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
"config": {
|
"config": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"policies": {
|
"policies": {
|
||||||
"consul-intentions": {
|
"consul-intentions-layer4": {
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"any": true
|
"any": true
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"and_ids": {
|
||||||
|
"ids": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/unsafe$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
"config": {
|
"config": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"policies": {
|
"policies": {
|
||||||
"consul-intentions": {
|
"consul-intentions-layer4": {
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"any": true
|
"any": true
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
"safe_regex": {
|
"safe_regex": {
|
||||||
"google_re2": {
|
"google_re2": {
|
||||||
},
|
},
|
||||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
"safe_regex": {
|
"safe_regex": {
|
||||||
"google_re2": {
|
"google_re2": {
|
||||||
},
|
},
|
||||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"not_id": {
|
"not_id": {
|
||||||
"authenticated": {
|
"authenticated": {
|
||||||
|
@ -58,6 +71,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_id": {
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
"config": {
|
"config": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"policies": {
|
"policies": {
|
||||||
"consul-intentions": {
|
"consul-intentions-layer4": {
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"any": true
|
"any": true
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
"config": {
|
"config": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"policies": {
|
"policies": {
|
||||||
"consul-intentions": {
|
"consul-intentions-layer4": {
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"any": true
|
"any": true
|
||||||
|
|
29
agent/xds/testdata/rbac/default-deny-service-wildcard-allow--httpfilter.golden
vendored
Normal file
29
agent/xds/testdata/rbac/default-deny-service-wildcard-allow--httpfilter.golden
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer4": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"any": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/[^/]+$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
"config": {
|
"config": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"policies": {
|
"policies": {
|
||||||
"consul-intentions": {
|
"consul-intentions-layer4": {
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"any": true
|
"any": true
|
||||||
|
|
231
agent/xds/testdata/rbac/default-deny-single-intention-with-kitchen-sink-perms--httpfilter.golden
vendored
Normal file
231
agent/xds/testdata/rbac/default-deny-single-intention-with-kitchen-sink-perms--httpfilter.golden
vendored
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer7-0": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"prefix": "/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "/v[123]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": ":method",
|
||||||
|
"safe_regex_match": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "GET|HEAD|OPTIONS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"prefix": "/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": "x-foo",
|
||||||
|
"present_match": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"exact_match": "xyz",
|
||||||
|
"name": "x-bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": "x-dib",
|
||||||
|
"prefix_match": "gaz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": "x-gir",
|
||||||
|
"suffix_match": "zim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": "x-zim",
|
||||||
|
"safe_regex_match": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "gi[rR]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-foo",
|
||||||
|
"present_match": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"exact_match": "xyz",
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-dib",
|
||||||
|
"prefix_match": "gaz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-gir",
|
||||||
|
"suffix_match": "zim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"invert_match": true,
|
||||||
|
"name": "z-zim",
|
||||||
|
"safe_regex_match": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "gi[rR]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "/v[123]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"name": ":method",
|
||||||
|
"safe_regex_match": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "GET|HEAD|OPTIONS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"prefix": "/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
agent/xds/testdata/rbac/default-deny-single-intention-with-kitchen-sink-perms.golden
vendored
Normal file
8
agent/xds/testdata/rbac/default-deny-single-intention-with-kitchen-sink-perms.golden
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.network.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
},
|
||||||
|
"stat_prefix": "connect_authz"
|
||||||
|
}
|
||||||
|
}
|
57
agent/xds/testdata/rbac/default-deny-two-path-deny-and-path-allow--httpfilter.golden
vendored
Normal file
57
agent/xds/testdata/rbac/default-deny-two-path-deny-and-path-allow--httpfilter.golden
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.http.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
"policies": {
|
||||||
|
"consul-intentions-layer7-0": {
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"and_rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"prefix": "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not_rule": {
|
||||||
|
"url_path": {
|
||||||
|
"path": {
|
||||||
|
"exact": "/v1/secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"principals": [
|
||||||
|
{
|
||||||
|
"authenticated": {
|
||||||
|
"principal_name": {
|
||||||
|
"safe_regex": {
|
||||||
|
"google_re2": {
|
||||||
|
},
|
||||||
|
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "envoy.filters.network.rbac",
|
||||||
|
"config": {
|
||||||
|
"rules": {
|
||||||
|
},
|
||||||
|
"stat_prefix": "connect_authz"
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,8 @@ type ServiceIntentionsConfigEntry struct {
|
||||||
type SourceIntention struct {
|
type SourceIntention struct {
|
||||||
Name string
|
Name string
|
||||||
Namespace string `json:",omitempty"`
|
Namespace string `json:",omitempty"`
|
||||||
Action IntentionAction
|
Action IntentionAction `json:",omitempty"`
|
||||||
|
Permissions []*IntentionPermission `json:",omitempty"`
|
||||||
Precedence int
|
Precedence int
|
||||||
Type IntentionSourceType
|
Type IntentionSourceType
|
||||||
Description string `json:",omitempty"`
|
Description string `json:",omitempty"`
|
||||||
|
@ -52,3 +53,28 @@ func (e *ServiceIntentionsConfigEntry) GetCreateIndex() uint64 {
|
||||||
func (e *ServiceIntentionsConfigEntry) GetModifyIndex() uint64 {
|
func (e *ServiceIntentionsConfigEntry) GetModifyIndex() uint64 {
|
||||||
return e.ModifyIndex
|
return e.ModifyIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IntentionPermission struct {
|
||||||
|
Action IntentionAction
|
||||||
|
HTTP *IntentionHTTPPermission `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntentionHTTPPermission struct {
|
||||||
|
PathExact string `json:",omitempty" alias:"path_exact"`
|
||||||
|
PathPrefix string `json:",omitempty" alias:"path_prefix"`
|
||||||
|
PathRegex string `json:",omitempty" alias:"path_regex"`
|
||||||
|
|
||||||
|
Header []IntentionHTTPHeaderPermission `json:",omitempty"`
|
||||||
|
|
||||||
|
Methods []string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntentionHTTPHeaderPermission struct {
|
||||||
|
Name string
|
||||||
|
Present bool `json:",omitempty"`
|
||||||
|
Exact string `json:",omitempty"`
|
||||||
|
Prefix string `json:",omitempty"`
|
||||||
|
Suffix string `json:",omitempty"`
|
||||||
|
Regex string `json:",omitempty"`
|
||||||
|
Invert bool `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
|
@ -900,6 +900,168 @@ func TestDecodeConfigEntry(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "service-intentions: kitchen sink",
|
||||||
|
body: `
|
||||||
|
{
|
||||||
|
"Kind": "service-intentions",
|
||||||
|
"Name": "web",
|
||||||
|
"Meta" : {
|
||||||
|
"foo": "bar",
|
||||||
|
"gir": "zim"
|
||||||
|
},
|
||||||
|
"Sources": [
|
||||||
|
{
|
||||||
|
"Name": "foo",
|
||||||
|
"Action": "deny",
|
||||||
|
"Type": "consul",
|
||||||
|
"Description": "foo desc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "bar",
|
||||||
|
"Action": "allow",
|
||||||
|
"Description": "bar desc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "l7",
|
||||||
|
"Permissions": [
|
||||||
|
{
|
||||||
|
"Action": "deny",
|
||||||
|
"HTTP": {
|
||||||
|
"PathExact": "/admin",
|
||||||
|
"Header": [
|
||||||
|
{
|
||||||
|
"Name": "hdr-present",
|
||||||
|
"Present": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-exact",
|
||||||
|
"Exact": "exact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-prefix",
|
||||||
|
"Prefix": "prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-suffix",
|
||||||
|
"Suffix": "suffix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-regex",
|
||||||
|
"Regex": "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-absent",
|
||||||
|
"Present": true,
|
||||||
|
"Invert": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": "allow",
|
||||||
|
"HTTP": {
|
||||||
|
"PathPrefix": "/v3/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": "allow",
|
||||||
|
"HTTP": {
|
||||||
|
"PathRegex": "/v[12]/.*",
|
||||||
|
"Methods": [
|
||||||
|
"GET",
|
||||||
|
"POST"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "*",
|
||||||
|
"Action": "deny",
|
||||||
|
"Description": "wild desc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expect: &ServiceIntentionsConfigEntry{
|
||||||
|
Kind: "service-intentions",
|
||||||
|
Name: "web",
|
||||||
|
Meta: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"gir": "zim",
|
||||||
|
},
|
||||||
|
Sources: []*SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Action: "deny",
|
||||||
|
Type: "consul",
|
||||||
|
Description: "foo desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "bar",
|
||||||
|
Action: "allow",
|
||||||
|
Description: "bar desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "l7",
|
||||||
|
Permissions: []*IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: "deny",
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathExact: "/admin",
|
||||||
|
Header: []IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "hdr-present",
|
||||||
|
Present: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-exact",
|
||||||
|
Exact: "exact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-prefix",
|
||||||
|
Prefix: "prefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-suffix",
|
||||||
|
Suffix: "suffix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-regex",
|
||||||
|
Regex: "regex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-absent",
|
||||||
|
Present: true,
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "allow",
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/v3/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "allow",
|
||||||
|
HTTP: &IntentionHTTPPermission{
|
||||||
|
PathRegex: "/v[12]/.*",
|
||||||
|
Methods: []string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "*",
|
||||||
|
Action: "deny",
|
||||||
|
Description: "wild desc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
tc := tc
|
tc := tc
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,14 @@ type Intention struct {
|
||||||
SourceType IntentionSourceType
|
SourceType IntentionSourceType
|
||||||
|
|
||||||
// Action is whether this is an allowlist or denylist intention.
|
// Action is whether this is an allowlist or denylist intention.
|
||||||
Action IntentionAction
|
Action IntentionAction `json:",omitempty"`
|
||||||
|
|
||||||
|
// Permissions is the list of additional L7 attributes that extend the
|
||||||
|
// intention definition.
|
||||||
|
//
|
||||||
|
// NOTE: This field is not editable unless editing the underlying
|
||||||
|
// service-intentions config entry directly.
|
||||||
|
Permissions []*IntentionPermission `json:",omitempty"`
|
||||||
|
|
||||||
// DefaultAddr is not used.
|
// DefaultAddr is not used.
|
||||||
// Deprecated: DefaultAddr is not used and may be removed in a future version.
|
// Deprecated: DefaultAddr is not used and may be removed in a future version.
|
||||||
|
@ -69,10 +76,20 @@ type Intention struct {
|
||||||
|
|
||||||
// String returns human-friendly output describing ths intention.
|
// String returns human-friendly output describing ths intention.
|
||||||
func (i *Intention) String() string {
|
func (i *Intention) String() string {
|
||||||
|
var detail string
|
||||||
|
switch n := len(i.Permissions); n {
|
||||||
|
case 0:
|
||||||
|
detail = string(i.Action)
|
||||||
|
case 1:
|
||||||
|
detail = "1 permission"
|
||||||
|
default:
|
||||||
|
detail = fmt.Sprintf("%d permissions", len(i.Permissions))
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s => %s (%s)",
|
return fmt.Sprintf("%s => %s (%s)",
|
||||||
i.SourceString(),
|
i.SourceString(),
|
||||||
i.DestinationString(),
|
i.DestinationString(),
|
||||||
i.Action)
|
detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SourceString returns the namespace/name format for the source, or
|
// SourceString returns the namespace/name format for the source, or
|
||||||
|
|
|
@ -1931,6 +1931,57 @@ func TestParseConfigEntry(t *testing.T) {
|
||||||
name = "bar"
|
name = "bar"
|
||||||
action = "allow"
|
action = "allow"
|
||||||
description = "bar desc"
|
description = "bar desc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "l7"
|
||||||
|
permissions = [
|
||||||
|
{
|
||||||
|
action = "deny"
|
||||||
|
http {
|
||||||
|
path_exact = "/admin"
|
||||||
|
header = [
|
||||||
|
{
|
||||||
|
name = "hdr-present"
|
||||||
|
present = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-exact"
|
||||||
|
exact = "exact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-prefix"
|
||||||
|
prefix = "prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-suffix"
|
||||||
|
suffix = "suffix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-regex"
|
||||||
|
regex = "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "hdr-absent"
|
||||||
|
present = true
|
||||||
|
invert = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_prefix = "/v3/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_regex = "/v[12]/.*"
|
||||||
|
methods = ["GET", "POST"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
sources {
|
sources {
|
||||||
|
@ -1957,6 +2008,57 @@ func TestParseConfigEntry(t *testing.T) {
|
||||||
Name = "bar"
|
Name = "bar"
|
||||||
Action = "allow"
|
Action = "allow"
|
||||||
Description = "bar desc"
|
Description = "bar desc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "l7"
|
||||||
|
Permissions = [
|
||||||
|
{
|
||||||
|
Action = "deny"
|
||||||
|
HTTP {
|
||||||
|
PathExact = "/admin"
|
||||||
|
Header = [
|
||||||
|
{
|
||||||
|
Name = "hdr-present"
|
||||||
|
Present = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-exact"
|
||||||
|
Exact = "exact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-prefix"
|
||||||
|
Prefix = "prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-suffix"
|
||||||
|
Suffix = "suffix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-regex"
|
||||||
|
Regex = "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name = "hdr-absent"
|
||||||
|
Present = true
|
||||||
|
Invert = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action = "allow"
|
||||||
|
HTTP {
|
||||||
|
PathPrefix = "/v3/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action = "allow"
|
||||||
|
HTTP {
|
||||||
|
PathRegex = "/v[12]/.*"
|
||||||
|
Methods = ["GET", "POST"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Sources {
|
Sources {
|
||||||
|
@ -1985,6 +2087,60 @@ func TestParseConfigEntry(t *testing.T) {
|
||||||
"action": "allow",
|
"action": "allow",
|
||||||
"description": "bar desc"
|
"description": "bar desc"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "l7",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"action": "deny",
|
||||||
|
"http": {
|
||||||
|
"path_exact": "/admin",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"name": "hdr-present",
|
||||||
|
"present": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hdr-exact",
|
||||||
|
"exact": "exact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hdr-prefix",
|
||||||
|
"prefix": "prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hdr-suffix",
|
||||||
|
"suffix": "suffix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hdr-regex",
|
||||||
|
"regex": "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hdr-absent",
|
||||||
|
"present": true,
|
||||||
|
"invert": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "allow",
|
||||||
|
"http": {
|
||||||
|
"path_prefix": "/v3/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "allow",
|
||||||
|
"http": {
|
||||||
|
"path_regex": "/v[12]/.*",
|
||||||
|
"methods": [
|
||||||
|
"GET",
|
||||||
|
"POST"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "*",
|
"name": "*",
|
||||||
"action": "deny",
|
"action": "deny",
|
||||||
|
@ -2013,6 +2169,60 @@ func TestParseConfigEntry(t *testing.T) {
|
||||||
"Action": "allow",
|
"Action": "allow",
|
||||||
"Description": "bar desc"
|
"Description": "bar desc"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Name": "l7",
|
||||||
|
"Permissions": [
|
||||||
|
{
|
||||||
|
"Action": "deny",
|
||||||
|
"HTTP": {
|
||||||
|
"PathExact": "/admin",
|
||||||
|
"Header": [
|
||||||
|
{
|
||||||
|
"Name": "hdr-present",
|
||||||
|
"Present": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-exact",
|
||||||
|
"Exact": "exact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-prefix",
|
||||||
|
"Prefix": "prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-suffix",
|
||||||
|
"Suffix": "suffix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-regex",
|
||||||
|
"Regex": "regex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hdr-absent",
|
||||||
|
"Present": true,
|
||||||
|
"Invert": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": "allow",
|
||||||
|
"HTTP": {
|
||||||
|
"PathPrefix": "/v3/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": "allow",
|
||||||
|
"HTTP": {
|
||||||
|
"PathRegex": "/v[12]/.*",
|
||||||
|
"Methods": [
|
||||||
|
"GET",
|
||||||
|
"POST"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "*",
|
"Name": "*",
|
||||||
"Action": "deny",
|
"Action": "deny",
|
||||||
|
@ -2040,6 +2250,57 @@ func TestParseConfigEntry(t *testing.T) {
|
||||||
Action: "allow",
|
Action: "allow",
|
||||||
Description: "bar desc",
|
Description: "bar desc",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "l7",
|
||||||
|
Permissions: []*api.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: "deny",
|
||||||
|
HTTP: &api.IntentionHTTPPermission{
|
||||||
|
PathExact: "/admin",
|
||||||
|
Header: []api.IntentionHTTPHeaderPermission{
|
||||||
|
{
|
||||||
|
Name: "hdr-present",
|
||||||
|
Present: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-exact",
|
||||||
|
Exact: "exact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-prefix",
|
||||||
|
Prefix: "prefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-suffix",
|
||||||
|
Suffix: "suffix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-regex",
|
||||||
|
Regex: "regex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "hdr-absent",
|
||||||
|
Present: true,
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "allow",
|
||||||
|
HTTP: &api.IntentionHTTPPermission{
|
||||||
|
PathPrefix: "/v3/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: "allow",
|
||||||
|
HTTP: &api.IntentionHTTPPermission{
|
||||||
|
PathRegex: "/v[12]/.*",
|
||||||
|
Methods: []string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "*",
|
Name: "*",
|
||||||
Action: "deny",
|
Action: "deny",
|
||||||
|
|
|
@ -177,22 +177,34 @@ func (c *cmd) ixnsFromArgs(args []string) ([]*api.Intention, error) {
|
||||||
func (c *cmd) ixnsFromFiles(args []string) ([]*api.Intention, error) {
|
func (c *cmd) ixnsFromFiles(args []string) ([]*api.Intention, error) {
|
||||||
var result []*api.Intention
|
var result []*api.Intention
|
||||||
for _, path := range args {
|
for _, path := range args {
|
||||||
|
ixn, err := c.ixnFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, ixn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cmd) ixnFromFile(path string) (*api.Intention, error) {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
var ixn api.Intention
|
var ixn api.Intention
|
||||||
err = json.NewDecoder(f).Decode(&ixn)
|
if err := json.NewDecoder(f).Decode(&ixn); err != nil {
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, &ixn)
|
if len(ixn.Permissions) > 0 {
|
||||||
|
return nil, fmt.Errorf("cannot create L7 intention from file %q using this CLI; use 'consul config write' instead", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return &ixn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ixnAction returns the api.IntentionAction based on the flag set.
|
// ixnAction returns the api.IntentionAction based on the flag set.
|
||||||
|
|
|
@ -174,6 +174,47 @@ func TestIntentionCreate_File(t *testing.T) {
|
||||||
require.Equal(api.IntentionActionAllow, ixns[0].Action)
|
require.Equal(api.IntentionActionAllow, ixns[0].Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntentionCreate_File_L7_fails(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require := require.New(t)
|
||||||
|
a := agent.NewTestAgent(t, ``)
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
|
||||||
|
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
c := New(ui)
|
||||||
|
|
||||||
|
contents := `
|
||||||
|
{
|
||||||
|
"SourceName": "foo",
|
||||||
|
"DestinationName": "bar",
|
||||||
|
"Permissions": [
|
||||||
|
{
|
||||||
|
"Action": "allow",
|
||||||
|
"HTTP": {
|
||||||
|
"PathExact": "/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
f := testutil.TempFile(t, "intention-create-command-file")
|
||||||
|
if _, err := f.WriteString(contents); err != nil {
|
||||||
|
t.Fatalf("err: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-file",
|
||||||
|
f.Name(),
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(1, c.Run(args), ui.ErrorWriter.String())
|
||||||
|
require.Contains(ui.ErrorWriter.String(), "cannot create L7 intention from file")
|
||||||
|
}
|
||||||
|
|
||||||
func TestIntentionCreate_FileNoExist(t *testing.T) {
|
func TestIntentionCreate_FileNoExist(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,28 @@ func TestIntentionDelete(t *testing.T) {
|
||||||
}, nil)
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Ensure "api" is L7
|
||||||
|
_, _, err = client.ConfigEntries().Set(&api.ServiceConfigEntry{
|
||||||
|
Kind: api.ServiceDefaults,
|
||||||
|
Name: "api",
|
||||||
|
Protocol: "http",
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.Connect().IntentionUpsert(&api.Intention{
|
||||||
|
SourceName: "web",
|
||||||
|
DestinationName: "api",
|
||||||
|
Permissions: []*api.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: api.IntentionActionAllow,
|
||||||
|
HTTP: &api.IntentionHTTPPermission{
|
||||||
|
PathExact: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("l4 intention", func(t *testing.T) {
|
t.Run("l4 intention", func(t *testing.T) {
|
||||||
t.Run("one arg", func(t *testing.T) {
|
t.Run("one arg", func(t *testing.T) {
|
||||||
ui := cli.NewMockUi()
|
ui := cli.NewMockUi()
|
||||||
|
@ -110,6 +132,20 @@ func TestIntentionDelete(t *testing.T) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("l7 intention", func(t *testing.T) {
|
||||||
|
t.Run("two args", func(t *testing.T) {
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
c := New(ui)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"web", "api",
|
||||||
|
}
|
||||||
|
require.Equal(t, 0, c.Run(args), ui.ErrorWriter.String())
|
||||||
|
require.Contains(t, ui.OutputWriter.String(), "deleted")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// They should all be gone.
|
// They should all be gone.
|
||||||
{
|
{
|
||||||
ixns, _, err := client.Connect().Intentions(nil)
|
ixns, _, err := client.Connect().Intentions(nil)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package get
|
package get
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -60,7 +61,9 @@ func (c *cmd) Run(args []string) int {
|
||||||
data := []string{
|
data := []string{
|
||||||
fmt.Sprintf("Source:\x1f%s", ixn.SourceString()),
|
fmt.Sprintf("Source:\x1f%s", ixn.SourceString()),
|
||||||
fmt.Sprintf("Destination:\x1f%s", ixn.DestinationString()),
|
fmt.Sprintf("Destination:\x1f%s", ixn.DestinationString()),
|
||||||
fmt.Sprintf("Action:\x1f%s", ixn.Action),
|
}
|
||||||
|
if ixn.Action != "" {
|
||||||
|
data = append(data, fmt.Sprintf("Action:\x1f%s", ixn.Action))
|
||||||
}
|
}
|
||||||
if ixn.ID != "" {
|
if ixn.ID != "" {
|
||||||
data = append(data, fmt.Sprintf("ID:\x1f%s", ixn.ID))
|
data = append(data, fmt.Sprintf("ID:\x1f%s", ixn.ID))
|
||||||
|
@ -84,6 +87,14 @@ func (c *cmd) Run(args []string) int {
|
||||||
|
|
||||||
c.UI.Output(columnize.Format(data, &columnize.Config{Delim: string([]byte{0x1f})}))
|
c.UI.Output(columnize.Format(data, &columnize.Config{Delim: string([]byte{0x1f})}))
|
||||||
|
|
||||||
|
if len(ixn.Permissions) > 0 {
|
||||||
|
b, err := json.MarshalIndent(ixn.Permissions, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
c.UI.Output("Permissions:\n" + string(b))
|
||||||
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,28 @@ func TestGetFromArgs(t *testing.T) {
|
||||||
}, nil)
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Ensure "y" is L7
|
||||||
|
_, _, err = client.ConfigEntries().Set(&api.ServiceConfigEntry{
|
||||||
|
Kind: api.ServiceDefaults,
|
||||||
|
Name: "y",
|
||||||
|
Protocol: "http",
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.Connect().IntentionUpsert(&api.Intention{
|
||||||
|
SourceName: "x",
|
||||||
|
DestinationName: "y",
|
||||||
|
Permissions: []*api.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: api.IntentionActionAllow,
|
||||||
|
HTTP: &api.IntentionHTTPPermission{
|
||||||
|
PathExact: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("l4 intention", func(t *testing.T) {
|
t.Run("l4 intention", func(t *testing.T) {
|
||||||
t.Run("one arg", func(t *testing.T) {
|
t.Run("one arg", func(t *testing.T) {
|
||||||
ixn, err := GetFromArgs(client, []string{id0})
|
ixn, err := GetFromArgs(client, []string{id0})
|
||||||
|
@ -36,6 +58,7 @@ func TestGetFromArgs(t *testing.T) {
|
||||||
require.Equal(t, "a", ixn.SourceName)
|
require.Equal(t, "a", ixn.SourceName)
|
||||||
require.Equal(t, "b", ixn.DestinationName)
|
require.Equal(t, "b", ixn.DestinationName)
|
||||||
require.Equal(t, api.IntentionActionAllow, ixn.Action)
|
require.Equal(t, api.IntentionActionAllow, ixn.Action)
|
||||||
|
require.Empty(t, ixn.Permissions)
|
||||||
})
|
})
|
||||||
t.Run("two args", func(t *testing.T) {
|
t.Run("two args", func(t *testing.T) {
|
||||||
ixn, err := GetFromArgs(client, []string{"a", "b"})
|
ixn, err := GetFromArgs(client, []string{"a", "b"})
|
||||||
|
@ -44,6 +67,24 @@ func TestGetFromArgs(t *testing.T) {
|
||||||
require.Equal(t, "a", ixn.SourceName)
|
require.Equal(t, "a", ixn.SourceName)
|
||||||
require.Equal(t, "b", ixn.DestinationName)
|
require.Equal(t, "b", ixn.DestinationName)
|
||||||
require.Equal(t, api.IntentionActionAllow, ixn.Action)
|
require.Equal(t, api.IntentionActionAllow, ixn.Action)
|
||||||
|
require.Empty(t, ixn.Permissions)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("l7 intention", func(t *testing.T) {
|
||||||
|
t.Run("two args", func(t *testing.T) {
|
||||||
|
ixn, err := GetFromArgs(client, []string{"x", "y"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, ixn.ID)
|
||||||
|
require.Equal(t, "x", ixn.SourceName)
|
||||||
|
require.Equal(t, "y", ixn.DestinationName)
|
||||||
|
require.Empty(t, ixn.Action)
|
||||||
|
require.Equal(t, []*api.IntentionPermission{{
|
||||||
|
Action: api.IntentionActionAllow,
|
||||||
|
HTTP: &api.IntentionHTTPPermission{
|
||||||
|
PathExact: "/foo",
|
||||||
|
},
|
||||||
|
}}, ixn.Permissions)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
snapshot_envoy_admin localhost:19000 s1 primary || true
|
||||||
|
snapshot_envoy_admin localhost:19001 s2 || true
|
|
@ -0,0 +1,97 @@
|
||||||
|
enable_central_service_config = true
|
||||||
|
|
||||||
|
acl {
|
||||||
|
default_policy = "deny"
|
||||||
|
}
|
||||||
|
|
||||||
|
config_entries {
|
||||||
|
bootstrap {
|
||||||
|
kind = "service-defaults"
|
||||||
|
name = "s2"
|
||||||
|
protocol = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: test header invert
|
||||||
|
bootstrap {
|
||||||
|
kind = "service-intentions"
|
||||||
|
name = "s2"
|
||||||
|
|
||||||
|
sources {
|
||||||
|
name = "s1"
|
||||||
|
permissions = [
|
||||||
|
// paths
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http { path_exact = "/exact" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http { path_prefix = "/prefix" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http { path_regex = "/reg[ex]{2}" }
|
||||||
|
},
|
||||||
|
// headers
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_exact = "/hdr-present"
|
||||||
|
header = [{
|
||||||
|
name = "x-test-debug"
|
||||||
|
present = true
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_exact = "/hdr-exact"
|
||||||
|
header = [{
|
||||||
|
name = "x-test-debug"
|
||||||
|
exact = "exact"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_exact = "/hdr-prefix"
|
||||||
|
header = [{
|
||||||
|
name = "x-test-debug"
|
||||||
|
prefix = "prefi"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_exact = "/hdr-suffix"
|
||||||
|
header = [{
|
||||||
|
name = "x-test-debug"
|
||||||
|
suffix = "uffix"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_exact = "/hdr-regex"
|
||||||
|
header = [{
|
||||||
|
name = "x-test-debug"
|
||||||
|
regex = "reg[ex]{2}"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// methods
|
||||||
|
{
|
||||||
|
action = "allow"
|
||||||
|
http {
|
||||||
|
path_exact = "/method-match"
|
||||||
|
methods = ["GET", "PUT"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# wait for bootstrap to apply config entries
|
||||||
|
wait_for_config_entry service-defaults s2
|
||||||
|
wait_for_config_entry service-intentions s2
|
||||||
|
|
||||||
|
gen_envoy_bootstrap s1 19000
|
||||||
|
gen_envoy_bootstrap s2 19001
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
load helpers
|
||||||
|
|
||||||
|
@test "s1 proxy admin is up on :19000" {
|
||||||
|
retry_default curl -f -s localhost:19000/stats -o /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s2 proxy admin is up on :19001" {
|
||||||
|
retry_default curl -f -s localhost:19001/stats -o /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 proxy listener should be up and have right cert" {
|
||||||
|
assert_proxy_presents_cert_uri localhost:21000 s1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s2 proxy listener should be up and have right cert" {
|
||||||
|
assert_proxy_presents_cert_uri localhost:21001 s2
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s2 proxies should be healthy" {
|
||||||
|
assert_service_has_healthy_instances s2 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 upstream should have healthy endpoints for s2" {
|
||||||
|
assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# these all use the same context: "s1 upstream should be able to connect to s2 via upstream s2"
|
||||||
|
|
||||||
|
@test "test exact path" {
|
||||||
|
must_pass_http_request GET localhost:5000/exact
|
||||||
|
must_fail_http_request GET localhost:5000/exact-nope
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "test prefix path" {
|
||||||
|
must_pass_http_request GET localhost:5000/prefix
|
||||||
|
must_fail_http_request GET localhost:5000/nope-prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "test regex path" {
|
||||||
|
must_pass_http_request GET localhost:5000/regex
|
||||||
|
must_fail_http_request GET localhost:5000/reggex
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "test present header" {
|
||||||
|
must_pass_http_request GET localhost:5000/hdr-present anything
|
||||||
|
must_fail_http_request GET localhost:5000/hdr-present ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "test exact header" {
|
||||||
|
must_pass_http_request GET localhost:5000/hdr-exact exact
|
||||||
|
must_fail_http_request GET localhost:5000/hdr-exact exact-nope
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "test prefix header" {
|
||||||
|
must_pass_http_request GET localhost:5000/hdr-prefix prefix
|
||||||
|
must_fail_http_request GET localhost:5000/hdr-prefix nope-prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "test suffix header" {
|
||||||
|
must_pass_http_request GET localhost:5000/hdr-suffix suffix
|
||||||
|
must_fail_http_request GET localhost:5000/hdr-suffix suffix-nope
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "test regex header" {
|
||||||
|
must_pass_http_request GET localhost:5000/hdr-regex regex
|
||||||
|
must_fail_http_request GET localhost:5000/hdr-regex reggex
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "test method match" {
|
||||||
|
must_pass_http_request GET localhost:5000/method-match
|
||||||
|
must_pass_http_request PUT localhost:5000/method-match
|
||||||
|
must_fail_http_request POST localhost:5000/method-match
|
||||||
|
must_fail_http_request HEAD localhost:5000/method-match
|
||||||
|
}
|
||||||
|
|
||||||
|
# @test "s1 upstream should NOT be able to connect to s2" {
|
||||||
|
# run retry_default must_fail_tcp_connection localhost:5000
|
||||||
|
|
||||||
|
# echo "OUTPUT $output"
|
||||||
|
|
||||||
|
# [ "$status" == "0" ]
|
||||||
|
# }
|
|
@ -554,6 +554,73 @@ function must_fail_http_connection {
|
||||||
echo "$output" | grep "${expect_response}"
|
echo "$output" | grep "${expect_response}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# must_pass_http_request allows you to craft a specific http request to assert
|
||||||
|
# that envoy will NOT reject the request. Primarily of use for testing L7
|
||||||
|
# intentions.
|
||||||
|
function must_pass_http_request {
|
||||||
|
local METHOD=$1
|
||||||
|
local URL=$2
|
||||||
|
local DEBUG_HEADER_VALUE="${3:-""}"
|
||||||
|
|
||||||
|
local extra_args
|
||||||
|
if [[ -n "${DEBUG_HEADER_VALUE}" ]]; then
|
||||||
|
extra_args="-H x-test-debug:${DEBUG_HEADER_VALUE}"
|
||||||
|
fi
|
||||||
|
case "$METHOD" in
|
||||||
|
GET)
|
||||||
|
;;
|
||||||
|
DELETE)
|
||||||
|
extra_args="$extra_args -X${METHOD}"
|
||||||
|
;;
|
||||||
|
PUT|POST)
|
||||||
|
extra_args="$extra_args -d'{}' -X${METHOD}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
run retry_default curl -v -s -f $extra_args "$URL"
|
||||||
|
[ "$status" == 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# must_fail_http_request allows you to craft a specific http request to assert
|
||||||
|
# that envoy will reject the request. Primarily of use for testing L7
|
||||||
|
# intentions.
|
||||||
|
function must_fail_http_request {
|
||||||
|
local METHOD=$1
|
||||||
|
local URL=$2
|
||||||
|
local DEBUG_HEADER_VALUE="${3:-""}"
|
||||||
|
|
||||||
|
local extra_args
|
||||||
|
if [[ -n "${DEBUG_HEADER_VALUE}" ]]; then
|
||||||
|
extra_args="-H x-test-debug:${DEBUG_HEADER_VALUE}"
|
||||||
|
fi
|
||||||
|
case "$METHOD" in
|
||||||
|
HEAD)
|
||||||
|
extra_args="$extra_args -I"
|
||||||
|
;;
|
||||||
|
GET)
|
||||||
|
;;
|
||||||
|
DELETE)
|
||||||
|
extra_args="$extra_args -X${METHOD}"
|
||||||
|
;;
|
||||||
|
PUT|POST)
|
||||||
|
extra_args="$extra_args -d'{}' -X${METHOD}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Attempt to curl through upstream
|
||||||
|
run retry_default curl -s -i $extra_args "$URL"
|
||||||
|
|
||||||
|
echo "OUTPUT $output"
|
||||||
|
|
||||||
|
echo "$output" | grep "403 Forbidden"
|
||||||
|
}
|
||||||
|
|
||||||
function gen_envoy_bootstrap {
|
function gen_envoy_bootstrap {
|
||||||
SERVICE=$1
|
SERVICE=$1
|
||||||
ADMIN_PORT=$2
|
ADMIN_PORT=$2
|
||||||
|
|
|
@ -38,6 +38,7 @@ func TestEnvoy(t *testing.T) {
|
||||||
"case-ingress-gateway-simple",
|
"case-ingress-gateway-simple",
|
||||||
"case-ingress-gateway-tls",
|
"case-ingress-gateway-tls",
|
||||||
"case-ingress-mesh-gateways-resolver",
|
"case-ingress-mesh-gateways-resolver",
|
||||||
|
"case-l7-intentions",
|
||||||
"case-multidc-rsa-ca",
|
"case-multidc-rsa-ca",
|
||||||
"case-prometheus",
|
"case-prometheus",
|
||||||
"case-statsd-udp",
|
"case-statsd-udp",
|
||||||
|
|
|
@ -6,11 +6,34 @@ unset CDPATH
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
## no rbac url_path support
|
||||||
|
# 1.12.0
|
||||||
|
# 1.12.1
|
||||||
|
# 1.12.2
|
||||||
|
# 1.13.0
|
||||||
|
|
||||||
|
## does not exist in docker
|
||||||
|
# 1.13.5
|
||||||
|
# 1.14.0
|
||||||
versions=(
|
versions=(
|
||||||
1.15.0
|
1.12.3
|
||||||
1.14.4
|
1.12.4
|
||||||
1.13.4
|
1.12.5
|
||||||
1.12.6
|
1.12.6
|
||||||
|
1.12.7
|
||||||
|
1.13.1
|
||||||
|
1.13.2
|
||||||
|
1.13.3
|
||||||
|
1.13.4
|
||||||
|
1.13.6
|
||||||
|
1.14.1
|
||||||
|
1.14.2
|
||||||
|
1.14.3
|
||||||
|
1.14.4
|
||||||
|
1.14.5
|
||||||
|
1.15.0
|
||||||
|
1.15.1
|
||||||
|
1.15.2
|
||||||
)
|
)
|
||||||
|
|
||||||
for v in "${versions[@]}"; do
|
for v in "${versions[@]}"; do
|
||||||
|
|
Loading…
Reference in New Issue