mirror of https://github.com/status-im/consul.git
Add the plumbing for APIGW JWT work (#18609)
* Add the plumbing for APIGW JWT work * Remove unneeded import * Add deep equal function for HTTPMatch * Added plumbing for status conditions * Remove unneeded comment * Fix comments * Add calls in xds listener for apigateway to setup listener jwt auth
This commit is contained in:
parent
d45c3c2755
commit
9876923e23
|
@ -63,6 +63,8 @@ func (r *apiGatewayReconciler) Reconcile(ctx context.Context, req controller.Req
|
|||
return reconcileEntry(r.fsm.State(), r.logger, ctx, req, r.reconcileTCPRoute, r.cleanupRoute)
|
||||
case structs.InlineCertificate:
|
||||
return r.enqueueCertificateReferencedGateways(r.fsm.State(), ctx, req)
|
||||
case structs.JWTProvider:
|
||||
return r.enqueueJWTProviderReferencedGatewaysAndHTTPRoutes(r.fsm.State(), ctx, req)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -233,6 +235,7 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
|
|||
logger.Warn("error retrieving bound api gateway", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
meta := newGatewayMeta(gateway, bound)
|
||||
|
||||
certificateErrors, err := meta.checkCertificates(store)
|
||||
|
@ -241,6 +244,12 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
|
|||
return err
|
||||
}
|
||||
|
||||
jwtErrors, err := meta.checkJWTProviders(store)
|
||||
if err != nil {
|
||||
logger.Warn("error checking gateway JWT Providers", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// set each listener as having valid certs, then overwrite that status condition
|
||||
// if there are any certificate errors
|
||||
meta.eachListener(func(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener) error {
|
||||
|
@ -260,7 +269,12 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
|
|||
|
||||
if len(certificateErrors) > 0 {
|
||||
updater.SetCondition(invalidCertificates())
|
||||
} else {
|
||||
}
|
||||
if len(jwtErrors) > 0 {
|
||||
updater.SetCondition(invalidJWTProviders())
|
||||
}
|
||||
|
||||
if len(certificateErrors) == 0 && len(jwtErrors) == 0 {
|
||||
updater.SetCondition(gatewayAccepted())
|
||||
}
|
||||
|
||||
|
@ -463,6 +477,13 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
|
|||
updater.SetCondition(routeNoUpstreams())
|
||||
}
|
||||
|
||||
if httpRoute, ok := route.(*structs.HTTPRouteConfigEntry); ok {
|
||||
err := validateJWTForRoute(store, updater, httpRoute)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// the route is valid, attempt to bind it to all gateways
|
||||
r.logger.Trace("binding routes to gateway")
|
||||
modifiedGateways, boundRefs, bindErrors := bindRoutesToGateways(route, meta...)
|
||||
|
@ -536,6 +557,11 @@ func NewAPIGatewayController(fsm *fsm.FSM, publisher state.EventPublisher, updat
|
|||
&stream.SubscribeRequest{
|
||||
Topic: state.EventTopicInlineCertificate,
|
||||
Subject: stream.SubjectWildcard,
|
||||
},
|
||||
).Subscribe(
|
||||
&stream.SubscribeRequest{
|
||||
Topic: state.EventTopicJWTProvider,
|
||||
Subject: stream.SubjectWildcard,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -897,6 +923,31 @@ func invalidCertificates() structs.Condition {
|
|||
)
|
||||
}
|
||||
|
||||
// invalidJWTProvider returns a condition used when a gateway listener references
|
||||
// a JWTProvider that does not exist. It takes a ref used to scope the condition
|
||||
// to a given APIGateway listener.
|
||||
func invalidJWTProvider(ref structs.ResourceReference, err error) structs.Condition {
|
||||
return structs.NewGatewayCondition(
|
||||
api.GatewayConditionResolvedRefs,
|
||||
api.ConditionStatusFalse,
|
||||
api.GatewayListenerReasonInvalidJWTProviderRef,
|
||||
err.Error(),
|
||||
ref,
|
||||
)
|
||||
}
|
||||
|
||||
// invalidJWTProviders is used to set the overall condition of the APIGateway
|
||||
// to invalid due to missing JWT providers that it references.
|
||||
func invalidJWTProviders() structs.Condition {
|
||||
return structs.NewGatewayCondition(
|
||||
api.GatewayConditionAccepted,
|
||||
api.ConditionStatusFalse,
|
||||
api.GatewayReasonInvalidJWTProviders,
|
||||
"gateway references invalid JWT Providers",
|
||||
structs.ResourceReference{},
|
||||
)
|
||||
}
|
||||
|
||||
// gatewayListenerNoConflicts marks an APIGateway listener as having no conflicts within its
|
||||
// bound routes
|
||||
func gatewayListenerNoConflicts(ref structs.ResourceReference) structs.Condition {
|
||||
|
@ -944,6 +995,18 @@ func gatewayNotFound(ref structs.ResourceReference) structs.Condition {
|
|||
)
|
||||
}
|
||||
|
||||
// jwtProviderNotFound marks a Route as having failed to bind to a referenced APIGateway due to
|
||||
// one or more of the referenced JWT providers not existing (or having not been reconciled yet)
|
||||
func jwtProviderNotFound(ref structs.ResourceReference, err error) structs.Condition {
|
||||
return structs.NewRouteCondition(
|
||||
api.RouteConditionBound,
|
||||
api.ConditionStatusFalse,
|
||||
api.RouteReasonGatewayNotFound,
|
||||
err.Error(),
|
||||
ref,
|
||||
)
|
||||
}
|
||||
|
||||
// routeUnbound marks the route as having failed to bind to the referenced APIGateway
|
||||
func routeUnbound(ref structs.ResourceReference, err error) structs.Condition {
|
||||
return structs.NewRouteCondition(
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !consulent
|
||||
// +build !consulent
|
||||
|
||||
package gateways
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/consul/agent/consul/controller"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
)
|
||||
|
||||
func (r *apiGatewayReconciler) enqueueJWTProviderReferencedGatewaysAndHTTPRoutes(_ *state.Store, _ context.Context, _ controller.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *gatewayMeta) checkJWTProviders(_ *state.Store) (map[structs.ResourceReference]error, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func validateJWTForRoute(_ *state.Store, _ *structs.StatusUpdater, _ *structs.HTTPRouteConfigEntry) error {
|
||||
return nil
|
||||
}
|
|
@ -54,6 +54,11 @@ func (h *handlerAPIGateway) initialize(ctx context.Context) (ConfigSnapshot, err
|
|||
return snap, err
|
||||
}
|
||||
|
||||
err = watchJWTProviders(ctx, h)
|
||||
if err != nil {
|
||||
return snap, err
|
||||
}
|
||||
|
||||
snap.APIGateway.Listeners = make(map[string]structs.APIGatewayListener)
|
||||
snap.APIGateway.BoundListeners = make(map[string]structs.BoundAPIGatewayListener)
|
||||
snap.APIGateway.HTTPRoutes = watch.NewMap[structs.ResourceReference, *structs.HTTPRouteConfigEntry]()
|
||||
|
@ -97,27 +102,33 @@ func (h *handlerAPIGateway) handleUpdate(ctx context.Context, u UpdateEvent, sna
|
|||
return fmt.Errorf("error filling agent cache: %v", u.Err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case u.CorrelationID == rootsWatchID:
|
||||
switch u.CorrelationID {
|
||||
case rootsWatchID:
|
||||
// Handle change in the CA roots
|
||||
if err := h.handleRootCAUpdate(u, snap); err != nil {
|
||||
return err
|
||||
}
|
||||
case u.CorrelationID == apiGatewayConfigWatchID || u.CorrelationID == boundGatewayConfigWatchID:
|
||||
case apiGatewayConfigWatchID, boundGatewayConfigWatchID:
|
||||
// Handle change in the api-gateway or bound-api-gateway config entry
|
||||
if err := h.handleGatewayConfigUpdate(ctx, u, snap, u.CorrelationID); err != nil {
|
||||
return err
|
||||
}
|
||||
case u.CorrelationID == inlineCertificateConfigWatchID:
|
||||
case inlineCertificateConfigWatchID:
|
||||
// Handle change in an attached inline-certificate config entry
|
||||
if err := h.handleInlineCertConfigUpdate(ctx, u, snap); err != nil {
|
||||
return err
|
||||
}
|
||||
case u.CorrelationID == routeConfigWatchID:
|
||||
case routeConfigWatchID:
|
||||
// Handle change in an attached http-route or tcp-route config entry
|
||||
if err := h.handleRouteConfigUpdate(ctx, u, snap); err != nil {
|
||||
return err
|
||||
}
|
||||
case jwtProviderID:
|
||||
err := setJWTProvider(u, snap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
if err := (*handlerUpstreams)(h).handleUpdateUpstreams(ctx, u, snap); err != nil {
|
||||
return err
|
||||
|
|
|
@ -343,6 +343,38 @@ type HTTPMatch struct {
|
|||
Query []HTTPQueryMatch
|
||||
}
|
||||
|
||||
func (m HTTPMatch) DeepEqual(other HTTPMatch) bool {
|
||||
if m.Method != other.Method {
|
||||
return false
|
||||
}
|
||||
|
||||
if m.Path != other.Path {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(m.Headers) != len(other.Headers) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(m.Query) != len(other.Query) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(m.Headers); i++ {
|
||||
if m.Headers[i] != other.Headers[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(m.Query); i++ {
|
||||
if m.Query[i] != other.Query[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// HTTPMatchMethod specifies which type of HTTP verb should
|
||||
// be used for matching a given request.
|
||||
type HTTPMatchMethod string
|
||||
|
|
|
@ -437,3 +437,476 @@ func TestHTTPRoute(t *testing.T) {
|
|||
}
|
||||
testConfigEntryNormalizeAndValidate(t, cases)
|
||||
}
|
||||
|
||||
func TestHTTPMatch_DeepEqual(t *testing.T) {
|
||||
type fields struct {
|
||||
Headers []HTTPHeaderMatch
|
||||
Method HTTPMatchMethod
|
||||
Path HTTPPathMatch
|
||||
Query []HTTPQueryMatch
|
||||
}
|
||||
type args struct {
|
||||
other HTTPMatch
|
||||
}
|
||||
tests := map[string]struct {
|
||||
match HTTPMatch
|
||||
other HTTPMatch
|
||||
want bool
|
||||
}{
|
||||
"all fields equal": {
|
||||
match: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
other: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
"differing number of header matches": {
|
||||
match: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
other: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
"differing header matches": {
|
||||
match: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h4",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
other: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
"different path matching": {
|
||||
match: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/zoidberg",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
other: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
"differing methods": {
|
||||
match: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodConnect,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
other: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
"differing number of query matches": {
|
||||
match: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
other: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
"different query matches": {
|
||||
match: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "another",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
other: HTTPMatch{
|
||||
Headers: []HTTPHeaderMatch{
|
||||
{
|
||||
Match: HTTPHeaderMatchExact,
|
||||
Name: "h1",
|
||||
Value: "a",
|
||||
},
|
||||
{
|
||||
Match: HTTPHeaderMatchPrefix,
|
||||
Name: "h2",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
Method: HTTPMatchMethodGet,
|
||||
Path: HTTPPathMatch{
|
||||
Match: HTTPPathMatchType(HTTPHeaderMatchPrefix),
|
||||
Value: "/bender",
|
||||
},
|
||||
Query: []HTTPQueryMatch{
|
||||
{
|
||||
Match: HTTPQueryMatchExact,
|
||||
Name: "q",
|
||||
Value: "nibbler",
|
||||
},
|
||||
{
|
||||
Match: HTTPQueryMatchPresent,
|
||||
Name: "ship",
|
||||
Value: "planet express",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
name := name
|
||||
tt := tt
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tt.match.DeepEqual(tt.other); got != tt.want {
|
||||
t.Errorf("HTTPMatch.DeepEqual() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,6 @@ type perRouteFilterBuilder struct {
|
|||
route *structs.HTTPRouteConfigEntry
|
||||
}
|
||||
|
||||
func (p perRouteFilterBuilder) buildFilter(match *envoy_route_v3.RouteMatch) (map[string]*anypb.Any, error) {
|
||||
func (p perRouteFilterBuilder) buildTypedPerFilterConfig(match *envoy_route_v3.RouteMatch, routeAction *envoy_route_v3.Route_Route) (map[string]*anypb.Any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -8,7 +8,10 @@ import (
|
|||
|
||||
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
envoy_http_jwt_authn_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3"
|
||||
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||
|
||||
"github.com/hashicorp/consul/agent/xds/naming"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
@ -137,6 +140,45 @@ func (s *ResourceGenerator) makeAPIGatewayListeners(address string, cfgSnap *pro
|
|||
logger: s.Logger,
|
||||
}
|
||||
listener := makeListener(listenerOpts)
|
||||
|
||||
route, _ := cfgSnap.APIGateway.HTTPRoutes.Get(readyListener.routeReference)
|
||||
foundJWT := false
|
||||
if listenerCfg.Override != nil && listenerCfg.Override.JWT != nil {
|
||||
foundJWT = true
|
||||
}
|
||||
|
||||
if !foundJWT && listenerCfg.Default != nil && listenerCfg.Default.JWT != nil {
|
||||
foundJWT = true
|
||||
}
|
||||
|
||||
if !foundJWT {
|
||||
for _, rule := range route.Rules {
|
||||
if rule.Filters.JWT != nil {
|
||||
foundJWT = true
|
||||
break
|
||||
}
|
||||
for _, svc := range rule.Services {
|
||||
if svc.Filters.JWT != nil {
|
||||
foundJWT = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var authFilters []*envoy_http_v3.HttpFilter
|
||||
if foundJWT {
|
||||
builder := &GatewayAuthFilterBuilder{
|
||||
listener: listenerCfg,
|
||||
route: route,
|
||||
providers: cfgSnap.JWTProviders,
|
||||
envoyProviders: make(map[string]*envoy_http_jwt_authn_v3.JwtProvider, len(cfgSnap.JWTProviders)),
|
||||
}
|
||||
authFilters, err = builder.makeGatewayAuthFilters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
filterOpts := listenerFilterOpts{
|
||||
useRDS: true,
|
||||
protocol: listenerKey.Protocol,
|
||||
|
@ -145,7 +187,7 @@ func (s *ResourceGenerator) makeAPIGatewayListeners(address string, cfgSnap *pro
|
|||
cluster: "",
|
||||
statPrefix: "ingress_upstream_",
|
||||
routePath: "",
|
||||
httpAuthzFilters: nil,
|
||||
httpAuthzFilters: authFilters,
|
||||
accessLogs: &cfgSnap.Proxy.AccessLogs,
|
||||
logger: s.Logger,
|
||||
}
|
||||
|
@ -210,7 +252,6 @@ type readyListener struct {
|
|||
|
||||
// getReadyListeners returns a map containing the list of upstreams for each listener that is ready
|
||||
func getReadyListeners(cfgSnap *proxycfg.ConfigSnapshot) map[string]readyListener {
|
||||
|
||||
ready := map[string]readyListener{}
|
||||
for _, l := range cfgSnap.APIGateway.Listeners {
|
||||
// Only include upstreams for listeners that are ready
|
||||
|
@ -278,7 +319,7 @@ func makeDownstreamTLSContextFromSnapshotAPIListenerConfig(cfgSnap *proxycfg.Con
|
|||
func makeCommonTLSContextFromSnapshotAPIGatewayListenerConfig(cfgSnap *proxycfg.ConfigSnapshot, listenerCfg structs.APIGatewayListener) (*envoy_tls_v3.CommonTlsContext, error) {
|
||||
var tlsContext *envoy_tls_v3.CommonTlsContext
|
||||
|
||||
//API Gateway TLS config is per listener
|
||||
// API Gateway TLS config is per listener
|
||||
tlsCfg, err := resolveAPIListenerTLSConfig(listenerCfg.TLS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -321,8 +362,8 @@ func makeInlineOverrideFilterChains(cfgSnap *proxycfg.ConfigSnapshot,
|
|||
tlsCfg structs.GatewayTLSConfig,
|
||||
protocol string,
|
||||
filterOpts listenerFilterOpts,
|
||||
certs []structs.InlineCertificateConfigEntry) ([]*envoy_listener_v3.FilterChain, error) {
|
||||
|
||||
certs []structs.InlineCertificateConfigEntry,
|
||||
) ([]*envoy_listener_v3.FilterChain, error) {
|
||||
var chains []*envoy_listener_v3.FilterChain
|
||||
|
||||
constructChain := func(name string, hosts []string, tlsContext *envoy_tls_v3.CommonTlsContext) error {
|
||||
|
|
|
@ -58,7 +58,7 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh
|
|||
continue
|
||||
}
|
||||
|
||||
virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, []string{"*"}, false)
|
||||
virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, []string{"*"}, false, perRouteFilterBuilder{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -94,12 +94,12 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh
|
|||
addressesMap[routeName] = make(map[string]string)
|
||||
}
|
||||
// cluster name is unique per address/port so we should not be doing any override here
|
||||
|
||||
clusterName := clusterNameForDestination(cfgSnap, svcConfig.Name, address, svcConfig.NamespaceOrDefault(), svcConfig.PartitionOrDefault())
|
||||
addressesMap[routeName][clusterName] = address
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -119,7 +119,6 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh
|
|||
}
|
||||
|
||||
func (s *ResourceGenerator) makeRoutesForAddresses(routeName string, addresses map[string]string) ([]proto.Message, error) {
|
||||
|
||||
var resources []proto.Message
|
||||
|
||||
route, err := makeNamedAddressesRoute(routeName, addresses)
|
||||
|
@ -201,7 +200,8 @@ func (s *ResourceGenerator) makeRoutes(
|
|||
cfgSnap *proxycfg.ConfigSnapshot,
|
||||
svc structs.ServiceName,
|
||||
clusterName string,
|
||||
autoHostRewrite bool) ([]proto.Message, error) {
|
||||
autoHostRewrite bool,
|
||||
) ([]proto.Message, error) {
|
||||
resolver, hasResolver := cfgSnap.TerminatingGateway.ServiceResolvers[svc]
|
||||
|
||||
if !hasResolver {
|
||||
|
@ -255,6 +255,7 @@ func (s *ResourceGenerator) routesForMeshGateway(cfgSnap *proxycfg.ConfigSnapsho
|
|||
chain,
|
||||
[]string{"*"},
|
||||
true,
|
||||
perRouteFilterBuilder{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -378,7 +379,7 @@ func (s *ResourceGenerator) routesForIngressGateway(cfgSnap *proxycfg.ConfigSnap
|
|||
}
|
||||
|
||||
domains := generateUpstreamIngressDomains(listenerKey, u)
|
||||
virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false)
|
||||
virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false, perRouteFilterBuilder{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -435,6 +436,7 @@ func (s *ResourceGenerator) routesForAPIGateway(cfgSnap *proxycfg.ConfigSnapshot
|
|||
readyUpstreamsList := getReadyListeners(cfgSnap)
|
||||
|
||||
for _, readyUpstreams := range readyUpstreamsList {
|
||||
readyUpstreams := readyUpstreams
|
||||
listenerCfg := readyUpstreams.listenerCfg
|
||||
// Do not create any route configuration for TCP listeners
|
||||
if listenerCfg.Protocol != structs.ListenerProtocolHTTP {
|
||||
|
@ -461,12 +463,13 @@ func (s *ResourceGenerator) routesForAPIGateway(cfgSnap *proxycfg.ConfigSnapshot
|
|||
// specific naming convention in discoverychain.consolidateHTTPRoutes. If we don't
|
||||
// convert our route to use the same naming convention, we won't find any chains below.
|
||||
reformatedRoutes := discoverychain.ReformatHTTPRoute(route, &listenerCfg, cfgSnap.APIGateway.GatewayConfig)
|
||||
|
||||
filterBuilder := perRouteFilterBuilder{providerMap: cfgSnap.JWTProviders, listener: &listenerCfg, route: route}
|
||||
for _, reformatedRoute := range reformatedRoutes {
|
||||
reformatedRoute := reformatedRoute
|
||||
|
||||
upstream := buildHTTPRouteUpstream(reformatedRoute, listenerCfg)
|
||||
uid := proxycfg.NewUpstreamID(&upstream)
|
||||
|
||||
chain := cfgSnap.APIGateway.DiscoveryChain[uid]
|
||||
if chain == nil {
|
||||
// Note that if we continue here we must also do this in the cluster generation
|
||||
|
@ -476,7 +479,7 @@ func (s *ResourceGenerator) routesForAPIGateway(cfgSnap *proxycfg.ConfigSnapshot
|
|||
|
||||
domains := generateUpstreamAPIsDomains(listenerKey, upstream, reformatedRoute.Hostnames)
|
||||
|
||||
virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false)
|
||||
virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false, filterBuilder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -605,6 +608,7 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain(
|
|||
chain *structs.CompiledDiscoveryChain,
|
||||
serviceDomains []string,
|
||||
forMeshGateway bool,
|
||||
filterBuilder perRouteFilterBuilder,
|
||||
) (*envoy_route_v3.VirtualHost, error) {
|
||||
routeName := uid.EnvoyID()
|
||||
var routes []*envoy_route_v3.Route
|
||||
|
@ -624,6 +628,7 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain(
|
|||
routes = make([]*envoy_route_v3.Route, 0, len(startNode.Routes))
|
||||
|
||||
for _, discoveryRoute := range startNode.Routes {
|
||||
discoveryRoute := discoveryRoute
|
||||
routeMatch := makeRouteMatchForDiscoveryRoute(discoveryRoute)
|
||||
|
||||
var (
|
||||
|
@ -688,8 +693,13 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain(
|
|||
}
|
||||
}
|
||||
|
||||
filter, err := filterBuilder.buildTypedPerFilterConfig(routeMatch, routeAction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
route.Match = routeMatch
|
||||
route.Action = routeAction
|
||||
route.TypedPerFilterConfig = filter
|
||||
|
||||
routes = append(routes, route)
|
||||
}
|
||||
|
|
|
@ -106,6 +106,10 @@ const (
|
|||
// certificates and cannot bind to any routes
|
||||
GatewayReasonInvalidCertificates GatewayConditionReason = "InvalidCertificates"
|
||||
|
||||
// This reason is used with the "Accepted" condition when the gateway has multiple invalid
|
||||
// JWT providers and cannot bind to any routes
|
||||
GatewayReasonInvalidJWTProviders GatewayConditionReason = "InvalidJWTProviders"
|
||||
|
||||
// This condition indicates that the gateway was unable to resolve
|
||||
// conflicting specification requirements for this Listener. If a
|
||||
// Listener is conflicted, its network port should not be configured
|
||||
|
@ -163,6 +167,14 @@ const (
|
|||
// If the reference is not allowed, the reason RefNotPermitted must be used
|
||||
// instead.
|
||||
GatewayListenerReasonInvalidCertificateRef GatewayConditionReason = "InvalidCertificateRef"
|
||||
|
||||
// This reason is used with the "ResolvedRefs" condition when a
|
||||
// Listener has a JWT configuration with at least one JWTProvider
|
||||
// that is invalid or does not exist.
|
||||
// A JWTProvider is considered invalid when it refers to a nonexistent
|
||||
// or unsupported resource or kind, or when the data within that resource
|
||||
// is malformed.
|
||||
GatewayListenerReasonInvalidJWTProviderRef GatewayConditionReason = "InvalidJWTProviderRef"
|
||||
)
|
||||
|
||||
var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[ConditionStatus][]GatewayConditionReason{
|
||||
|
@ -172,6 +184,7 @@ var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[Condition
|
|||
},
|
||||
ConditionStatusFalse: {
|
||||
GatewayReasonInvalidCertificates,
|
||||
GatewayReasonInvalidJWTProviders,
|
||||
},
|
||||
ConditionStatusUnknown: {},
|
||||
},
|
||||
|
@ -190,6 +203,7 @@ var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[Condition
|
|||
},
|
||||
ConditionStatusFalse: {
|
||||
GatewayListenerReasonInvalidCertificateRef,
|
||||
GatewayListenerReasonInvalidJWTProviderRef,
|
||||
},
|
||||
ConditionStatusUnknown: {},
|
||||
},
|
||||
|
@ -282,6 +296,10 @@ const (
|
|||
// This reason is used with the "Bound" condition when the route fails
|
||||
// to find the gateway
|
||||
RouteReasonGatewayNotFound RouteConditionReason = "GatewayNotFound"
|
||||
|
||||
// This reason is used with the "Accepted" condition when the route references non-existent
|
||||
// JWTProviders
|
||||
RouteReasonJWTProvidersNotFound RouteConditionReason = "JWTProvidersNotFound"
|
||||
)
|
||||
|
||||
var validRouteConditionReasonsMapping = map[RouteConditionType]map[ConditionStatus][]RouteConditionReason{
|
||||
|
@ -302,6 +320,7 @@ var validRouteConditionReasonsMapping = map[RouteConditionType]map[ConditionStat
|
|||
ConditionStatusFalse: {
|
||||
RouteReasonGatewayNotFound,
|
||||
RouteReasonFailedToBind,
|
||||
RouteReasonJWTProvidersNotFound,
|
||||
},
|
||||
ConditionStatusUnknown: {},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue