Skip to content

Commit

Permalink
configure TypedPerFilterConfig with direct responses
Browse files Browse the repository at this point in the history
Signed-off-by: shadi-altarsha <shadi.altarsha@reddit.com>
  • Loading branch information
shadi-altarsha committed Nov 14, 2023
1 parent dba50bc commit 27c73bc
Show file tree
Hide file tree
Showing 6 changed files with 518 additions and 0 deletions.
2 changes: 2 additions & 0 deletions changelogs/unreleased/5962-shadialtarsha-small.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
## Configure TypedPerFilterConfig with direct responses
Fixes a bug to disable external auth on direct responses.
9 changes: 9 additions & 0 deletions internal/envoy/v3/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ func buildRoute(dagRoute *dag.Route, vhostName string, secure bool) *envoy_route
// redirect routes to *both* the insecure and secure vhosts.
route.Action = UpgradeHTTPS()
case dagRoute.DirectResponse != nil:
route.TypedPerFilterConfig = map[string]*anypb.Any{}

// Apply per-route authorization policy modifications.
if dagRoute.AuthDisabled {
route.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = routeAuthzDisabled()
} else if len(dagRoute.AuthContext) > 0 {
route.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = routeAuthzContext(dagRoute.AuthContext)
}

route.Action = routeDirectResponse(dagRoute.DirectResponse)
case dagRoute.Redirect != nil:
// TODO request/response headers?
Expand Down
75 changes: 75 additions & 0 deletions internal/envoy/v3/route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,81 @@ func TestRouteDirectResponse(t *testing.T) {
}
}

func TestBuildRouteWithDirectResponse(t *testing.T) {
tests := map[string]struct {
dagRoute *dag.Route
vhostName string
secure bool
want *envoy_route_v3.Route
}{
"direct-response-with-auth": {
dagRoute: &dag.Route{
DirectResponse: &dag.DirectResponse{
StatusCode: 500,
Body: "Internal Server Error",
},
AuthContext: map[string]string{
"PrincipalName": "user",
},
PathMatchCondition: &dag.PrefixMatchCondition{
Prefix: "/foo",
PrefixMatchType: dag.PrefixMatchString,
},
},
vhostName: "example",
secure: true,
want: &envoy_route_v3.Route{
TypedPerFilterConfig: map[string]*anypb.Any{
"envoy.filters.http.ext_authz": routeAuthzContext(map[string]string{
"PrincipalName": "user",
}),
},
Action: routeDirectResponse(&dag.DirectResponse{
StatusCode: 500,
Body: "Internal Server Error",
}),
Match: &envoy_route_v3.RouteMatch{
PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{
Prefix: "/foo",
},
},
},
},
"direct-response-auth-disabled": {
dagRoute: &dag.Route{
DirectResponse: &dag.DirectResponse{
StatusCode: 403,
},
AuthDisabled: true,
PathMatchCondition: &dag.PrefixMatchCondition{
Prefix: "/foo",
PrefixMatchType: dag.PrefixMatchString,
},
},
vhostName: "example",
secure: false,
want: &envoy_route_v3.Route{
TypedPerFilterConfig: map[string]*anypb.Any{
"envoy.filters.http.ext_authz": routeAuthzDisabled(),
},
Action: routeDirectResponse(&dag.DirectResponse{StatusCode: 403}),
Match: &envoy_route_v3.RouteMatch{
PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{
Prefix: "/foo",
},
},
},
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := buildRoute(tc.dagRoute, tc.vhostName, tc.secure)
protobuf.ExpectEqual(t, tc.want, got)
})
}
}

func TestWeightedClusters(t *testing.T) {
tests := map[string]struct {
route *dag.Route
Expand Down
228 changes: 228 additions & 0 deletions test/e2e/httpproxy/external_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,232 @@ func testExternalAuth(namespace string) {
assert.Equal(t, "default", body.RequestHeaders.Get("Auth-Context-Target"))
assert.Equal(t, "externalauth.projectcontour.io", body.RequestHeaders.Get("Auth-Context-Hostname"))
})

Specify("external auth can be configured on a direct response route", func() {
t := f.T()

f.Fixtures.Echo.Deploy(namespace, "echo")
f.Certs.CreateSelfSignedCert(namespace, "echo", "echo", "externalauth.projectcontour.io")

f.Certs.CreateSelfSignedCert(namespace, "testserver-cert", "testserver-cert", "testserver")

// auth testserver
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "testserver",
Labels: map[string]string{
"app.kubernetes.io/name": "testserver",
},
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app.kubernetes.io/name": "testserver"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app.kubernetes.io/name": "testserver"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "testserver",
Image: "ghcr.io/projectcontour/contour-authserver:v4",
ImagePullPolicy: corev1.PullIfNotPresent,
Command: []string{
"/contour-authserver",
},
Args: []string{
"testserver",
"--address=:9443",
"--tls-ca-path=/tls/ca.crt",
"--tls-cert-path=/tls/tls.crt",
"--tls-key-path=/tls/tls.key",
},
Ports: []corev1.ContainerPort{
{
Name: "auth",
ContainerPort: 9443,
Protocol: corev1.ProtocolTCP,
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "tls",
MountPath: "/tls",
ReadOnly: true,
},
},
},
},
Volumes: []corev1.Volume{
{
Name: "tls",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "testserver-cert",
},
},
},
},
},
},
},
}
require.NoError(t, f.Client.Create(context.TODO(), deployment))

svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "testserver",
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/name": "testserver",
},
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "auth",
Protocol: corev1.ProtocolTCP,
Port: 9443,
TargetPort: intstr.FromInt(9443),
},
},
Selector: map[string]string{
"app.kubernetes.io/name": "testserver",
},
Type: corev1.ServiceTypeClusterIP,
},
}
require.NoError(t, f.Client.Create(context.TODO(), svc))

extSvc := &contourv1alpha1.ExtensionService{
ObjectMeta: metav1.ObjectMeta{
Name: "testserver",
Namespace: namespace,
},
Spec: contourv1alpha1.ExtensionServiceSpec{
Services: []contourv1alpha1.ExtensionServiceTarget{
{
Name: "testserver",
Port: 9443,
},
},
},
}
require.NoError(t, f.Client.Create(context.TODO(), extSvc))

p := &contourv1.HTTPProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "external-auth",
},
Spec: contourv1.HTTPProxySpec{
VirtualHost: &contourv1.VirtualHost{
Fqdn: "externalauth.projectcontour.io",
TLS: &contourv1.TLS{
SecretName: "echo",
},
Authorization: &contourv1.AuthorizationServer{
ResponseTimeout: "500ms",
ExtensionServiceRef: contourv1.ExtensionServiceReference{
Name: extSvc.Name,
Namespace: extSvc.Namespace,
},
AuthPolicy: &contourv1.AuthorizationPolicy{
Context: map[string]string{
"hostname": "externalauth.projectcontour.io",
},
},
},
},
Routes: []contourv1.Route{
{
Conditions: []contourv1.MatchCondition{
{
Prefix: "/first",
},
},
AuthPolicy: &contourv1.AuthorizationPolicy{
Context: map[string]string{
"target": "first",
},
},
Services: []contourv1.Service{
{
Name: "echo",
Port: 80,
},
},
},
{
Conditions: []contourv1.MatchCondition{
{
Prefix: "/second",
},
},
AuthPolicy: &contourv1.AuthorizationPolicy{
Disabled: true,
},
DirectResponsePolicy: &contourv1.HTTPDirectResponsePolicy{
StatusCode: 200,
Body: "ok",
},
},
{
AuthPolicy: &contourv1.AuthorizationPolicy{
Context: map[string]string{
"target": "second",
},
},
Services: []contourv1.Service{
{
Name: "echo",
Port: 80,
},
},
},
},
},
}
f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid)

// By default, requests to /first should not be authorized.
res, ok := f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{
Host: p.Spec.VirtualHost.Fqdn,
Path: "/first",
Condition: e2e.HasStatusCode(401),
})
require.NotNil(t, res, "request never succeeded")
require.Truef(t, ok, "expected 401 response code, got %d", res.StatusCode)

// The `testserver` authorization server will accept any request with
// "allow" in the path, so this request should succeed. We can tell that
// the authorization server processed it by inspecting the context headers
// that it injects.
res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{
Host: p.Spec.VirtualHost.Fqdn,
Path: "/first/allow",
Condition: e2e.HasStatusCode(200),
})
require.NotNil(t, res, "request never succeeded")
require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode)

body := f.GetEchoResponseBody(res.Body)
assert.Equal(t, "first", body.RequestHeaders.Get("Auth-Context-Target"))
assert.Equal(t, "externalauth.projectcontour.io", body.RequestHeaders.Get("Auth-Context-Hostname"))

// THe /second route disables authorization so this request should succeed.
res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{
Host: p.Spec.VirtualHost.Fqdn,
Path: "/second",
Condition: e2e.HasStatusCode(200),
})
require.NotNil(t, res, "request never succeeded")
require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode)

assert.Equal(t, "ok", string(res.Body))
assert.Empty(t, res.Headers["Auth-Context-Target"])
assert.Empty(t, res.Headers["Auth-Context-Hostname"])
})
}
Loading

0 comments on commit 27c73bc

Please sign in to comment.