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
|
||||
//
|
||||
// NOTE: This endpoint treats any L7 intentions as DENY.
|
||||
//
|
||||
// Note: when this logic changes, consider if the Intention.Check RPC method
|
||||
// also needs to be updated.
|
||||
func (s *HTTPHandlers) AgentConnectAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
|
|
|
@ -11,10 +11,15 @@ import (
|
|||
"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
|
||||
// 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.
|
||||
//
|
||||
// NOTE: This treats any L7 intentions as DENY.
|
||||
//
|
||||
// The ACL token and the auth request are provided and the auth decision (true
|
||||
// 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"))
|
||||
}
|
||||
|
||||
// Test the authorization for each match
|
||||
// Figure out which source matches this request.
|
||||
var ixnMatch *structs.Intention
|
||||
for _, ixn := range reply.Matches[0] {
|
||||
if auth, ok := uriService.Authorize(ixn); ok {
|
||||
reason = fmt.Sprintf("Matched intention: %s", ixn.String())
|
||||
if _, ok := uriService.Authorize(ixn); ok {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -288,10 +288,10 @@ func (s *Intention) Apply(
|
|||
}
|
||||
|
||||
if prevEntry == nil {
|
||||
upsertEntry = args.Intention.ToConfigEntry()
|
||||
upsertEntry = args.Intention.ToConfigEntry(true)
|
||||
} else {
|
||||
upsertEntry = prevEntry.Clone()
|
||||
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention())
|
||||
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention(true))
|
||||
}
|
||||
|
||||
case structs.IntentionOpUpdate:
|
||||
|
@ -306,7 +306,7 @@ func (s *Intention) Apply(
|
|||
upsertEntry = prevEntry.Clone()
|
||||
for i, src := range upsertEntry.Sources {
|
||||
if src.LegacyID == args.Intention.ID {
|
||||
upsertEntry.Sources[i] = args.Intention.ToSourceIntention()
|
||||
upsertEntry.Sources[i] = args.Intention.ToSourceIntention(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -339,7 +339,7 @@ func (s *Intention) Apply(
|
|||
return fmt.Errorf("Meta must not be specified")
|
||||
}
|
||||
|
||||
upsertEntry = args.Intention.ToConfigEntry()
|
||||
upsertEntry = args.Intention.ToConfigEntry(false)
|
||||
} else {
|
||||
upsertEntry = prevEntry.Clone()
|
||||
|
||||
|
@ -363,13 +363,13 @@ func (s *Intention) Apply(
|
|||
found := false
|
||||
for i, src := range upsertEntry.Sources {
|
||||
if src.SourceServiceName() == sn {
|
||||
upsertEntry.Sources[i] = args.Intention.ToSourceIntention()
|
||||
upsertEntry.Sources[i] = args.Intention.ToSourceIntention(false)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
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
|
||||
// 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
|
||||
// a look at the agent authorize endpoint (agent/agent_endpoint.go) since
|
||||
// the logic there is similar.
|
||||
|
@ -802,14 +804,30 @@ func (s *Intention) Check(
|
|||
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] {
|
||||
if auth, ok := uri.Authorize(ixn); ok {
|
||||
reply.Allowed = auth
|
||||
return nil
|
||||
if _, ok := uri.Authorize(ixn); ok {
|
||||
ixnMatch = ixn
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ixnMatch != nil {
|
||||
if len(ixnMatch.Permissions) == 0 {
|
||||
// This is an L4 intention.
|
||||
reply.Allowed = ixnMatch.Action == structs.IntentionActionAllow
|
||||
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
|
||||
// specifying the anonymous token token, which will get that behavior.
|
||||
// The default behavior if ACLs are disabled is to allow connections
|
||||
|
|
|
@ -343,6 +343,22 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
|||
|
||||
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 {
|
||||
req.Datacenter = "dc1"
|
||||
var ignored string
|
||||
|
@ -419,6 +435,10 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
|||
got := resp.Intentions[0]
|
||||
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.
|
||||
require.Empty(t, got.ID)
|
||||
require.True(t, got.CreatedAt.IsZero())
|
||||
|
@ -483,6 +503,10 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
|||
got := resp.Intentions[0]
|
||||
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.
|
||||
require.Empty(t, got.ID)
|
||||
require.True(t, got.CreatedAt.IsZero())
|
||||
|
@ -502,8 +526,15 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
|||
Intention: &structs.Intention{
|
||||
SourceName: "assay",
|
||||
DestinationName: "test",
|
||||
Action: structs.IntentionActionDeny,
|
||||
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]
|
||||
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.
|
||||
require.Empty(t, got.ID)
|
||||
require.True(t, got.CreatedAt.IsZero())
|
||||
|
@ -556,10 +598,17 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
|||
{
|
||||
Name: "assay",
|
||||
EnterpriseMeta: *defaultEntMeta,
|
||||
Action: structs.IntentionActionDeny,
|
||||
Description: "original-2",
|
||||
Precedence: 9,
|
||||
Type: structs.IntentionSourceConsul,
|
||||
Permissions: []*structs.IntentionPermission{
|
||||
{
|
||||
Action: structs.IntentionActionAllow,
|
||||
HTTP: &structs.IntentionHTTPPermission{
|
||||
PathExact: "/foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RaftIndex: entry.RaftIndex,
|
||||
|
@ -609,10 +658,17 @@ func TestIntentionApply_WithoutIDs(t *testing.T) {
|
|||
{
|
||||
Name: "assay",
|
||||
EnterpriseMeta: *defaultEntMeta,
|
||||
Action: structs.IntentionActionDeny,
|
||||
Description: "original-2",
|
||||
Precedence: 9,
|
||||
Type: structs.IntentionSourceConsul,
|
||||
Permissions: []*structs.IntentionPermission{
|
||||
{
|
||||
Action: structs.IntentionActionAllow,
|
||||
HTTP: &structs.IntentionHTTPPermission{
|
||||
PathExact: "/foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RaftIndex: entry.RaftIndex,
|
||||
|
|
|
@ -383,6 +383,16 @@ func (s *Server) replicateLegacyIntentionsOnce(ctx context.Context, lastFetchInd
|
|||
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.
|
||||
deletes, updates := diffIntentions(local, remote.Intentions)
|
||||
txnOpSets := batchLegacyIntentionUpdates(deletes, updates)
|
||||
|
|
|
@ -324,7 +324,7 @@ func insertConfigEntryWithTxn(tx *txn, idx uint64, conf structs.ConfigEntry) err
|
|||
func validateProposedConfigEntryInGraph(
|
||||
tx ReadTxn,
|
||||
kind, name string,
|
||||
next structs.ConfigEntry,
|
||||
proposedEntry structs.ConfigEntry,
|
||||
entMeta *structs.EnterpriseMeta,
|
||||
) error {
|
||||
validateAllChains := false
|
||||
|
@ -350,14 +350,11 @@ func validateProposedConfigEntryInGraph(
|
|||
return err
|
||||
}
|
||||
case structs.ServiceIntentions:
|
||||
// TODO(rb): should this validate protocols?
|
||||
return nil
|
||||
|
||||
default:
|
||||
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(
|
||||
|
@ -475,7 +472,7 @@ func (s *Store) discoveryChainSourcesTxn(tx ReadTxn, ws memdb.WatchSet, dc strin
|
|||
func validateProposedConfigEntryInServiceGraph(
|
||||
tx ReadTxn,
|
||||
kind, name string,
|
||||
next structs.ConfigEntry,
|
||||
proposedEntry structs.ConfigEntry,
|
||||
validateAllChains bool,
|
||||
entMeta *structs.EnterpriseMeta,
|
||||
) error {
|
||||
|
@ -484,6 +481,7 @@ func validateProposedConfigEntryInServiceGraph(
|
|||
var (
|
||||
checkChains = make(map[structs.ServiceID]struct{})
|
||||
checkIngress []*structs.IngressGatewayConfigEntry
|
||||
checkIntentions []*structs.ServiceIntentionsConfigEntry
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
for _, entry := range ingressEntries {
|
||||
ingress, ok := entry.(*structs.IngressGatewayConfigEntry)
|
||||
if !ok {
|
||||
return fmt.Errorf("type %T is not an ingress gateway config entry", entry)
|
||||
|
@ -515,17 +513,43 @@ func validateProposedConfigEntryInServiceGraph(
|
|||
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 {
|
||||
// Checking an ingress pointing to multiple chains.
|
||||
|
||||
// This is the case for deleting a config entry
|
||||
if next == nil {
|
||||
if proposedEntry == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ingress, ok := next.(*structs.IngressGatewayConfigEntry)
|
||||
ingress, ok := proposedEntry.(*structs.IngressGatewayConfigEntry)
|
||||
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)
|
||||
|
||||
|
@ -536,6 +560,28 @@ func validateProposedConfigEntryInServiceGraph(
|
|||
} else {
|
||||
// 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)
|
||||
checkChains[sid] = struct{}{}
|
||||
|
||||
|
@ -559,16 +605,20 @@ func validateProposedConfigEntryInServiceGraph(
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure if any ingress is affected that we fetch all of the chains needed
|
||||
// to fully validate that ingress.
|
||||
// Ensure if any ingress or intention is affected that we fetch all of the
|
||||
// chains needed to fully validate them.
|
||||
for _, ingress := range checkIngress {
|
||||
for _, svcID := range ingress.ListRelatedServices() {
|
||||
checkChains[svcID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, ixn := range checkIntentions {
|
||||
sn := ixn.DestinationServiceName()
|
||||
checkChains[sn.ToServiceID()] = struct{}{}
|
||||
}
|
||||
|
||||
overrides := map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||
structs.NewConfigEntryKindName(kind, name, entMeta): next,
|
||||
structs.NewConfigEntryKindName(kind, name, entMeta): proposedEntry,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
@ -108,15 +109,12 @@ func (e *ServiceRouterConfigEntry) Normalize() error {
|
|||
}
|
||||
|
||||
httpMatch := route.Match.HTTP
|
||||
if len(httpMatch.Methods) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for j := 0; j < len(httpMatch.Methods); j++ {
|
||||
httpMatch.Methods[j] = strings.ToUpper(httpMatch.Methods[j])
|
||||
}
|
||||
|
||||
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 {
|
||||
found := make(map[string]struct{})
|
||||
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 {
|
||||
return fmt.Errorf("Route[%d] Methods contains %q more than once", i, m)
|
||||
}
|
||||
|
@ -227,6 +228,23 @@ func (e *ServiceRouterConfigEntry) Validate() error {
|
|||
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 {
|
||||
return canReadDiscoveryChain(e, rule)
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intent
|
|||
SourceName: src.Name,
|
||||
SourceType: src.Type,
|
||||
Action: src.Action,
|
||||
Permissions: src.Permissions,
|
||||
Meta: meta,
|
||||
Precedence: src.Precedence,
|
||||
DestinationNS: e.NamespaceOrDefault(),
|
||||
|
@ -135,7 +136,27 @@ type SourceIntention struct {
|
|||
// Action is whether this is an allowlist or denylist intention.
|
||||
//
|
||||
// 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
|
||||
// larger numbers being applied first. This is a read-only field, on
|
||||
|
@ -182,6 +203,65 @@ type SourceIntention struct {
|
|||
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 {
|
||||
if m == nil {
|
||||
return nil
|
||||
|
@ -202,6 +282,13 @@ func (x *SourceIntention) Clone() *SourceIntention {
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -303,6 +390,16 @@ func (e *ServiceIntentionsConfigEntry) normalize(legacyWrite bool) error {
|
|||
src.LegacyCreateTime = 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
|
||||
|
@ -370,6 +467,20 @@ func (e *ServiceIntentionsConfigEntry) LegacyValidate() error {
|
|||
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 {
|
||||
if e.Name == "" {
|
||||
return fmt.Errorf("Name is required")
|
||||
|
@ -379,6 +490,8 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
destIsWild := e.HasWildcardDestination()
|
||||
|
||||
if legacyWrite {
|
||||
if len(e.Meta) > 0 {
|
||||
return fmt.Errorf("Meta must be omitted for legacy intention writes")
|
||||
|
@ -445,10 +558,20 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
|||
}
|
||||
}
|
||||
|
||||
switch src.Action {
|
||||
case IntentionActionAllow, IntentionActionDeny:
|
||||
default:
|
||||
return fmt.Errorf("Sources[%d].Action must be set to 'allow' or 'deny'", i)
|
||||
if legacyWrite || len(src.Permissions) == 0 {
|
||||
switch src.Action {
|
||||
case IntentionActionAllow, IntentionActionDeny:
|
||||
default:
|
||||
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 {
|
||||
|
@ -457,6 +580,94 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
|||
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()
|
||||
if _, exists := seenSources[serviceName]; exists {
|
||||
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)
|
||||
for _, ixn := range ixns {
|
||||
thisEntry := ixn.ToConfigEntry()
|
||||
thisEntry := ixn.ToConfigEntry(true)
|
||||
sn := thisEntry.DestinationServiceName()
|
||||
|
||||
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'`,
|
||||
},
|
||||
"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": {
|
||||
entry: &ServiceIntentionsConfigEntry{
|
||||
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 {
|
||||
tc := tc
|
||||
|
|
|
@ -973,6 +973,57 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
name = "bar"
|
||||
action = "allow"
|
||||
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 {
|
||||
|
@ -999,6 +1050,57 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
Sources {
|
||||
|
@ -1026,6 +1128,57 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
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",
|
||||
|
|
|
@ -55,7 +55,14 @@ type Intention struct {
|
|||
SourceType IntentionSourceType
|
||||
|
||||
// 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.
|
||||
// Deprecated: DefaultAddr is not used and may be removed in a future version.
|
||||
|
@ -77,10 +84,11 @@ type Intention struct {
|
|||
// or modified.
|
||||
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
|
||||
// one DC to another keeping the content Hash will allow us to detect
|
||||
// This is needed mainly for legacy replication purposes. When replicating
|
||||
// from one DC to another keeping the content Hash will allow us to detect
|
||||
// content changes more efficiently than checking every single field
|
||||
Hash []byte `bexpr:"-" json:",omitempty"`
|
||||
|
||||
|
@ -89,6 +97,12 @@ type Intention struct {
|
|||
|
||||
func (t *Intention) Clone() *Intention {
|
||||
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.Hash = nil
|
||||
return &t2
|
||||
|
@ -267,6 +281,11 @@ func (x *Intention) Validate() error {
|
|||
"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 {
|
||||
case IntentionSourceConsul:
|
||||
default:
|
||||
|
@ -365,11 +384,25 @@ func (x *Intention) countExact(ns, n string) int {
|
|||
|
||||
// String returns a human-friendly string for this intention.
|
||||
func (x *Intention) String() string {
|
||||
return fmt.Sprintf("%s %s/%s => %s/%s (ID: %s, Precedence: %d)",
|
||||
strings.ToUpper(string(x.Action)),
|
||||
var idPart string
|
||||
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.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.
|
||||
|
@ -397,21 +430,22 @@ func (x *Intention) DestinationServiceName() ServiceName {
|
|||
|
||||
// NOTE this is just used to manipulate user-provided data before an insert
|
||||
// The RPC execution will do Normalize + Validate for us.
|
||||
func (x *Intention) ToConfigEntry() *ServiceIntentionsConfigEntry {
|
||||
func (x *Intention) ToConfigEntry(legacy bool) *ServiceIntentionsConfigEntry {
|
||||
return &ServiceIntentionsConfigEntry{
|
||||
Kind: ServiceIntentions,
|
||||
Name: x.DestinationName,
|
||||
EnterpriseMeta: *x.DestinationEnterpriseMeta(),
|
||||
Sources: []*SourceIntention{x.ToSourceIntention()},
|
||||
Sources: []*SourceIntention{x.ToSourceIntention(legacy)},
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Intention) ToSourceIntention() *SourceIntention {
|
||||
return &SourceIntention{
|
||||
func (x *Intention) ToSourceIntention(legacy bool) *SourceIntention {
|
||||
src := &SourceIntention{
|
||||
Name: x.SourceName,
|
||||
EnterpriseMeta: *x.SourceEnterpriseMeta(),
|
||||
Action: x.Action,
|
||||
Precedence: 0, // Ignore, let it be computed.
|
||||
Permissions: nil, // explicitly not symmetric with the old APIs
|
||||
Precedence: 0, // Ignore, let it be computed.
|
||||
LegacyID: x.ID,
|
||||
Type: x.SourceType,
|
||||
Description: x.Description,
|
||||
|
@ -419,6 +453,10 @@ func (x *Intention) ToSourceIntention() *SourceIntention {
|
|||
LegacyCreateTime: nil, // Ignore
|
||||
LegacyUpdateTime: nil, // Ignore
|
||||
}
|
||||
if !legacy {
|
||||
src.Permissions = x.Permissions
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
// the zero'th point release of the last element of proxysupport.EnvoyVersions.
|
||||
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 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
envoycore "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||
envoytype "github.com/envoyproxy/go-control-plane/envoy/type"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/hashicorp/go-version"
|
||||
"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 (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
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"
|
||||
|
@ -14,7 +16,7 @@ import (
|
|||
)
|
||||
|
||||
func makeRBACNetworkFilter(intentions structs.Intentions, intentionDefaultAllow bool) (*envoylistener.Filter, error) {
|
||||
rules, err := makeRBACRules(intentions, intentionDefaultAllow)
|
||||
rules, err := makeRBACRules(intentions, intentionDefaultAllow, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -27,7 +29,7 @@ func makeRBACNetworkFilter(intentions structs.Intentions, intentionDefaultAllow
|
|||
}
|
||||
|
||||
func makeRBACHTTPFilter(intentions structs.Intentions, intentionDefaultAllow bool) (*envoyhttp.HttpFilter, error) {
|
||||
rules, err := makeRBACRules(intentions, intentionDefaultAllow)
|
||||
rules, err := makeRBACRules(intentions, intentionDefaultAllow, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -38,16 +40,238 @@ func makeRBACHTTPFilter(intentions structs.Intentions, intentionDefaultAllow boo
|
|||
return makeEnvoyHTTPFilter("envoy.filters.http.rbac", cfg)
|
||||
}
|
||||
|
||||
type rbacIntention struct {
|
||||
Source structs.ServiceName
|
||||
NotSources []structs.ServiceName
|
||||
Allow bool
|
||||
Precedence int
|
||||
Skip bool
|
||||
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 (r *rbacIntention) Simplify() {
|
||||
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 {
|
||||
Source structs.ServiceName
|
||||
NotSources []structs.ServiceName
|
||||
Action intentionAction
|
||||
Permissions []*rbacPermission
|
||||
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
|
||||
|
||||
ComputedPrincipal *envoyrbac.Principal
|
||||
}
|
||||
|
||||
func (r *rbacIntention) FlattenPrincipal() *envoyrbac.Principal {
|
||||
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 {
|
||||
|
@ -132,7 +356,7 @@ func simplifyNotSourceSlice(notSources []structs.ServiceName) []structs.ServiceN
|
|||
// <default> : DENY
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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?
|
||||
|
||||
// Omit any lower-precedence intentions that share the same source.
|
||||
intentions = removeSameSourceIntentions(intentions)
|
||||
|
||||
// First build up just the basic principal matches.
|
||||
rbacIxns := make([]*rbacIntention, 0, len(intentions))
|
||||
for _, ixn := range intentions {
|
||||
rbacIxns = append(rbacIxns, &rbacIntention{
|
||||
Source: ixn.SourceServiceName(),
|
||||
Allow: (ixn.Action == structs.IntentionActionAllow),
|
||||
Precedence: ixn.Precedence,
|
||||
})
|
||||
}
|
||||
rbacIxns := intentionListToIntermediateRBACForm(intentions, isHTTP)
|
||||
|
||||
// Normalize: if we are in default-deny then all intentions must be allows and vice versa
|
||||
intentionDefaultAction := intentionActionFromBool(intentionDefaultAllow)
|
||||
|
||||
var rbacAction envoyrbac.RBAC_Action
|
||||
if intentionDefaultAllow {
|
||||
|
@ -173,69 +388,46 @@ func makeRBACRules(intentions structs.Intentions, intentionDefaultAllow bool) (*
|
|||
rbacAction = envoyrbac.RBAC_ALLOW
|
||||
}
|
||||
|
||||
// First walk backwards and if we encounter an intention with an action
|
||||
// that is the same as the default intention action, add it to all
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
// Remove source and permissions precedence.
|
||||
rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction)
|
||||
|
||||
// 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{
|
||||
Action: rbacAction,
|
||||
Action: rbacAction,
|
||||
Policies: make(map[string]*envoyrbac.Policy),
|
||||
}
|
||||
if len(principals) > 0 {
|
||||
policy := &envoyrbac.Policy{
|
||||
Principals: principals,
|
||||
|
||||
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{
|
||||
Principals: []*envoyrbac.Principal{rbacIxn.ComputedPrincipal},
|
||||
Permissions: make([]*envoyrbac.Permission, 0, len(rbacIxn.Permissions)),
|
||||
}
|
||||
for _, perm := range rbacIxn.Permissions {
|
||||
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()},
|
||||
}
|
||||
rbac.Policies = map[string]*envoyrbac.Policy{
|
||||
"consul-intentions": policy,
|
||||
}
|
||||
}
|
||||
|
||||
if len(rbac.Policies) == 0 {
|
||||
rbac.Policies = nil
|
||||
}
|
||||
return rbac, nil
|
||||
}
|
||||
|
||||
|
@ -267,14 +459,6 @@ func removeSameSourceIntentions(intentions structs.Intentions) structs.Intention
|
|||
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
|
||||
// 'against' service name via wildcard rules.
|
||||
//
|
||||
|
@ -372,3 +556,142 @@ func anyPermission() *envoyrbac.Permission {
|
|||
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"
|
||||
)
|
||||
|
||||
func TestMakeRBACNetworkFilter(t *testing.T) {
|
||||
func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
|
||||
testIntention := func(t *testing.T, src, dst string, action structs.IntentionAction) *structs.Intention {
|
||||
t.Helper()
|
||||
ixn := structs.TestIntention(t)
|
||||
|
@ -25,6 +25,11 @@ func TestMakeRBACNetworkFilter(t *testing.T) {
|
|||
testSourceIntention := func(src string, action structs.IntentionAction) *structs.Intention {
|
||||
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 {
|
||||
sort.SliceStable(ixns, func(i, j int) bool {
|
||||
return ixns[j].Precedence < ixns[i].Precedence
|
||||
|
@ -97,17 +102,163 @@ func TestMakeRBACNetworkFilter(t *testing.T) {
|
|||
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 {
|
||||
tt := tt
|
||||
t.Run(name, func(t *testing.T) {
|
||||
filter, err := makeRBACNetworkFilter(tt.intentions, tt.intentionDefaultAllow)
|
||||
require.NoError(t, err)
|
||||
t.Run("network filter", func(t *testing.T) {
|
||||
filter, err := makeRBACNetworkFilter(tt.intentions, tt.intentionDefaultAllow)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotJSON := protoToJSON(t, filter)
|
||||
gotJSON := protoToJSON(t, filter)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
97
agent/xds/testdata/rbac/default-allow-kitchen-sink--httpfilter.golden
vendored
Normal file
97
agent/xds/testdata/rbac/default-allow-kitchen-sink--httpfilter.golden
vendored
Normal file
|
@ -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": {
|
||||
"action": "DENY",
|
||||
"policies": {
|
||||
"consul-intentions": {
|
||||
"consul-intentions-layer4": {
|
||||
"permissions": [
|
||||
{
|
||||
"any": true
|
||||
|
@ -17,7 +17,7 @@
|
|||
"safe_regex": {
|
||||
"google_re2": {
|
||||
},
|
||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@
|
|||
"safe_regex": {
|
||||
"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": {
|
||||
"authenticated": {
|
||||
|
@ -59,6 +72,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"not_id": {
|
||||
"authenticated": {
|
||||
"principal_name": {
|
||||
"safe_regex": {
|
||||
"google_re2": {
|
||||
},
|
||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
30
agent/xds/testdata/rbac/default-allow-one-deny--httpfilter.golden
vendored
Normal file
30
agent/xds/testdata/rbac/default-allow-one-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/web$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"rules": {
|
||||
"action": "DENY",
|
||||
"policies": {
|
||||
"consul-intentions": {
|
||||
"consul-intentions-layer4": {
|
||||
"permissions": [
|
||||
{
|
||||
"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": {
|
||||
"action": "DENY",
|
||||
"policies": {
|
||||
"consul-intentions": {
|
||||
"consul-intentions-layer4": {
|
||||
"permissions": [
|
||||
{
|
||||
"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$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow.golden
vendored
Normal file
31
agent/xds/testdata/rbac/default-allow-two-path-deny-and-path-allow.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"
|
||||
}
|
||||
}
|
48
agent/xds/testdata/rbac/default-deny-allow-deny--httpfilter.golden
vendored
Normal file
48
agent/xds/testdata/rbac/default-deny-allow-deny--httpfilter.golden
vendored
Normal file
|
@ -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": {
|
||||
"rules": {
|
||||
"policies": {
|
||||
"consul-intentions": {
|
||||
"consul-intentions-layer4": {
|
||||
"permissions": [
|
||||
{
|
||||
"any": true
|
||||
|
|
96
agent/xds/testdata/rbac/default-deny-kitchen-sink--httpfilter.golden
vendored
Normal file
96
agent/xds/testdata/rbac/default-deny-kitchen-sink--httpfilter.golden
vendored
Normal file
|
@ -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": {
|
||||
"rules": {
|
||||
"policies": {
|
||||
"consul-intentions": {
|
||||
"consul-intentions-layer4": {
|
||||
"permissions": [
|
||||
{
|
||||
"any": true
|
||||
|
@ -16,7 +16,7 @@
|
|||
"safe_regex": {
|
||||
"google_re2": {
|
||||
},
|
||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/web$"
|
||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
|||
"safe_regex": {
|
||||
"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": {
|
||||
"authenticated": {
|
||||
|
@ -58,6 +71,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"not_id": {
|
||||
"authenticated": {
|
||||
"principal_name": {
|
||||
"safe_regex": {
|
||||
"google_re2": {
|
||||
},
|
||||
"regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/cron$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
29
agent/xds/testdata/rbac/default-deny-mixed-precedence--httpfilter.golden
vendored
Normal file
29
agent/xds/testdata/rbac/default-deny-mixed-precedence--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/web$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"config": {
|
||||
"rules": {
|
||||
"policies": {
|
||||
"consul-intentions": {
|
||||
"consul-intentions-layer4": {
|
||||
"permissions": [
|
||||
{
|
||||
"any": true
|
||||
|
|
29
agent/xds/testdata/rbac/default-deny-one-allow--httpfilter.golden
vendored
Normal file
29
agent/xds/testdata/rbac/default-deny-one-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/web$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"config": {
|
||||
"rules": {
|
||||
"policies": {
|
||||
"consul-intentions": {
|
||||
"consul-intentions-layer4": {
|
||||
"permissions": [
|
||||
{
|
||||
"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": {
|
||||
"rules": {
|
||||
"policies": {
|
||||
"consul-intentions": {
|
||||
"consul-intentions-layer4": {
|
||||
"permissions": [
|
||||
{
|
||||
"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$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
agent/xds/testdata/rbac/default-deny-two-path-deny-and-path-allow.golden
vendored
Normal file
8
agent/xds/testdata/rbac/default-deny-two-path-deny-and-path-allow.golden
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "envoy.filters.network.rbac",
|
||||
"config": {
|
||||
"rules": {
|
||||
},
|
||||
"stat_prefix": "connect_authz"
|
||||
}
|
||||
}
|
|
@ -17,8 +17,9 @@ type ServiceIntentionsConfigEntry struct {
|
|||
|
||||
type SourceIntention struct {
|
||||
Name string
|
||||
Namespace string `json:",omitempty"`
|
||||
Action IntentionAction
|
||||
Namespace string `json:",omitempty"`
|
||||
Action IntentionAction `json:",omitempty"`
|
||||
Permissions []*IntentionPermission `json:",omitempty"`
|
||||
Precedence int
|
||||
Type IntentionSourceType
|
||||
Description string `json:",omitempty"`
|
||||
|
@ -52,3 +53,28 @@ func (e *ServiceIntentionsConfigEntry) GetCreateIndex() uint64 {
|
|||
func (e *ServiceIntentionsConfigEntry) GetModifyIndex() uint64 {
|
||||
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
|
||||
|
||||
|
|
|
@ -34,7 +34,14 @@ type Intention struct {
|
|||
SourceType IntentionSourceType
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
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)",
|
||||
i.SourceString(),
|
||||
i.DestinationString(),
|
||||
i.Action)
|
||||
detail)
|
||||
}
|
||||
|
||||
// SourceString returns the namespace/name format for the source, or
|
||||
|
|
|
@ -1917,8 +1917,8 @@ func TestParseConfigEntry(t *testing.T) {
|
|||
kind = "service-intentions"
|
||||
name = "web"
|
||||
meta {
|
||||
"foo" = "bar"
|
||||
"gir" = "zim"
|
||||
"foo" = "bar"
|
||||
"gir" = "zim"
|
||||
}
|
||||
sources = [
|
||||
{
|
||||
|
@ -1931,6 +1931,57 @@ func TestParseConfigEntry(t *testing.T) {
|
|||
name = "bar"
|
||||
action = "allow"
|
||||
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 {
|
||||
|
@ -1957,6 +2008,57 @@ func TestParseConfigEntry(t *testing.T) {
|
|||
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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
Sources {
|
||||
|
@ -1969,7 +2071,7 @@ func TestParseConfigEntry(t *testing.T) {
|
|||
{
|
||||
"kind": "service-intentions",
|
||||
"name": "web",
|
||||
"meta" : {
|
||||
"meta": {
|
||||
"foo": "bar",
|
||||
"gir": "zim"
|
||||
},
|
||||
|
@ -1985,6 +2087,60 @@ func TestParseConfigEntry(t *testing.T) {
|
|||
"action": "allow",
|
||||
"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": "*",
|
||||
"action": "deny",
|
||||
|
@ -2013,6 +2169,60 @@ func TestParseConfigEntry(t *testing.T) {
|
|||
"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",
|
||||
|
@ -2040,6 +2250,57 @@ func TestParseConfigEntry(t *testing.T) {
|
|||
Action: "allow",
|
||||
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: "*",
|
||||
Action: "deny",
|
||||
|
|
|
@ -177,24 +177,36 @@ func (c *cmd) ixnsFromArgs(args []string) ([]*api.Intention, error) {
|
|||
func (c *cmd) ixnsFromFiles(args []string) ([]*api.Intention, error) {
|
||||
var result []*api.Intention
|
||||
for _, path := range args {
|
||||
f, err := os.Open(path)
|
||||
ixn, err := c.ixnFromFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ixn api.Intention
|
||||
err = json.NewDecoder(f).Decode(&ixn)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, &ixn)
|
||||
result = append(result, ixn)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *cmd) ixnFromFile(path string) (*api.Intention, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var ixn api.Intention
|
||||
if err := json.NewDecoder(f).Decode(&ixn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 &ixn, nil
|
||||
}
|
||||
|
||||
// ixnAction returns the api.IntentionAction based on the flag set.
|
||||
func (c *cmd) ixnAction() api.IntentionAction {
|
||||
if c.flagAllow {
|
||||
|
|
|
@ -174,6 +174,47 @@ func TestIntentionCreate_File(t *testing.T) {
|
|||
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) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -85,6 +85,28 @@ func TestIntentionDelete(t *testing.T) {
|
|||
}, nil)
|
||||
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("one arg", func(t *testing.T) {
|
||||
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.
|
||||
{
|
||||
ixns, _, err := client.Connect().Intentions(nil)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package get
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -60,7 +61,9 @@ func (c *cmd) Run(args []string) int {
|
|||
data := []string{
|
||||
fmt.Sprintf("Source:\x1f%s", ixn.SourceString()),
|
||||
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 != "" {
|
||||
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})}))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,28 @@ func TestGetFromArgs(t *testing.T) {
|
|||
}, nil)
|
||||
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("one arg", func(t *testing.T) {
|
||||
ixn, err := GetFromArgs(client, []string{id0})
|
||||
|
@ -36,6 +58,7 @@ func TestGetFromArgs(t *testing.T) {
|
|||
require.Equal(t, "a", ixn.SourceName)
|
||||
require.Equal(t, "b", ixn.DestinationName)
|
||||
require.Equal(t, api.IntentionActionAllow, ixn.Action)
|
||||
require.Empty(t, ixn.Permissions)
|
||||
})
|
||||
t.Run("two args", func(t *testing.T) {
|
||||
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, "b", ixn.DestinationName)
|
||||
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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
10
test/integration/connect/envoy/case-l7-intentions/setup.sh
Normal file
10
test/integration/connect/envoy/case-l7-intentions/setup.sh
Normal file
|
@ -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}"
|
||||
}
|
||||
|
||||
# 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 {
|
||||
SERVICE=$1
|
||||
ADMIN_PORT=$2
|
||||
|
|
|
@ -38,6 +38,7 @@ func TestEnvoy(t *testing.T) {
|
|||
"case-ingress-gateway-simple",
|
||||
"case-ingress-gateway-tls",
|
||||
"case-ingress-mesh-gateways-resolver",
|
||||
"case-l7-intentions",
|
||||
"case-multidc-rsa-ca",
|
||||
"case-prometheus",
|
||||
"case-statsd-udp",
|
||||
|
|
|
@ -6,11 +6,34 @@ unset CDPATH
|
|||
|
||||
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=(
|
||||
1.15.0
|
||||
1.14.4
|
||||
1.13.4
|
||||
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
|
||||
)
|
||||
|
||||
for v in "${versions[@]}"; do
|
||||
|
|
Loading…
Reference in a new issue