Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration test for ext-authz Envoy extension #17980

Merged
merged 4 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions test/integration/consul-container/libs/cluster/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ type Agent interface {
type Config struct {
// NodeName is set for the consul agent name and container name
// 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
// NodeID is used to configure node_id in agent config file
// 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

// ExternalDataDir is data directory to copy consul data from, if set.
Expand Down Expand Up @@ -83,10 +83,7 @@ func (c *Config) DockerImage() string {
func (c Config) Clone() Config {
c2 := c
if c.Cmd != nil {
c2.Cmd = make([]string, len(c.Cmd))
for i, v := range c.Cmd {
c2.Cmd[i] = v
}
copy(c2.Cmd, c.Cmd)
}
return c2
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
}
Original file line number Diff line number Diff line change
@@ -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)
}