Integration test for ext-authz Envoy extension (#17980)

This commit is contained in:
Chris Thain 2023-07-04 08:09:17 -07:00 committed by GitHub
parent 80394278b8
commit 4f0bdd35e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 185 additions and 6 deletions

View File

@ -46,11 +46,11 @@ type Agent interface {
type Config struct { type Config struct {
// NodeName is set for the consul agent name and container name // NodeName is set for the consul agent name and container name
// Equivalent to the -node command-line flag. // Equivalent to the -node command-line flag.
// If empty, a randam name will be generated // If empty, a random name will be generated
NodeName string NodeName string
// NodeID is used to configure node_id in agent config file // NodeID is used to configure node_id in agent config file
// Equivalent to the -node-id command-line flag. // Equivalent to the -node-id command-line flag.
// If empty, a randam name will be generated // If empty, a random name will be generated
NodeID string NodeID string
// ExternalDataDir is data directory to copy consul data from, if set. // ExternalDataDir is data directory to copy consul data from, if set.
@ -83,10 +83,7 @@ func (c *Config) DockerImage() string {
func (c Config) Clone() Config { func (c Config) Clone() Config {
c2 := c c2 := c
if c.Cmd != nil { if c.Cmd != nil {
c2.Cmd = make([]string, len(c.Cmd)) copy(c2.Cmd, c.Cmd)
for i, v := range c.Cmd {
c2.Cmd[i] = v
}
} }
return c2 return c2
} }

View File

@ -0,0 +1,170 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package envoyextensions
import (
"context"
"fmt"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil/retry"
libassert "github.com/hashicorp/consul/test/integration/consul-container/libs/assert"
libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster"
libservice "github.com/hashicorp/consul/test/integration/consul-container/libs/service"
"github.com/hashicorp/consul/test/integration/consul-container/libs/topology"
"github.com/hashicorp/go-cleanhttp"
)
// TestExtAuthzLocal Summary
// This test makes sure two services in the same datacenter have connectivity.
// A simulated client (a direct HTTP call) talks to it's upstream proxy through the mesh.
// The upstream (static-server) is configured with a `builtin/ext-authz` extension that
// calls an OPA external authorization service to authorize incoming HTTP requests.
// The external authorization service is deployed as a container on the local network.
//
// Steps:
// - Create a single agent cluster.
// - Create the example static-server and sidecar containers, then register them both with Consul
// - Create an example static-client sidecar, then register both the service and sidecar with Consul
// - Create an OPA external authorization container on the local network, this doesn't need to be registered with Consul.
// - Configure the static-server service with a `builtin/ext-authz` EnvoyExtension targeting the OPA ext-authz service.
// - Make sure a call to the client sidecar local bind port returns the expected response from the upstream static-server:
// - A call to `/allow` returns 200 OK.
// - A call to any other endpoint returns 403 Forbidden.
func TestExtAuthzLocal(t *testing.T) {
t.Parallel()
cluster, _, _ := topology.NewCluster(t, &topology.ClusterConfig{
NumServers: 1,
NumClients: 1,
ApplyDefaultProxySettings: true,
BuildOpts: &libcluster.BuildOptions{
Datacenter: "dc1",
InjectAutoEncryption: true,
InjectGossipEncryption: true,
},
})
createLocalAuthzService(t, cluster)
clientService := createServices(t, cluster)
_, port := clientService.GetAddr()
_, adminPort := clientService.GetAdminAddr()
libassert.AssertUpstreamEndpointStatus(t, adminPort, "static-server.default", "HEALTHY", 1)
libassert.GetEnvoyListenerTCPFilters(t, adminPort)
libassert.AssertContainerState(t, clientService, "running")
libassert.AssertFortioName(t, fmt.Sprintf("http://localhost:%d", port), "static-server", "")
// Wire up the ext-authz envoy extension for the static-server
consul := cluster.APIClient(0)
defaults := api.ServiceConfigEntry{
Kind: api.ServiceDefaults,
Name: "static-server",
Protocol: "http",
EnvoyExtensions: []api.EnvoyExtension{{
Name: "builtin/ext-authz",
Arguments: map[string]any{
"Config": map[string]any{
"GrpcService": map[string]any{
"Target": map[string]any{"URI": "127.0.0.1:9191"},
},
},
},
}},
}
consul.ConfigEntries().Set(&defaults, nil)
// Make requests to the static-server. We expect that all requests are rejected with 403 Forbidden
// unless they are to the /allow path.
baseURL := fmt.Sprintf("http://localhost:%d", port)
doRequest(t, baseURL, http.StatusForbidden)
doRequest(t, baseURL+"/allow", http.StatusOK)
}
func createServices(t *testing.T, cluster *libcluster.Cluster) libservice.Service {
node := cluster.Agents[0]
client := node.GetClient()
// Create a service and proxy instance
serviceOpts := &libservice.ServiceOpts{
Name: libservice.StaticServerServiceName,
ID: "static-server",
HTTPPort: 8080,
GRPCPort: 8079,
}
// Create a service and proxy instance
_, _, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOpts)
require.NoError(t, err)
libassert.CatalogServiceExists(t, client, "static-server-sidecar-proxy", nil)
libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName, nil)
// Create a client proxy instance with the server as an upstream
clientConnectProxy, err := libservice.CreateAndRegisterStaticClientSidecar(node, "", false, false)
require.NoError(t, err)
libassert.CatalogServiceExists(t, client, "static-client-sidecar-proxy", nil)
return clientConnectProxy
}
func createLocalAuthzService(t *testing.T, cluster *libcluster.Cluster) {
node := cluster.Agents[0]
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
req := testcontainers.ContainerRequest{
Image: "openpolicyagent/opa:0.53.0-envoy-3",
AutoRemove: true,
Name: "ext-authz",
Env: make(map[string]string),
Cmd: []string{
"run",
"--server",
"--addr=localhost:8181",
"--diagnostic-addr=0.0.0.0:8282",
"--set=plugins.envoy_ext_authz_grpc.addr=:9191",
"--set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow",
"--set=decision_logs.console=true",
"--set=status.console=true",
"--ignore=.*",
"/testdata/policies/policy.rego",
},
Mounts: []testcontainers.ContainerMount{{
Source: testcontainers.DockerBindMountSource{
HostPath: fmt.Sprintf("%s/testdata", cwd),
},
Target: "/testdata",
ReadOnly: true,
}},
}
ctx := context.Background()
exposedPorts := []string{}
_, err = libcluster.LaunchContainerOnNode(ctx, node, req, exposedPorts)
if err != nil {
t.Fatal(err)
}
}
func doRequest(t *testing.T, url string, expStatus int) {
retry.RunWith(&retry.Timer{Timeout: 5 * time.Second, Wait: time.Second}, t, func(r *retry.R) {
resp, err := cleanhttp.DefaultClient().Get(url)
require.NoError(r, err)
require.Equal(r, expStatus, resp.StatusCode)
})
}

View File

@ -0,0 +1,12 @@
package envoy.authz
import future.keywords
import input.attributes.request.http as http_request
default allow := false
allow if {
http_request.method == "GET"
glob.match("/allow", ["/"], http_request.path)
}