mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
459 lines
17 KiB
459 lines
17 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
package catalogv2 |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"net/http" |
|
"strconv" |
|
"testing" |
|
"time" |
|
|
|
"github.com/stretchr/testify/require" |
|
"google.golang.org/grpc/codes" |
|
"google.golang.org/grpc/status" |
|
|
|
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" |
|
"github.com/hashicorp/consul/proto-public/pbresource" |
|
"github.com/hashicorp/consul/sdk/testutil/retry" |
|
"github.com/hashicorp/consul/test-integ/topoutil" |
|
libassert "github.com/hashicorp/consul/test/integration/consul-container/libs/assert" |
|
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils" |
|
"github.com/hashicorp/consul/testing/deployer/sprawl/sprawltest" |
|
"github.com/hashicorp/consul/testing/deployer/topology" |
|
) |
|
|
|
type testCase struct { |
|
permissions []*permission |
|
result []*testResult |
|
} |
|
|
|
type permission struct { |
|
allow bool |
|
excludeSource bool |
|
includeSourceTenancy bool |
|
excludeSourceTenancy bool |
|
destRules []*destRules |
|
} |
|
|
|
type destRules struct { |
|
values *ruleValues |
|
excludes []*ruleValues |
|
} |
|
|
|
type ruleValues struct { |
|
portNames []string |
|
path string |
|
pathPref string |
|
pathReg string |
|
headers []string |
|
methods []string |
|
} |
|
|
|
type testResult struct { |
|
fail bool |
|
port string |
|
path string |
|
headers map[string]string |
|
} |
|
|
|
func newTrafficPermissions(p *permission, srcTenancy *pbresource.Tenancy) *pbauth.TrafficPermissions { |
|
sources := []*pbauth.Source{{ |
|
IdentityName: "static-client", |
|
Namespace: srcTenancy.Namespace, |
|
Partition: srcTenancy.Partition, |
|
}} |
|
destinationRules := []*pbauth.DestinationRule{} |
|
if p != nil { |
|
srcId := "static-client" |
|
if p.includeSourceTenancy { |
|
srcId = "" |
|
} |
|
if p.excludeSource { |
|
sources = []*pbauth.Source{{ |
|
IdentityName: srcId, |
|
Namespace: srcTenancy.Namespace, |
|
Partition: srcTenancy.Partition, |
|
Exclude: []*pbauth.ExcludeSource{{ |
|
IdentityName: "static-client", |
|
Namespace: srcTenancy.Namespace, |
|
Partition: srcTenancy.Partition, |
|
}}, |
|
}} |
|
} else { |
|
sources = []*pbauth.Source{{ |
|
IdentityName: srcId, |
|
Namespace: srcTenancy.Namespace, |
|
Partition: srcTenancy.Partition, |
|
}} |
|
} |
|
for _, dr := range p.destRules { |
|
destRule := &pbauth.DestinationRule{} |
|
if dr.values != nil { |
|
destRule.PathExact = dr.values.path |
|
destRule.PathPrefix = dr.values.pathPref |
|
destRule.PathRegex = dr.values.pathReg |
|
destRule.Methods = dr.values.methods |
|
destRule.PortNames = dr.values.portNames |
|
destRule.Headers = []*pbauth.DestinationRuleHeader{} |
|
for _, h := range dr.values.headers { |
|
destRule.Headers = append(destRule.Headers, &pbauth.DestinationRuleHeader{ |
|
Name: h, |
|
Present: true, |
|
}) |
|
} |
|
} |
|
var excludePermissions []*pbauth.ExcludePermissionRule |
|
for _, e := range dr.excludes { |
|
eRule := &pbauth.ExcludePermissionRule{ |
|
PathExact: e.path, |
|
PathPrefix: e.pathPref, |
|
PathRegex: e.pathReg, |
|
Methods: e.methods, |
|
PortNames: e.portNames, |
|
} |
|
eRule.Headers = []*pbauth.DestinationRuleHeader{} |
|
for _, h := range e.headers { |
|
eRule.Headers = append(eRule.Headers, &pbauth.DestinationRuleHeader{ |
|
Name: h, |
|
Present: true, |
|
}) |
|
} |
|
excludePermissions = append(excludePermissions, eRule) |
|
} |
|
destRule.Exclude = excludePermissions |
|
destinationRules = append(destinationRules, destRule) |
|
} |
|
} |
|
action := pbauth.Action_ACTION_ALLOW |
|
if !p.allow { |
|
action = pbauth.Action_ACTION_DENY |
|
} |
|
return &pbauth.TrafficPermissions{ |
|
Destination: &pbauth.Destination{ |
|
IdentityName: "static-server", |
|
}, |
|
Action: action, |
|
Permissions: []*pbauth.Permission{{ |
|
Sources: sources, |
|
DestinationRules: destinationRules, |
|
}}, |
|
} |
|
|
|
} |
|
|
|
// This tests runs a gauntlet of traffic permissions updates and validates that the request status codes match the intended rules |
|
func TestL7TrafficPermissions(t *testing.T) { |
|
testcases := map[string]testCase{ |
|
// L4 permissions |
|
"basic": {permissions: []*permission{{allow: true}}, result: []*testResult{{fail: false}}}, |
|
"client-exclude": {permissions: []*permission{{allow: true, includeSourceTenancy: true, excludeSource: true}}, result: []*testResult{{fail: true}}}, |
|
"allow-all-client-in-tenancy": {permissions: []*permission{{allow: true, includeSourceTenancy: true}}, result: []*testResult{{fail: false}}}, |
|
"only-one-port": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{portNames: []string{"http"}}}}}}, result: []*testResult{{fail: true, port: "http2"}}}, |
|
"exclude-port": {permissions: []*permission{{allow: true, destRules: []*destRules{{excludes: []*ruleValues{{portNames: []string{"http"}}}}}}}, result: []*testResult{{fail: true, port: "http"}}}, |
|
// L7 permissions |
|
"methods": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{methods: []string{"POST", "PUT", "PATCH", "DELETE", "CONNECT", "HEAD", "OPTIONS", "TRACE"}, pathPref: "/"}}}}}, |
|
// fortio fetch2 is configured to GET |
|
result: []*testResult{{fail: true}}}, |
|
"headers": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{headers: []string{"a", "b"}, pathPref: "/"}}}}}, |
|
result: []*testResult{{fail: true}, {fail: true, headers: map[string]string{"a": "1"}}, {fail: false, headers: map[string]string{"a": "1", "b": "2"}}}}, |
|
"path-prefix-all": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/", methods: []string{"GET"}}}}}}, result: []*testResult{{fail: false}}}, |
|
"method-exclude": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/"}, excludes: []*ruleValues{{methods: []string{"GET"}}}}}}}, |
|
// fortio fetch2 is configured to GET |
|
result: []*testResult{{fail: true}}}, |
|
"exclude-paths-and-headers": {permissions: []*permission{{allow: true, destRules: []*destRules{ |
|
{ |
|
values: &ruleValues{pathPref: "/f", headers: []string{"a"}}, |
|
excludes: []*ruleValues{{headers: []string{"b"}, path: "/foobar"}}, |
|
}}}}, |
|
result: []*testResult{ |
|
{fail: false, path: "foobar", headers: map[string]string{"a": "1"}}, |
|
{fail: false, path: "foo", headers: map[string]string{"a": "1", "b": "2"}}, |
|
{fail: true, path: "foobar", headers: map[string]string{"a": "1", "b": "2"}}, |
|
{fail: false, path: "foo", headers: map[string]string{"a": "1"}}, |
|
{fail: true, path: "foo", headers: map[string]string{"b": "2"}}, |
|
{fail: true, path: "baz", headers: map[string]string{"a": "1"}}, |
|
}}, |
|
"exclude-paths-or-headers": {permissions: []*permission{{allow: true, destRules: []*destRules{ |
|
{values: &ruleValues{pathPref: "/f", headers: []string{"a"}}, excludes: []*ruleValues{{headers: []string{"b"}}, {path: "/foobar"}}}}}}, |
|
result: []*testResult{ |
|
{fail: true, path: "foobar", headers: map[string]string{"a": "1"}}, |
|
{fail: true, path: "foo", headers: map[string]string{"a": "1", "b": "2"}}, |
|
{fail: true, path: "foobar", headers: map[string]string{"a": "1", "b": "2"}}, |
|
{fail: false, path: "foo", headers: map[string]string{"a": "1"}}, |
|
{fail: false, path: "foo", headers: map[string]string{"a": "1"}}, |
|
{fail: true, path: "baz", port: "http", headers: map[string]string{"a": "1"}}, |
|
}}, |
|
"path-or-header": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/bar"}}, {values: &ruleValues{headers: []string{"b"}}}}}}, |
|
result: []*testResult{ |
|
{fail: false, path: "bar"}, |
|
{fail: false, path: "foo", headers: map[string]string{"a": "1", "b": "2"}}, |
|
{fail: false, path: "bar", headers: map[string]string{"b": "2"}}, |
|
{fail: true, path: "foo", headers: map[string]string{"a": "1"}}, |
|
}}, |
|
"path-and-header": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/bar", headers: []string{"b"}}}}}}, |
|
result: []*testResult{ |
|
{fail: true, path: "bar"}, |
|
{fail: true, path: "foo", headers: map[string]string{"a": "1", "b": "2"}}, |
|
{fail: false, path: "bar", headers: map[string]string{"b": "2"}}, |
|
{fail: true, path: "foo", headers: map[string]string{"a": "1"}}, |
|
}}, |
|
"path-regex-exclude": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/"}, excludes: []*ruleValues{{pathReg: ".*dns.*"}}}}}}, |
|
result: []*testResult{{fail: true, path: "fortio/rest/dns"}, {fail: false, path: "fortio/rest/status"}}}, |
|
"header-include-exclude-by-port": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/", headers: []string{"experiment1", "experiment2"}}, excludes: []*ruleValues{{portNames: []string{"http2"}, headers: []string{"experiment1"}}}}}}}, |
|
result: []*testResult{{fail: true, port: "http2", headers: map[string]string{"experiment1": "a", "experiment2": "b"}}, |
|
{fail: false, port: "http", headers: map[string]string{"experiment1": "a", "experiment2": "b"}}, |
|
{fail: true, port: "http2", headers: map[string]string{"experiment2": "b"}}, |
|
{fail: true, port: "http", headers: map[string]string{"experiment3": "c"}}, |
|
}}, |
|
"two-tp-or": {permissions: []*permission{{allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/bar"}}}}, {allow: true, destRules: []*destRules{{values: &ruleValues{headers: []string{"b"}}}}}}, |
|
result: []*testResult{ |
|
{fail: false, path: "bar"}, |
|
{fail: false, path: "foo", headers: map[string]string{"a": "1", "b": "2"}}, |
|
{fail: false, path: "bar", headers: map[string]string{"b": "2"}}, |
|
{fail: true, path: "foo", headers: map[string]string{"a": "1"}}, |
|
}}, |
|
} |
|
if utils.IsEnterprise() { |
|
// DENY and ALLOW permissions |
|
testcases["deny-cancel-allow"] = testCase{permissions: []*permission{{allow: true}, {allow: false}}, result: []*testResult{{fail: true}}} |
|
testcases["l4-deny-l7-allow"] = testCase{permissions: []*permission{{allow: false}, {allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/"}}}}}, result: []*testResult{{fail: true}, {fail: true, path: "test"}}} |
|
testcases["l7-deny-l4-allow"] = testCase{permissions: []*permission{{allow: true}, {allow: true, destRules: []*destRules{{values: &ruleValues{pathPref: "/"}}}}, {allow: false, destRules: []*destRules{{values: &ruleValues{pathPref: "/foo"}}}}}, |
|
result: []*testResult{{fail: false}, {fail: false, path: "test"}, {fail: true, path: "foo-bar"}}} |
|
} |
|
|
|
tenancies := []*pbresource.Tenancy{ |
|
{ |
|
Partition: "default", |
|
Namespace: "default", |
|
}, |
|
} |
|
if utils.IsEnterprise() { |
|
tenancies = append(tenancies, &pbresource.Tenancy{ |
|
Partition: "ap1", |
|
Namespace: "ns1", |
|
}) |
|
} |
|
cfg := testL7TrafficPermissionsCreator{tenancies}.NewConfig(t) |
|
targetImage := utils.TargetImages() |
|
imageName := targetImage.Consul |
|
if utils.IsEnterprise() { |
|
imageName = targetImage.ConsulEnterprise |
|
} |
|
t.Log("running with target image: " + imageName) |
|
|
|
sp := sprawltest.Launch(t, cfg) |
|
|
|
asserter := topoutil.NewAsserter(sp) |
|
|
|
topo := sp.Topology() |
|
cluster := topo.Clusters["dc1"] |
|
ships := topo.ComputeRelationships() |
|
|
|
clientV2 := sp.ResourceServiceClientForCluster(cluster.Name) |
|
|
|
// Make sure services exist |
|
for _, tenancy := range tenancies { |
|
for _, name := range []string{ |
|
"static-server", |
|
"static-client", |
|
} { |
|
libassert.CatalogV2ServiceHasEndpointCount(t, clientV2, name, tenancy, len(tenancies)) |
|
} |
|
} |
|
var initialTrafficPerms []*pbresource.Resource |
|
for testName, tc := range testcases { |
|
// Delete old TP and write new one for a new test case |
|
mustDeleteTestResources(t, clientV2, initialTrafficPerms) |
|
initialTrafficPerms = []*pbresource.Resource{} |
|
for _, st := range tenancies { |
|
for _, dt := range tenancies { |
|
for i, p := range tc.permissions { |
|
newTrafficPerms := sprawltest.MustSetResourceData(t, &pbresource.Resource{ |
|
Id: &pbresource.ID{ |
|
Type: pbauth.TrafficPermissionsType, |
|
Name: "static-server-perms" + strconv.Itoa(i) + "-" + st.Namespace + "-" + st.Partition, |
|
Tenancy: dt, |
|
}, |
|
}, newTrafficPermissions(p, st)) |
|
mustWriteTestResource(t, clientV2, newTrafficPerms) |
|
initialTrafficPerms = append(initialTrafficPerms, newTrafficPerms) |
|
} |
|
} |
|
} |
|
t.Log(initialTrafficPerms) |
|
// Wait for the resource updates to go through and Envoy to be ready |
|
time.Sleep(1 * time.Second) |
|
// Check the default server workload envoy config for RBAC filters matching testcase criteria |
|
serverWorkload := cluster.WorkloadsByID(topology.ID{ |
|
Partition: "default", |
|
Namespace: "default", |
|
Name: "static-server", |
|
}) |
|
asserter.AssertEnvoyHTTPrbacFiltersContainIntentions(t, serverWorkload[0]) |
|
// Check relationships |
|
for _, ship := range ships { |
|
t.Run("case: "+testName+":"+ship.Destination.PortName+":("+ship.Caller.ID.Partition+"/"+ship.Caller.ID.Namespace+ |
|
")("+ship.Destination.ID.Partition+"/"+ship.Destination.ID.Namespace+")", func(t *testing.T) { |
|
var ( |
|
wrk = ship.Caller |
|
dest = ship.Destination |
|
) |
|
for _, res := range tc.result { |
|
if res.port != "" && res.port != ship.Destination.PortName { |
|
continue |
|
} |
|
dest.ID.Name = "static-server" |
|
destClusterPrefix := clusterPrefix(dest.PortName, dest.ID, dest.Cluster) |
|
asserter.DestinationEndpointStatus(t, wrk, destClusterPrefix+".", "HEALTHY", len(tenancies)) |
|
status := http.StatusForbidden |
|
if res.fail == false { |
|
status = http.StatusOK |
|
} |
|
t.Log("Test request:"+res.path, res.headers, status) |
|
asserter.FortioFetch2ServiceStatusCodes(t, wrk, dest, res.path, res.headers, []int{status}) |
|
} |
|
}) |
|
} |
|
} |
|
} |
|
|
|
func mustWriteTestResource(t *testing.T, client pbresource.ResourceServiceClient, res *pbresource.Resource) { |
|
retryer := &retry.Timer{Timeout: time.Minute, Wait: time.Second} |
|
rsp, err := client.Write(context.Background(), &pbresource.WriteRequest{Resource: res}) |
|
require.NoError(t, err) |
|
retry.RunWith(retryer, t, func(r *retry.R) { |
|
readRsp, err := client.Read(context.Background(), &pbresource.ReadRequest{Id: rsp.Resource.Id}) |
|
require.NoError(r, err, "error reading %s", rsp.Resource.Id.Name) |
|
require.NotNil(r, readRsp) |
|
}) |
|
|
|
} |
|
|
|
func mustDeleteTestResources(t *testing.T, client pbresource.ResourceServiceClient, resources []*pbresource.Resource) { |
|
if len(resources) == 0 { |
|
return |
|
} |
|
retryer := &retry.Timer{Timeout: time.Minute, Wait: time.Second} |
|
for _, res := range resources { |
|
retry.RunWith(retryer, t, func(r *retry.R) { |
|
_, err := client.Delete(context.Background(), &pbresource.DeleteRequest{Id: res.Id}) |
|
if status.Code(err) == codes.NotFound { |
|
return |
|
} |
|
if err != nil && status.Code(err) != codes.Aborted { |
|
r.Stop(fmt.Errorf("failed to delete the resource: %w", err)) |
|
return |
|
} |
|
require.NoError(r, err) |
|
}) |
|
} |
|
} |
|
|
|
type testL7TrafficPermissionsCreator struct { |
|
tenancies []*pbresource.Tenancy |
|
} |
|
|
|
func (c testL7TrafficPermissionsCreator) NewConfig(t *testing.T) *topology.Config { |
|
const clusterName = "dc1" |
|
|
|
servers := topoutil.NewTopologyServerSet(clusterName+"-server", 1, []string{clusterName, "wan"}, nil) |
|
|
|
cluster := &topology.Cluster{ |
|
Enterprise: utils.IsEnterprise(), |
|
Name: clusterName, |
|
Nodes: servers, |
|
} |
|
|
|
lastNode := 0 |
|
nodeName := func() string { |
|
lastNode++ |
|
return fmt.Sprintf("%s-box%d", clusterName, lastNode) |
|
} |
|
|
|
for _, st := range c.tenancies { |
|
for _, dt := range c.tenancies { |
|
c.topologyConfigAddNodes(cluster, nodeName, st, dt) |
|
|
|
} |
|
} |
|
|
|
return &topology.Config{ |
|
Images: utils.TargetImages(), |
|
Networks: []*topology.Network{ |
|
{Name: clusterName}, |
|
{Name: "wan", Type: "wan"}, |
|
}, |
|
Clusters: []*topology.Cluster{ |
|
cluster, |
|
}, |
|
} |
|
} |
|
|
|
func (c testL7TrafficPermissionsCreator) topologyConfigAddNodes( |
|
cluster *topology.Cluster, |
|
nodeName func() string, |
|
sourceTenancy *pbresource.Tenancy, |
|
destinationTenancy *pbresource.Tenancy, |
|
) { |
|
clusterName := cluster.Name |
|
|
|
newID := func(name string, tenancy *pbresource.Tenancy) topology.ID { |
|
return topology.ID{ |
|
Partition: tenancy.Partition, |
|
Namespace: tenancy.Namespace, |
|
Name: name, |
|
} |
|
} |
|
|
|
serverNode := &topology.Node{ |
|
Kind: topology.NodeKindDataplane, |
|
Version: topology.NodeVersionV2, |
|
Partition: destinationTenancy.Partition, |
|
Name: nodeName(), |
|
Workloads: []*topology.Workload{ |
|
topoutil.NewFortioWorkloadWithDefaults( |
|
clusterName, |
|
newID("static-server", destinationTenancy), |
|
topology.NodeVersionV2, |
|
nil, |
|
), |
|
}, |
|
} |
|
|
|
clientNode := &topology.Node{ |
|
Kind: topology.NodeKindDataplane, |
|
Version: topology.NodeVersionV2, |
|
Partition: sourceTenancy.Partition, |
|
Name: nodeName(), |
|
Workloads: []*topology.Workload{ |
|
topoutil.NewFortioWorkloadWithDefaults( |
|
clusterName, |
|
newID("static-client", sourceTenancy), |
|
topology.NodeVersionV2, |
|
func(wrk *topology.Workload) { |
|
wrk.Destinations = append(wrk.Destinations, &topology.Destination{ |
|
ID: newID("static-server", destinationTenancy), |
|
PortName: "http", |
|
LocalAddress: "0.0.0.0", // needed for an assertion |
|
LocalPort: 5000, |
|
}, |
|
&topology.Destination{ |
|
ID: newID("static-server", destinationTenancy), |
|
PortName: "http2", |
|
LocalAddress: "0.0.0.0", // needed for an assertion |
|
LocalPort: 5001, |
|
}, |
|
) |
|
wrk.WorkloadIdentity = "static-client" |
|
}, |
|
), |
|
}, |
|
} |
|
|
|
cluster.Nodes = append(cluster.Nodes, |
|
clientNode, |
|
serverNode, |
|
) |
|
}
|
|
|