From 18752f9494d7913b5d287a242ed43d4a63a6e567 Mon Sep 17 00:00:00 2001 From: Lukas Boettcher <1340215+lukasboettcher@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:53:45 +0200 Subject: [PATCH] feat: improve SSAR and SSRR performance and usability Signed-off-by: Lukas Boettcher <1340215+lukasboettcher@users.noreply.github.com> --- internal/authorization/middleware.go | 96 ++++++-- internal/authorization/middleware_test.go | 270 ++++++++++++++++++++++ internal/modules/namespace/get.go | 2 +- internal/modules/namespace/list.go | 20 +- internal/modules/namespace/utils.go | 69 ++++++ internal/webserver/webserver.go | 63 ++++- 6 files changed, 486 insertions(+), 34 deletions(-) create mode 100644 internal/authorization/middleware_test.go create mode 100644 internal/modules/namespace/utils.go diff --git a/internal/authorization/middleware.go b/internal/authorization/middleware.go index c1381d9e..838688be 100644 --- a/internal/authorization/middleware.go +++ b/internal/authorization/middleware.go @@ -7,19 +7,69 @@ import ( authorizationv1 "k8s.io/api/authorization/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" "github.com/projectcapsule/capsule-proxy/internal/modules/clusterscoped" "github.com/projectcapsule/capsule-proxy/internal/tenant" "github.com/projectcapsule/capsule-proxy/internal/types" ) -func MutateAuthorization(proxyClusterScoped bool, proxyTenants []*tenant.ProxyTenant, obj *runtime.Object, gvk schema.GroupVersionKind) error { +// listVerb and watchVerb are the lowercase RBAC verbs used to advertise the +// ability to list/watch resources. Note that types.ListVerb is "List" +// (capitalised) which does not match the verbs sent by clients (e.g. kubectl), +// hence dedicated constants. +const ( + listVerb = "list" + watchVerb = "watch" +) + +// NamespacedResourceKey builds the lookup key identifying a namespaced resource +// that capsule-proxy proxies. It matches the (group, plural resource) pair +// carried by SelfSubjectAccessReview resource attributes. +func NamespacedResourceKey(group, resource string) string { + return group + "/" + resource +} + +// isCrossNamespaceListVerb reports whether the verb is a list or watch, i.e. the +// verbs used by `kubectl get -A` (list) and `-A -w` (watch). +func isCrossNamespaceListVerb(verb string) bool { + return strings.EqualFold(verb, listVerb) || strings.EqualFold(verb, watchVerb) +} + +// MutateAuthorization augments the API server's answer to self review requests +// (SelfSubjectAccessReview / SelfSubjectRulesReview) with the capabilities +// capsule-proxy adds on top of native RBAC, so that clients such as +// `kubectl auth can-i` reflect what actually works through the proxy. +func MutateAuthorization(proxyClusterScoped bool, proxyTenants []*tenant.ProxyTenant, namespacedResources sets.Set[string], obj *runtime.Object, gvk schema.GroupVersionKind) error { switch gvk.Kind { case "SelfSubjectAccessReview": //nolint:forcetypeassert accessReview := (*obj).(*authorizationv1.SelfSubjectAccessReview) - if accessReview.Spec.ResourceAttributes.Resource == types.Namespaces && accessReview.Spec.ResourceAttributes.Verb == types.ListVerb { - accessReview.Status.Allowed = true + + attributes := accessReview.Spec.ResourceAttributes + if attributes == nil { + return nil + } + + // capsule-proxy always lets tenant owners list their own namespaces. + if attributes.Resource == types.Namespaces && strings.EqualFold(attributes.Verb, listVerb) { + grantAccess(accessReview) + + return nil + } + + // capsule-proxy serves cross-namespace (`-A`) list/watch of any proxied + // namespaced resource for tenant owners, transparently scoping the + // result to the tenant namespaces. The native API server denies such a + // cluster-scoped request, so advertise the capability here to reflect + // what actually works through the proxy (e.g. `kubectl get pods -A`). + if len(proxyTenants) > 0 && + attributes.Namespace == "" && + isCrossNamespaceListVerb(attributes.Verb) && + namespacedResources.Has(NamespacedResourceKey(attributes.Group, attributes.Resource)) { + grantAccess(accessReview) + + return nil } if !proxyClusterScoped { @@ -27,9 +77,9 @@ func MutateAuthorization(proxyClusterScoped bool, proxyTenants []*tenant.ProxyTe } accessReviewGvk := schema.GroupVersionKind{ - Group: accessReview.Spec.ResourceAttributes.Group, - Version: accessReview.Spec.ResourceAttributes.Version, - Kind: accessReview.Spec.ResourceAttributes.Resource, + Group: attributes.Group, + Version: attributes.Version, + Kind: attributes.Resource, } verbs, req := clusterscoped.GetClusterScopeRequirements(&accessReviewGvk, proxyTenants) @@ -38,8 +88,8 @@ func MutateAuthorization(proxyClusterScoped bool, proxyTenants []*tenant.ProxyTe } for _, verb := range verbs { - if accessReview.Spec.ResourceAttributes.Verb == strings.ToLower(verb.String()) { - accessReview.Status.Allowed = true + if strings.EqualFold(attributes.Verb, verb.String()) { + grantAccess(accessReview) return nil } @@ -48,21 +98,21 @@ func MutateAuthorization(proxyClusterScoped bool, proxyTenants []*tenant.ProxyTe //nolint:forcetypeassert rules := (*obj).(*authorizationv1.SelfSubjectRulesReview) - var resourceRules []authorizationv1.ResourceRule + injectedRules := []authorizationv1.ResourceRule{ + { + APIGroups: []string{""}, + Resources: []string{types.Namespaces}, + Verbs: []string{listVerb}, + }, + } if proxyClusterScoped { - resourceRules = getAllResourceRules(proxyTenants) - } else { - resourceRules = []authorizationv1.ResourceRule{} + injectedRules = append(injectedRules, getAllResourceRules(proxyTenants)...) } - resourceRules = append(resourceRules, authorizationv1.ResourceRule{ - APIGroups: []string{""}, - Resources: []string{types.Namespaces}, - Verbs: []string{types.ListVerb}, - }) - - rules.Status = authorizationv1.SubjectRulesReviewStatus{ResourceRules: resourceRules} + // The rules resolved by the apiserver are kept and capsule-proxy only appends, + // so passed-through and namespaced permissions remain visible to the client. + rules.Status.ResourceRules = append(rules.Status.ResourceRules, injectedRules...) default: return fmt.Errorf("unsupported kind: %s", gvk.Kind) } @@ -70,6 +120,14 @@ func MutateAuthorization(proxyClusterScoped bool, proxyTenants []*tenant.ProxyTe return nil } +// grantAccess marks a SelfSubjectAccessReview as allowed by capsule-proxy, +// clearing any denial coming from the API server. +func grantAccess(accessReview *authorizationv1.SelfSubjectAccessReview) { + accessReview.Status.Allowed = true + accessReview.Status.Denied = false + accessReview.Status.Reason = "granted by capsule-proxy" +} + func getAllResourceRules(proxyTenants []*tenant.ProxyTenant) []authorizationv1.ResourceRule { resourceRules := []authorizationv1.ResourceRule{} diff --git a/internal/authorization/middleware_test.go b/internal/authorization/middleware_test.go new file mode 100644 index 00000000..af680623 --- /dev/null +++ b/internal/authorization/middleware_test.go @@ -0,0 +1,270 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package authorization + +import ( + "testing" + + authorizationv1 "k8s.io/api/authorization/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/projectcapsule/capsule-proxy/api/v1beta1" + "github.com/projectcapsule/capsule-proxy/internal/tenant" +) + +func hasResourceRule(rules []authorizationv1.ResourceRule, group, resource, verb string) bool { + for _, r := range rules { + var groupMatch, resourceMatch, verbMatch bool + + for _, g := range r.APIGroups { + if g == group { + groupMatch = true + } + } + + for _, res := range r.Resources { + if res == resource { + resourceMatch = true + } + } + + for _, v := range r.Verbs { + if v == verb { + verbMatch = true + } + } + + if groupMatch && resourceMatch && verbMatch { + return true + } + } + + return false +} + +func TestMutateAuthorization_SelfSubjectRulesReviewMerges(t *testing.T) { + t.Parallel() + + // Rules resolved by the API server for the requester must be preserved. + apiServerRule := authorizationv1.ResourceRule{ + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + Verbs: []string{"get", "list", "watch"}, + } + + review := &authorizationv1.SelfSubjectRulesReview{ + Status: authorizationv1.SubjectRulesReviewStatus{ + ResourceRules: []authorizationv1.ResourceRule{apiServerRule}, + NonResourceRules: []authorizationv1.NonResourceRule{{Verbs: []string{"get"}, NonResourceURLs: []string{"/healthz"}}}, + Incomplete: true, + }, + } + + proxyTenants := []*tenant.ProxyTenant{ + { + ClusterResources: []v1beta1.ClusterResource{ + { + APIGroups: []string{"storage.k8s.io"}, + Resources: []string{"storageclasses"}, + Operations: []v1beta1.ClusterResourceOperation{v1beta1.ClusterResourceOperationList}, + }, + }, + }, + } + + var obj runtime.Object = review + + if err := MutateAuthorization(true, proxyTenants, nil, &obj, schema.GroupVersionKind{Kind: "SelfSubjectRulesReview"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + rules := review.Status.ResourceRules + + if !hasResourceRule(rules, "apps", "deployments", "list") { + t.Errorf("expected the API server rules to be preserved, got %+v", rules) + } + + if !hasResourceRule(rules, "", "namespaces", "list") { + t.Errorf("expected injected namespaces/list rule, got %+v", rules) + } + + if !hasResourceRule(rules, "storage.k8s.io", "storageclasses", "list") { + t.Errorf("expected injected cluster resource rule, got %+v", rules) + } + + if !review.Status.Incomplete { + t.Errorf("expected Incomplete flag from the API server to be preserved") + } + + if len(review.Status.NonResourceRules) != 1 { + t.Errorf("expected NonResourceRules from the API server to be preserved, got %+v", review.Status.NonResourceRules) + } +} + +func TestMutateAuthorization_SelfSubjectRulesReviewWithoutClusterScoped(t *testing.T) { + t.Parallel() + + review := &authorizationv1.SelfSubjectRulesReview{} + + var obj runtime.Object = review + + if err := MutateAuthorization(false, nil, nil, &obj, schema.GroupVersionKind{Kind: "SelfSubjectRulesReview"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !hasResourceRule(review.Status.ResourceRules, "", "namespaces", "list") { + t.Errorf("expected injected namespaces/list rule, got %+v", review.Status.ResourceRules) + } +} + +func TestMutateAuthorization_SelfSubjectAccessReviewNamespacesList(t *testing.T) { + t.Parallel() + + review := &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Resource: "namespaces", + Verb: "list", + }, + }, + Status: authorizationv1.SubjectAccessReviewStatus{Denied: true}, + } + + var obj runtime.Object = review + + if err := MutateAuthorization(false, nil, nil, &obj, schema.GroupVersionKind{Kind: "SelfSubjectAccessReview"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !review.Status.Allowed { + t.Errorf("expected namespaces/list to be allowed") + } + + if review.Status.Denied { + t.Errorf("expected Denied to be cleared when capsule-proxy grants access") + } +} + +func TestMutateAuthorization_SelfSubjectAccessReviewNonResourceAttributes(t *testing.T) { + t.Parallel() + + // A review carrying NonResourceAttributes must not panic and must be left + // untouched. + review := &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + NonResourceAttributes: &authorizationv1.NonResourceAttributes{ + Path: "/healthz", + Verb: "get", + }, + }, + Status: authorizationv1.SubjectAccessReviewStatus{Allowed: true}, + } + + var obj runtime.Object = review + + if err := MutateAuthorization(true, nil, nil, &obj, schema.GroupVersionKind{Kind: "SelfSubjectAccessReview"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !review.Status.Allowed { + t.Errorf("expected the API server verdict to be preserved for NonResourceAttributes reviews") + } +} + +func namespacedAccessReview(group, resource, verb, namespace string) *authorizationv1.SelfSubjectAccessReview { + return &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Group: group, + Resource: resource, + Verb: verb, + Namespace: namespace, + }, + }, + Status: authorizationv1.SubjectAccessReviewStatus{Denied: true}, + } +} + +func TestMutateAuthorization_SelfSubjectAccessReviewNamespacedAllNamespaces(t *testing.T) { + t.Parallel() + + // capsule-proxy serves `kubectl get pods -A` for tenant owners, so an + // otherwise denied cluster-scoped list must be granted. + proxyTenants := []*tenant.ProxyTenant{{}} + namespaced := sets.New[string](NamespacedResourceKey("", "pods")) + + for _, verb := range []string{"list", "watch"} { + review := namespacedAccessReview("", "pods", verb, "") + + var obj runtime.Object = review + + if err := MutateAuthorization(false, proxyTenants, namespaced, &obj, schema.GroupVersionKind{Kind: "SelfSubjectAccessReview"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !review.Status.Allowed || review.Status.Denied { + t.Errorf("expected %s pods -A to be granted for a tenant owner, got %+v", verb, review.Status) + } + } +} + +func TestMutateAuthorization_SelfSubjectAccessReviewNamespacedNotTenantOwner(t *testing.T) { + t.Parallel() + + // Non tenant owners must keep the API server verdict untouched. + namespaced := sets.New[string](NamespacedResourceKey("", "pods")) + review := namespacedAccessReview("", "pods", "list", "") + + var obj runtime.Object = review + + if err := MutateAuthorization(false, nil, namespaced, &obj, schema.GroupVersionKind{Kind: "SelfSubjectAccessReview"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if review.Status.Allowed || !review.Status.Denied { + t.Errorf("expected the API server verdict to be preserved for non tenant owners, got %+v", review.Status) + } +} + +func TestMutateAuthorization_SelfSubjectAccessReviewNamespacedScopedRequest(t *testing.T) { + t.Parallel() + + // A namespace-scoped review is answered by the API server itself, so the + // proxy must not override its verdict. + proxyTenants := []*tenant.ProxyTenant{{}} + namespaced := sets.New[string](NamespacedResourceKey("", "pods")) + review := namespacedAccessReview("", "pods", "list", "tenant-ns") + + var obj runtime.Object = review + + if err := MutateAuthorization(false, proxyTenants, namespaced, &obj, schema.GroupVersionKind{Kind: "SelfSubjectAccessReview"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if review.Status.Allowed || !review.Status.Denied { + t.Errorf("expected namespace-scoped reviews to be left untouched, got %+v", review.Status) + } +} + +func TestMutateAuthorization_SelfSubjectAccessReviewNonProxiedResource(t *testing.T) { + t.Parallel() + + // A resource that capsule-proxy does not proxy as namespaced (e.g. a + // cluster-scoped resource) must not be granted through this path. + proxyTenants := []*tenant.ProxyTenant{{}} + namespaced := sets.New[string](NamespacedResourceKey("", "pods")) + review := namespacedAccessReview("", "nodes", "list", "") + + var obj runtime.Object = review + + if err := MutateAuthorization(false, proxyTenants, namespaced, &obj, schema.GroupVersionKind{Kind: "SelfSubjectAccessReview"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if review.Status.Allowed || !review.Status.Denied { + t.Errorf("expected non-proxied resources to be left untouched, got %+v", review.Status) + } +} diff --git a/internal/modules/namespace/get.go b/internal/modules/namespace/get.go index 88fd3a12..d8dd9543 100644 --- a/internal/modules/namespace/get.go +++ b/internal/modules/namespace/get.go @@ -100,7 +100,7 @@ func (l get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Req if !sets.NewString(userNamespaces...).Has(name) { return nil, errors.NewNotFoundError(name, l.GroupKind()) } - } else if !tenants.Has(tntName) { + } else if !tenants.Has(tntName) && !matchesClusterScopedNamespace(proxyTenants, ns) { return nil, errors.NewNotFoundError(name, l.GroupKind()) } diff --git a/internal/modules/namespace/list.go b/internal/modules/namespace/list.go index a1a293fa..dbcd57f2 100644 --- a/internal/modules/namespace/list.go +++ b/internal/modules/namespace/list.go @@ -11,7 +11,9 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/util/sets" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/projectcapsule/capsule-proxy/internal/controllers" "github.com/projectcapsule/capsule-proxy/internal/modules" @@ -23,13 +25,15 @@ import ( type list struct { roleBindingsReflector *controllers.RoleBindingReflector + reader client.Reader log logr.Logger gk schema.GroupVersionKind } -func List(roleBindingsReflector *controllers.RoleBindingReflector) modules.Module { +func List(roleBindingsReflector *controllers.RoleBindingReflector, reader client.Reader) modules.Module { return &list{ roleBindingsReflector: roleBindingsReflector, + reader: reader, log: ctrl.Log.WithName("namespace_list"), gk: schema.GroupVersionKind{ Group: corev1.GroupName, @@ -69,11 +73,23 @@ func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Re } } + // Namespaces can additionally be granted through cluster-scoped + // ClusterResources rules (e.g. GlobalProxySettings or ProxySettings) that + // select namespaces by label. This lets subjects that are not tenant owners + // list the matching namespaces. We resolve those rules to concrete namespace + // names and merge them, so a single name-based selector is produced. + clusterScoped, err := clusterScopedNamespaceNames(proxyRequest.GetHTTPRequest().Context(), l.reader, proxyTenants) + if err != nil { + return nil, errors.NewBadRequest(err, l.GroupKind()) + } + + userNamespaces = append(userNamespaces, clusterScoped...) + var r *labels.Requirement switch { case len(userNamespaces) > 0: - r, err = labels.NewRequirement(corev1.LabelMetadataName, selection.In, userNamespaces) + r, err = labels.NewRequirement(corev1.LabelMetadataName, selection.In, sets.List(sets.New(userNamespaces...))) default: r, err = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) } diff --git a/internal/modules/namespace/utils.go b/internal/modules/namespace/utils.go new file mode 100644 index 00000000..5bc13546 --- /dev/null +++ b/internal/modules/namespace/utils.go @@ -0,0 +1,69 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/projectcapsule/capsule-proxy/internal/modules/clusterscoped" + "github.com/projectcapsule/capsule-proxy/internal/modules/utils" + "github.com/projectcapsule/capsule-proxy/internal/tenant" + "github.com/projectcapsule/capsule-proxy/internal/types" +) + +// namespacesGVK returns the GroupVersionKind used to match cluster-scoped +// ClusterResources rules against the core namespaces resource. +func namespacesGVK() *schema.GroupVersionKind { + return &schema.GroupVersionKind{Group: corev1.GroupName, Version: types.V1, Kind: types.Namespaces} +} + +// clusterScopedNamespaceNames resolves the namespaces selected by cluster-scoped +// ClusterResources rules (matching the namespaces resource) to their names, so +// subjects granted access via GlobalProxySettings or ProxySettings can list the +// corresponding namespaces without being tenant owners. +func clusterScopedNamespaceNames(ctx context.Context, reader client.Reader, proxyTenants []*tenant.ProxyTenant) ([]string, error) { + _, requirements := clusterscoped.GetClusterScopeRequirements(namespacesGVK(), proxyTenants) + if len(requirements) == 0 { + return nil, nil + } + + selector, err := utils.HandleListSelector(requirements) + if err != nil { + return nil, err + } + + nsList := &corev1.NamespaceList{} + if err := reader.List(ctx, nsList, client.MatchingLabelsSelector{Selector: selector}); err != nil { + return nil, err + } + + names := make([]string, 0, len(nsList.Items)) + for i := range nsList.Items { + names = append(names, nsList.Items[i].GetName()) + } + + return names, nil +} + +// matchesClusterScopedNamespace reports whether the namespace is selected by any +// cluster-scoped ClusterResources rule (e.g. from GlobalProxySettings or +// ProxySettings), allowing subjects granted access through those rules to get +// the namespace without being tenant owners. +func matchesClusterScopedNamespace(proxyTenants []*tenant.ProxyTenant, ns *corev1.Namespace) bool { + _, requirements := clusterscoped.GetClusterScopeRequirements(namespacesGVK(), proxyTenants) + + nsLabels := labels.Set(ns.GetLabels()) + for _, requirement := range requirements { + if requirement.Matches(nsLabels) { + return true + } + } + + return false +} diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go index 6e7ad2ea..54eba9a2 100644 --- a/internal/webserver/webserver.go +++ b/internal/webserver/webserver.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "mime" "net" "net/http" "net/http/httptest" @@ -156,6 +157,12 @@ type kubeFilter struct { protoEncoder *protobuf.Serializer universalDecoder runtime.Decoder scheme *runtime.Scheme + + // namespacedResources holds the set of proxied namespaced resources (keyed + // via authorization.NamespacedResourceKey) for which capsule-proxy serves + // cross-namespace (`-A`) list/watch queries. It is used to advertise that + // capability through the self review (auth review) APIs. + namespacedResources sets.Set[string] } // NeedLeaderElection starts the proxy (webserver) independently of controller manager @@ -323,6 +330,12 @@ func (n *kubeFilter) reverseProxyMiddleware(next http.Handler) http.Handler { }) } +func hasBearerToken(request *http.Request) bool { + parts := strings.Fields(request.Header.Get("Authorization")) + + return len(parts) >= 2 && strings.EqualFold(parts[0], "Bearer") +} + func (n *kubeFilter) authorizationMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if !slices.Contains(authorization.Paths, request.URL.Path) { @@ -331,8 +344,24 @@ func (n *kubeFilter) authorizationMiddleware(next http.Handler) http.Handler { return } + // Self-review requests are forwarded to the API server using the + // caller's own bearer token whenever one is available, + // instead of having the proxy impersonate the user together with every + // one of their groups. + // + // Impersonating a token that carries hundreds of groups makes the API + // server authorize each group impersonation individually, which is + // extremely slow. + // A self-review answered with the caller's own + // credentials returns the exact same result at a fraction of the cost. + w := httptest.NewRecorder() - next.ServeHTTP(w, request) + + if hasBearerToken(request) { + n.reverseProxy.ServeHTTP(w, request) + } else { + next.ServeHTTP(w, request) + } result := w.Result() @@ -367,20 +396,27 @@ func (n *kubeFilter) authorizationMiddleware(next http.Handler) http.Handler { n.log.Error(err, "cannot decode authorization object") } - err = authorization.MutateAuthorization(n.gates.Enabled(features.ProxyClusterScoped), proxyTenants, &obj, *gvk) - if err != nil { + if err = authorization.MutateAuthorization(n.gates.Enabled(features.ProxyClusterScoped), proxyTenants, n.namespacedResources, &obj, *gvk); err != nil { n.log.Error(err, "cannot mutate authorization object") } - if request.Header.Get("Content-Type") == "application/json" { - body, err = utils.JsonEncode(obj, n.scheme) - if err != nil { - n.log.Error(err, "cannot marshal Authorization object to json") + var mediaType string + if mediaType, _, err = mime.ParseMediaType(request.Header.Get("Content-Type")); err != nil { + n.log.Error(err, "failed to parse Content-Type header") + } + + switch mediaType { + case "application/json": + if encoded, encodeErr := utils.JsonEncode(obj, n.scheme); encodeErr != nil { + n.log.Error(encodeErr, "cannot marshal Authorization object to json") + } else { + body = encoded } - } else if request.Header.Get("Content-Type") == "application/vnd.kubernetes.protobuf" { - body, err = runtime.Encode(n.protoEncoder, obj) - if err != nil { - n.log.Error(err, "cannot marshal Authorization object to protobuf") + case "application/vnd.kubernetes.protobuf": + if encoded, encodeErr := runtime.Encode(n.protoEncoder, obj); encodeErr != nil { + n.log.Error(encodeErr, "cannot marshal Authorization object to protobuf") + } else { + body = encoded } } @@ -479,7 +515,7 @@ func (n *kubeFilter) registerModules(ctx context.Context, root *mux.Router) { // We are using namespaces and tenants as default routes from the legacy // system, as their outcome heavily relies on the tenants config/status modList := []modules.Module{ - namespace.List(n.roleBindingsReflector), + namespace.List(n.roleBindingsReflector, n.reader), namespace.Get(n.roleBindingsReflector, n.reader), tenants.List(), tenants.Get(n.reader), @@ -528,9 +564,12 @@ func (n *kubeFilter) registerModules(ctx context.Context, root *mux.Router) { panic(err) } + n.namespacedResources = sets.New[string]() + for _, api := range apis { n.log.V(6).Info("adding generic namespaced resource", "url", api.Path()) modList = append(modList, namespaced.CatchAll(n.reader, n.writer, api.Path())) + n.namespacedResources.Insert(authorization.NamespacedResourceKey(api.Group, api.URLName)) } for _, i := range modList {