Skip to content

Commit

Permalink
Add builtin/ext-authz Envoy Extension (#17495)
Browse files Browse the repository at this point in the history
  • Loading branch information
cthain authored May 26, 2023
1 parent 2740d12 commit 516eb4f
Show file tree
Hide file tree
Showing 32 changed files with 4,031 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/17495.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
xds: Add a built-in Envoy extension that inserts External Authorization (ext_authz) network and HTTP filters.
```
133 changes: 133 additions & 0 deletions agent/envoyextensions/builtin/ext-authz/ext_authz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package extauthz

import (
"fmt"

envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
"github.com/mitchellh/mapstructure"

"github.com/hashicorp/consul/api"
ext_cmn "github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/hashicorp/go-multierror"
)

type extAuthz struct {
ext_cmn.BasicExtensionAdapter

// ProxyType identifies the type of Envoy proxy that this extension applies to.
// The extension will only be configured for proxies that match this type and
// will be ignored for all other proxy types.
ProxyType api.ServiceKind
// InsertOptions controls how the extension inserts the filter.
InsertOptions ext_cmn.InsertOptions
// Config holds the extension configuration.
Config extAuthzConfig
}

var _ ext_cmn.BasicExtension = (*extAuthz)(nil)

func Constructor(ext api.EnvoyExtension) (ext_cmn.EnvoyExtender, error) {
auth, err := newExtAuthz(ext)
if err != nil {
return nil, err
}
return &ext_cmn.BasicEnvoyExtender{
Extension: auth,
}, nil
}

// CanApply indicates if the ext-authz extension can be applied to the given extension runtime configuration.
func (a *extAuthz) CanApply(config *ext_cmn.RuntimeConfig) bool {
return config.Kind == api.ServiceKindConnectProxy
}

// PatchClusters modifies the cluster resources for the ext-authz extension.
//
// If the extension is configured to target an ext-authz service running on the local host network
// this func will insert a cluster for calling that service. It does nothing if the extension is
// configured to target an upstream service because the existing cluster for the upstream will be
// used directly by the filter.
func (a *extAuthz) PatchClusters(cfg *ext_cmn.RuntimeConfig, c ext_cmn.ClusterMap) (ext_cmn.ClusterMap, error) {
cluster, err := a.Config.toEnvoyCluster(cfg)
if err != nil {
return c, err
}
if cluster != nil {
c[cluster.Name] = cluster
}
return c, nil
}

// PatchFilters inserts an ext-authz filter into the list of network filters or the filter chain of the HTTP connection manager.
func (a *extAuthz) PatchFilters(cfg *ext_cmn.RuntimeConfig, filters []*envoy_listener_v3.Filter, isInboundListener bool) ([]*envoy_listener_v3.Filter, error) {
// The ext_authz extension only patches filters for inbound listeners.
if !isInboundListener {
return filters, nil
}

switch cfg.Protocol {
case "grpc", "http2", "http":
extAuthzFilter, err := a.Config.toEnvoyHttpFilter(cfg)
if err != nil {
return filters, err
}
return ext_cmn.InsertHTTPFilter(filters, extAuthzFilter, a.InsertOptions)
case "tcp":
fallthrough
default:
extAuthzFilter, err := a.Config.toEnvoyNetworkFilter(cfg)
if err != nil {
return filters, err
}
return ext_cmn.InsertNetworkFilter(filters, extAuthzFilter, a.InsertOptions)
}
}

func newExtAuthz(ext api.EnvoyExtension) (*extAuthz, error) {
auth := &extAuthz{}
if ext.Name != api.BuiltinExtAuthzExtension {
return auth, fmt.Errorf("expected extension name %q but got %q", api.BuiltinExtAuthzExtension, ext.Name)
}
if err := auth.fromArguments(ext.Arguments); err != nil {
return auth, err
}
// The filter's failure mode is always configured based on whether or not the extension is required.
auth.Config.failureModeAllow = !ext.Required
return auth, nil
}

func (a *extAuthz) fromArguments(args map[string]any) error {
if err := mapstructure.Decode(args, a); err != nil {
return err
}
a.normalize()
return a.validate()
}

func (a *extAuthz) normalize() {
if a.ProxyType == "" {
a.ProxyType = api.ServiceKindConnectProxy
}
if a.InsertOptions.Location == "" {
a.InsertOptions.Location = ext_cmn.InsertFirst
}
a.Config.normalize()
}

func (a *extAuthz) validate() error {
var resultErr error
if a.ProxyType != api.ServiceKindConnectProxy {
resultErr = multierror.Append(resultErr, fmt.Errorf("unsupported ProxyType %q, only %q is supported",
a.ProxyType,
api.ServiceKindConnectProxy))
}

if err := a.Config.validate(); err != nil {
resultErr = multierror.Append(resultErr, err)
}

return resultErr
}
174 changes: 174 additions & 0 deletions agent/envoyextensions/builtin/ext-authz/ext_authz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package extauthz

import (
"testing"

"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/stretchr/testify/require"
)

func TestConstructor(t *testing.T) {
t.Parallel()
cases := map[string]struct {
extName string
args map[string]any
errMsg string
}{
"invalid name": {
extName: "invalid",
errMsg: `expected extension name "builtin/ext-authz"`,
},
"invalid proxy type": {
args: map[string]any{"ProxyType": "invalid"},
errMsg: `unsupported ProxyType`,
},
"no service type": {
args: map[string]any{"ProxyType": "connect-proxy"},
errMsg: `exactly one of GrpcService or HttpService must be set`,
},
"both service types": {
args: map[string]any{
"ProxyType": "connect-proxy",
"Config": map[string]any{
"GrpcService": map[string]any{
"Target": map[string]any{
"URI": "localhost:9191",
},
},
"HttpService": map[string]any{
"Target": map[string]any{
"URI": "localhost:9191",
},
},
},
},
errMsg: `exactly one of GrpcService or HttpService must be set`,
},
"non-loopback address hostname": {
args: map[string]any{
"ProxyType": "connect-proxy",
"Config": map[string]any{
"GrpcService": map[string]any{
"Target": map[string]any{
"URI": "foo.bar.com:9191",
},
},
},
},
errMsg: `invalid host for Target.URI "foo.bar.com:9191": expected 'localhost' or '127.0.0.1'`,
},
"non-loopback address": {
args: map[string]any{
"ProxyType": "connect-proxy",
"Config": map[string]any{
"GrpcService": map[string]any{
"Target": map[string]any{
"URI": "10.0.0.1:9191",
},
},
},
},
errMsg: `invalid host for Target.URI "10.0.0.1:9191": expected 'localhost' or '127.0.0.1'`,
},
"no uri or service target": {
args: map[string]any{
"ProxyType": "connect-proxy",
"Config": map[string]any{
"HttpService": map[string]any{
"Target": map[string]any{
"Timeout": "1s",
},
},
},
},
errMsg: `exactly one of Target.Service or Target.URI must be set`,
},
"uri and service target": {
args: map[string]any{
"ProxyType": "connect-proxy",
"Config": map[string]any{
"GrpcService": map[string]any{
"Target": map[string]any{
"URI": "10.0.0.1:9191",
"Service": map[string]any{
"Name": "test-service",
},
},
},
},
},
errMsg: `exactly one of Target.Service or Target.URI must be set`,
},
"invalid status on error": {
args: map[string]any{
"ProxyType": "connect-proxy",
"Config": map[string]any{
"StatusOnError": 1,
"GrpcService": map[string]any{
"Target": map[string]any{
"URI": "10.0.0.1:9191",
"Service": map[string]any{
"Name": "test-service",
},
},
},
},
},
errMsg: `failed to validate Config.StatusOnError`,
},
"valid grpc service": {
args: map[string]any{
"ProxyType": "connect-proxy",
"Config": map[string]any{
"GrpcService": map[string]any{
"Target": map[string]any{
"URI": "localhost:9191",
},
},
},
},
},
"valid http service": {
args: map[string]any{
"ProxyType": "connect-proxy",
"Config": map[string]any{
"HttpService": map[string]any{
"Target": map[string]any{
"URI": "127.0.0.1:9191",
},
},
},
},
},
}
for name, c := range cases {
c := c
t.Run(name, func(t *testing.T) {
extName := api.BuiltinExtAuthzExtension
if c.extName != "" {
extName = c.extName
}
ext, err := newExtAuthz(api.EnvoyExtension{Name: extName, Arguments: c.args})
if c.errMsg == "" {
require.NoError(t, err)

httpFilter, err := ext.Config.toEnvoyHttpFilter(&extensioncommon.RuntimeConfig{})
require.NoError(t, err)
require.NotNil(t, httpFilter)

if ext.Config.isGRPC() {
netFilter, err := ext.Config.toEnvoyNetworkFilter(&extensioncommon.RuntimeConfig{})
require.NoError(t, err)
require.NotNil(t, netFilter)
}
} else {
require.Error(t, err)
require.Contains(t, err.Error(), c.errMsg)
}
})
}
}
Loading

0 comments on commit 516eb4f

Please sign in to comment.