Files
capsule/internal/webhook/namespace/validation/handler.go
T
Oliver BählerandGitHub 15a848d844 fix: allow administrator operations on tenant namespaces (#2037)
fix: allow administrator operations on tenant namespaces 

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-07-17 09:38:32 +02:00

239 lines
5.8 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package validation
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
"github.com/projectcapsule/capsule/pkg/runtime/events"
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
"github.com/projectcapsule/capsule/pkg/tenant"
)
func NamespaceHandler(configuration configuration.Configuration, hndlers ...handlers.TypedHandlerWithTenantUser[*corev1.Namespace]) handlers.Handler {
return &handler{
cfg: configuration,
handlers: hndlers,
}
}
type handler struct {
cfg configuration.Configuration
handlers []handlers.TypedHandlerWithTenantUser[*corev1.Namespace]
}
func (h *handler) OnCreate(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
return ad.ErroredResponse(err)
}
if !user.IsAdmin() && !user.IsCapsule() && !tenant.HasTenantReference(ns) {
return nil
}
tnt, err := tenant.ResolveNamespaceTenant(ctx, reader, ns)
if err != nil {
return ad.ErroredResponse(err)
}
if !user.IsAdmin() && !user.IsCapsule() && tnt != nil {
return ad.Deny("only tenant owners can create tenant-owned namespaces")
}
if tnt == nil {
return nil
}
if terminating := h.rejectOnTermination(
ctx,
c,
ns,
tnt,
); terminating != nil {
return terminating
}
for _, hndl := range h.handlers {
if response := hndl.OnCreate(c, reader, user, ns, decoder, recorder, tnt)(ctx, req); response != nil {
return response
}
}
return nil
}
}
func (h *handler) OnDelete(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
oldNs := &corev1.Namespace{}
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
return ad.ErroredResponse(err)
}
tnt, err := tenant.ResolveNamespaceTenant(ctx, reader, oldNs)
if err != nil && !user.IsAdmin() {
return ad.ErroredResponse(err)
}
if tnt == nil {
return nil
}
for _, hndl := range h.handlers {
if response := hndl.OnDelete(c, reader, user, oldNs, decoder, recorder, tnt)(ctx, req); response != nil {
return response
}
}
return nil
}
}
func (h *handler) OnUpdate(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
return ad.ErroredResponse(err)
}
oldNs := &corev1.Namespace{}
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
return ad.ErroredResponse(err)
}
oldHasTenantReference := tenant.HasTenantReference(oldNs)
newHasTenantReference := tenant.HasTenantReference(ns)
if !user.IsAdmin() {
switch {
case !oldHasTenantReference && newHasTenantReference:
return ad.Deny("namespace can not be patched into a tenant")
case oldHasTenantReference && !newHasTenantReference:
return ad.Deny("namespace can not remove tenant ownership")
case !oldHasTenantReference && !newHasTenantReference:
return nil
}
}
oldTenant, err := tenant.ResolveNamespaceTenant(ctx, reader, oldNs)
if err != nil && !user.IsAdmin() {
return ad.ErroredResponse(err)
}
newTenant, err := tenant.ResolveNamespaceTenant(ctx, reader, ns)
if err != nil {
return ad.ErroredResponse(err)
}
if !user.IsAdmin() {
if oldTenant == nil || newTenant == nil {
return ad.Deny("namespace tenant ownership is incomplete")
}
if oldTenant.GetName() != newTenant.GetName() || oldTenant.GetUID() != newTenant.GetUID() {
return ad.Deny("namespace can not be migrated between tenants")
}
if user.IsCapsule() && !tenant.NamespaceIsOwned(ctx, c, h.cfg, oldNs, oldTenant, user) {
recorder.LabeledEvent(
ns,
corev1.EventTypeWarning,
events.ReasonNamespaceHijack,
events.ActionValidationDenied,
"namespace can not be patched",
).
WithRequestAnnotations(req).
Emit(ctx)
return ad.Deny("denied patch request for this namespace")
}
}
tnt := newTenant
if !user.IsAdmin() {
tnt = oldTenant
}
if tnt == nil {
return nil
}
if terminating := h.rejectOnTermination(ctx, c, ns, newTenant); terminating != nil {
return terminating
}
for _, hndl := range h.handlers {
if response := hndl.OnUpdate(c, reader, user, ns, oldNs, decoder, recorder, tnt)(ctx, req); response != nil {
return response
}
}
return nil
}
}
func (h *handler) rejectOnTermination(
ctx context.Context,
c client.Reader,
ns *corev1.Namespace,
t *capsulev1beta2.Tenant,
) *admission.Response {
if t == nil {
return nil
}
tnt := &capsulev1beta2.Tenant{}
_ = c.Get(ctx, types.NamespacedName{Name: t.GetName()}, tnt)
if tnt.DeletionTimestamp == nil {
return nil
}
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
Name: ns.GetName(),
UID: ns.GetUID(),
})
if instance != nil {
return nil
}
err := fmt.Errorf("tenant is terminating and does not accept new namespaces")
return ad.ErroredResponse(err)
}