-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
builtin/ext-authz
Envoy Extension (#17495)
- Loading branch information
Showing
32 changed files
with
4,031 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
174
agent/envoyextensions/builtin/ext-authz/ext_authz_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.