mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-23 06:26:43 +00:00
- Add GatewayNamespacedName, AllowedGatewaySpec, GatewayRuleSpec types to pkg/api/ - Add Gateways field to NamespaceRuleEnforceBody for per-namespace gateway rules - Add HTTPRoute validation webhook to enforce allowed gateways - Add HTTPRoute default mutation webhook to inject default Gateway parentRef - Update Helm chart with validating/mutating webhook configurations for httproutes - Update CRD schemas for tenants and rulestatuses - Add GatewayForbiddenError and ReasonForbiddenGateway event reason Co-authored-by: oliverbaehler <26610571+oliverbaehler@users.noreply.github.com>
143 lines
3.7 KiB
Go
143 lines
3.7 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package httproute
|
|
|
|
import (
|
|
"context"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/events"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
|
|
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
|
|
)
|
|
|
|
type gatewayValidator struct{}
|
|
|
|
// GatewayValidator returns a TypedHandlerWithTenantWithRuleset that validates
|
|
// HTTPRoute parentRefs against the gateway rules configured in namespace rules.
|
|
func GatewayValidator() handlers.TypedHandlerWithTenantWithRuleset[*gatewayv1.HTTPRoute] {
|
|
return &gatewayValidator{}
|
|
}
|
|
|
|
func (h *gatewayValidator) OnCreate(
|
|
c client.Client,
|
|
obj *gatewayv1.HTTPRoute,
|
|
_ admission.Decoder,
|
|
recorder events.EventRecorder,
|
|
tnt *capsulev1beta2.Tenant,
|
|
rule *capsulev1beta2.NamespaceRuleBody,
|
|
) handlers.Func {
|
|
return func(ctx context.Context, req admission.Request) *admission.Response {
|
|
return h.validate(ctx, c, req, obj, tnt, recorder, rule)
|
|
}
|
|
}
|
|
|
|
func (h *gatewayValidator) OnUpdate(
|
|
c client.Client,
|
|
_ *gatewayv1.HTTPRoute,
|
|
obj *gatewayv1.HTTPRoute,
|
|
_ admission.Decoder,
|
|
recorder events.EventRecorder,
|
|
tnt *capsulev1beta2.Tenant,
|
|
rule *capsulev1beta2.NamespaceRuleBody,
|
|
) handlers.Func {
|
|
return func(ctx context.Context, req admission.Request) *admission.Response {
|
|
return h.validate(ctx, c, req, obj, tnt, recorder, rule)
|
|
}
|
|
}
|
|
|
|
func (h *gatewayValidator) OnDelete(
|
|
_ client.Client,
|
|
_ *gatewayv1.HTTPRoute,
|
|
_ admission.Decoder,
|
|
_ events.EventRecorder,
|
|
_ *capsulev1beta2.Tenant,
|
|
_ *capsulev1beta2.NamespaceRuleBody,
|
|
) handlers.Func {
|
|
return func(context.Context, admission.Request) *admission.Response {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (h *gatewayValidator) validate(
|
|
ctx context.Context,
|
|
c client.Client,
|
|
req admission.Request,
|
|
route *gatewayv1.HTTPRoute,
|
|
tnt *capsulev1beta2.Tenant,
|
|
recorder events.EventRecorder,
|
|
rule *capsulev1beta2.NamespaceRuleBody,
|
|
) *admission.Response {
|
|
if rule == nil || rule.Enforce.Gateways == nil || rule.Enforce.Gateways.Gateway == nil {
|
|
return nil
|
|
}
|
|
|
|
allowed := rule.Enforce.Gateways.Gateway
|
|
|
|
for _, parentRef := range route.Spec.ParentRefs {
|
|
// Only validate parentRefs that point to a Gateway resource.
|
|
if parentRef.Kind != nil && *parentRef.Kind != gatewayv1.Kind("Gateway") {
|
|
continue
|
|
}
|
|
|
|
if parentRef.Group != nil && string(*parentRef.Group) != gatewayv1.GroupName {
|
|
continue
|
|
}
|
|
|
|
gwName := string(parentRef.Name)
|
|
gwNamespace := route.Namespace
|
|
|
|
if parentRef.Namespace != nil {
|
|
gwNamespace = string(*parentRef.Namespace)
|
|
}
|
|
|
|
// Try to fetch the Gateway object for label-selector matching.
|
|
gw := &gatewayv1.Gateway{}
|
|
|
|
var gwObj client.Object
|
|
|
|
if err := c.Get(ctx, types.NamespacedName{Namespace: gwNamespace, Name: gwName}, gw); err != nil {
|
|
if !k8serrors.IsNotFound(err) {
|
|
return errResponse(err)
|
|
}
|
|
} else {
|
|
gwObj = gw
|
|
}
|
|
|
|
if !allowed.MatchGateway(gwNamespace, gwName, gwObj) {
|
|
recorder.Eventf(
|
|
tnt,
|
|
nil,
|
|
corev1.EventTypeWarning,
|
|
evt.ReasonForbiddenGateway,
|
|
evt.ActionValidationDenied,
|
|
"HTTPRoute %s/%s references forbidden Gateway %s/%s",
|
|
req.Namespace, req.Name, gwNamespace, gwName,
|
|
)
|
|
|
|
response := admission.Denied(
|
|
caperrors.NewGatewayForbidden(gwName, gwNamespace, *allowed).Error(),
|
|
)
|
|
|
|
return &response
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func errResponse(err error) *admission.Response {
|
|
resp := admission.Errored(500, err)
|
|
|
|
return &resp
|
|
}
|