CVE-2022-46167: Incorrect Authorization (CWE-863)
value: Capsule is a multi-tenancy and policy-based framework for Kubernetes. Prior to version 0.1.3, a ServiceAccount deployed in a Tenant Namespace, when granted with `PATCH` capabilities on its own Namespace, is able to edit it and remove the Owner Reference, breaking the reconciliation of the Capsule Operator and removing all the enforcement like Pod Security annotations, Network Policies, Limit Range and Resource Quota items. An attacker could detach the Namespace from a Tenant that is forbidding starting privileged Pods using the Pod Security labels by removing the OwnerReference, removing the enforcement labels, and being able to start privileged containers that would be able to start a generic Kubernetes privilege escalation. Patches have been released for version 0.1.3. No known workarounds are available.

CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 8.8
Published: 2022-12-02T19:15Z
Repository: https://github.com/clastix/capsule
Language: Go
Files changed: controllers/tenant/namespaces.go, main.go, pkg/utils/node_selector.go, pkg/webhook/namespace/freezed.go, pkg/webhook/namespace/owner_reference.go, pkg/webhook/namespace/patch.go, pkg/webhook/namespace/user_metadata.go, pkg/webhook/ownerreference/patching.go, pkg/webhook/tenant/cordoning.go, pkg/webhook/utils/in_capsule_groups.go, pkg/webhook/utils/is_capsule_user.go
Commit date: 2022-12-02 15:19:06 +0100
Commit message: fix: preventing serviceaccount privilege escalation

Diff: diff --git a/controllers/tenant/namespaces.go b/controllers/tenant/namespaces.go
index 00b2f64..73e2db3 100644
--- a/controllers/tenant/namespaces.go
+++ b/controllers/tenant/namespaces.go
@@ -15,10 +15,11 @@ import (
 	"k8s.io/client-go/util/retry"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
+	"github.com/clastix/capsule/pkg/utils"
 )
 
 // Ensuring all annotations are applied to each Namespace handled by the Tenant.
 func (r *Manager) syncNamespaces(ctx context.Context, tenant *capsulev1beta1.Tenant) (err error) {
 	group := new(errgroup.Group)
@@ -70,15 +71,11 @@ func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, t
 					labels[k] = v
 				}
 			}
 
 			if tnt.Spec.NodeSelector != nil {
-				var selector []string
-				for k, v := range tnt.Spec.NodeSelector {
-					selector = append(selector, fmt.Sprintf("%s=%s", k, v))
-				}
-				annotations["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
+				annotations = utils.BuildNodeSelector(tnt, annotations)
 			}
 
 			if tnt.Spec.IngressOptions.AllowedClasses != nil {
 				if len(tnt.Spec.IngressOptions.AllowedClasses.Exact) > 0 {
 					annotations[capsulev1beta1.AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressOptions.AllowedClasses.Exact, ",")
diff --git a/main.go b/main.go
index 7e89b52..1bba13f 100644
--- a/main.go
+++ b/main.go
@@ -203,11 +203,11 @@ func main() {
 		route.Ingress(ingress.Class(cfg), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
 		route.PVC(pvc.Handler()),
 		route.Service(service.Handler()),
 		route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
 		route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler()),
-		route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
+		route.OwnerReference(utils.InCapsuleGroups(cfg, namespacewebhook.OwnerReferenceHandler(), ownerreference.Handler(cfg))),
 		route.Cordoning(tenant.CordoningHandler(cfg), tenant.ResourceCounterHandler()),
 		route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
 	)
 
 	nodeWebhookSupported, _ := utils.NodeWebhookSupported(kubeVersion)
diff --git a/pkg/utils/node_selector.go b/pkg/utils/node_selector.go
new file mode 100644
index 0000000..d7160e8
--- /dev/null
+++ b/pkg/utils/node_selector.go
@@ -0,0 +1,35 @@
+// Copyright 2020-2021 Clastix Labs
+// SPDX-License-Identifier: Apache-2.0
+
+package utils
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
+)
+
+const (
+	NodeSelectorAnnotation = "scheduler.alpha.kubernetes.io/node-selector"
+)
+
+func BuildNodeSelector(tnt *capsulev1beta1.Tenant, nsAnnotations map[string]string) map[string]string {
+	if nsAnnotations == nil {
+		nsAnnotations = make(map[string]string)
+	}
+
+	selector := make([]string, 0, len(tnt.Spec.NodeSelector))
+
+	for k, v := range tnt.Spec.NodeSelector {
+		selector = append(selector, fmt.Sprintf("%s=%s", k, v))
+	}
+	// Sorting the resulting slice: iterating over maps is randomized, and we could end-up
+	// in multiple reconciliations upon multiple node selectors.
+	sort.Strings(selector)
+
+	nsAnnotations[NodeSelectorAnnotation] = strings.Join(selector, ",")
+
+	return nsAnnotations
+}
diff --git a/pkg/webhook/namespace/freezed.go b/pkg/webhook/namespace/freezed.go
index edda21e..290217a 100644
--- a/pkg/webhook/namespace/freezed.go
+++ b/pkg/webhook/namespace/freezed.go
@@ -67,11 +67,11 @@ func (r *freezedHandler) OnDelete(c client.Client, _ *admission.Decoder, recorde
 			return nil
 		}
 
 		tnt := tntList.Items[0]
 
-		if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
+		if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
 			recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be deleted, the current Tenant is freezed", req.Name)
 
 			response := admission.Denied("the selected Tenant is freezed")
 
 			return &response
@@ -99,11 +99,11 @@ func (r *freezedHandler) OnUpdate(c client.Client, decoder *admission.Decoder, r
 			return nil
 		}
 
 		tnt := tntList.Items[0]
 
-		if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
+		if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
 			recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be updated, the current Tenant is freezed", ns.GetName())
 
 			response := admission.Denied("the selected Tenant is freezed")
 
 			return &response
diff --git a/pkg/webhook/namespace/owner_reference.go b/pkg/webhook/namespace/owner_reference.go
new file mode 100644
index 0000000..b38dd40
--- /dev/null
+++ b/pkg/webhook/namespace/owner_reference.go
@@ -0,0 +1,64 @@
+// Copyright 2020-2021 Clastix Labs
+// SPDX-License-Identifier: Apache-2.0
+
+package namespace
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/tools/record"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	capsulewebhook "github.com/clastix/capsule/pkg/webhook"
+	"github.com/clastix/capsule/pkg/webhook/utils"
+)
+
+type ownerReferenceHandler struct{}
+
+func OwnerReferenceHandler() capsulewebhook.Handler {
+	return &ownerReferenceHandler{}
+}
+
+func (r *ownerReferenceHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
+	return func(ctx context.Context, req admission.Request) *admission.Response {
+		return nil
+	}
+}
+
+func (r *ownerReferenceHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
+	return func(ctx context.Context, req admission.Request) *admission.Response {
+		return nil
+	}
+}
+
+func (r *ownerReferenceHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
+	return func(ctx context.Context, req admission.Request) *admission.Response {
+		oldNs := &corev1.Namespace{}
+		if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
+			return utils.ErroredResponse(err)
+		}
+
+		newNs := &corev1.Namespace{}
+		if err := decoder.Decode(req, newNs); err != nil {
+			return utils.ErroredResponse(err)
+		}
+
+		if len(newNs.OwnerReferences) == 0 {
+			response := admission.Errored(http.StatusBadRequest, fmt.Errorf("the OwnerReference cannot be removed"))
+
+			return &response
+		}
+
+		if oldNs.GetOwnerReferences()[0].UID != newNs.GetOwnerReferences()[0].UID {
+			response := admission.Errored(http.StatusBadRequest, fmt.Errorf("the OwnerReference cannot be changed"))
+
+			return &response
+		}
+
+		return nil
+	}
+}
diff --git a/pkg/webhook/namespace/patch.go b/pkg/webhook/namespace/patch.go
index 47bc0c3..13a289e 100644
--- a/pkg/webhook/namespace/patch.go
+++ b/pkg/webhook/namespace/patch.go
@@ -1,7 +1,8 @@
 // Copyright 2020-2021 Clastix Labs
 // SPDX-License-Identifier: Apache-2.0
+
 package namespace
 
 import (
 	"context"
 	"fmt"
diff --git a/pkg/webhook/namespace/user_metadata.go b/pkg/webhook/namespace/user_metadata.go
index d3ea419..ac34be4 100644
--- a/pkg/webhook/namespace/user_metadata.go
+++ b/pkg/webhook/namespace/user_metadata.go
@@ -110,10 +110,29 @@ func (r *userMetadataHandler) OnUpdate(client client.Client, decoder *admission.
 			if err := client.Get(ctx, types.NamespacedName{Name: objectRef.Name}, tnt); err != nil {
 				return utils.ErroredResponse(err)
 			}
 		}
 
+		if len(tnt.Spec.NodeSelector) > 0 {
+			v, ok := newNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"]
+			if !ok {
+				response := admission.Denied("the node-selector annotation is enforced, cannot be removed")
+
+				recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodeSelectorDeletion", string(response.Result.Reason))
+
+				return &response
+			}
+
+			if v != oldNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"] {
+				response := admission.Denied("the the node-selector annotation is enforced, cannot be updated")
+
+				recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodeSelectorUpdate", string(response.Result.Reason))
+
+				return &response
+			}
+		}
+
 		var labels, annotations map[string]string
 
 		for key, value := range newNs.GetLabels() {
 			if _, ok := oldNs.GetLabels()[key]; ok {
 				if labels == nil {
diff --git a/pkg/webhook/ownerreference/patching.go b/pkg/webhook/ownerreference/patching.go
index ac465eb..df7f963 100644
--- a/pkg/webhook/ownerreference/patching.go
+++ b/pkg/webhook/ownerreference/patching.go
@@ -47,11 +47,11 @@ func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, rec
 	}
 }
 
 func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) *admission.Response {
-		return h.setOwnerRef(ctx, req, client, decoder, recorder)
+		return nil
 	}
 }
 
 func (h *handler) setOwnerRef(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
 	ns := &corev1.Namespace{}
diff --git a/pkg/webhook/tenant/cordoning.go b/pkg/webhook/tenant/cordoning.go
index f1b4ebd..cc024ac 100644
--- a/pkg/webhook/tenant/cordoning.go
+++ b/pkg/webhook/tenant/cordoning.go
@@ -42,11 +42,11 @@ func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client,
 	if len(tntList.Items) == 0 {
 		return nil
 	}
 
 	tnt := tntList.Items[0]
-	if tnt.IsCordoned() && utils.IsCapsuleUser(req, h.configuration.UserGroups()) {
+	if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, clt, h.configuration.UserGroups()) {
 		recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "%s %s/%s cannot be %sd, current Tenant is freezed", req.Kind.String(), req.Namespace, req.Name, strings.ToLower(string(req.Operation)))
 
 		response := admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName()))
 
 		return &response
diff --git a/pkg/webhook/utils/in_capsule_groups.go b/pkg/webhook/utils/in_capsule_groups.go
index 6278e0b..9089cec 100644
--- a/pkg/webhook/utils/in_capsule_groups.go
+++ b/pkg/webhook/utils/in_capsule_groups.go
@@ -26,11 +26,11 @@ type handler struct {
 	handlers      []webhook.Handler
 }
 
 func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
 	return func(ctx context.Context, req admission.Request) *admission.Response {
-		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
+		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
 			return nil
 		}
 
 		for _, hndl := range h.handlers {
 			if response := hndl.OnCreate(client, decoder, recorder)(ctx, req); response != nil {
@@ -42,11 +42,11 @@ func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, rec
 	}
 }
 
 func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
 	return func(ctx context.Context, req admission.Request) *admission.Response {
-		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
+		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
 			return nil
 		}
 
 		for _, hndl := range h.handlers {
 			if response := hndl.OnDelete(client, decoder, recorder)(ctx, req); response != nil {
@@ -58,11 +58,11 @@ func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, rec
 	}
 }
 
 func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
 	return func(ctx context.Context, req admission.Request) *admission.Response {
-		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
+		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
 			return nil
 		}
 
 		for _, hndl := range h.handlers {
 			if response := hndl.OnUpdate(client, decoder, recorder)(ctx, req); response != nil {
diff --git a/pkg/webhook/utils/is_capsule_user.go b/pkg/webhook/utils/is_capsule_user.go
index 0acb429..5c16307 100644
--- a/pkg/webhook/utils/is_capsule_user.go
+++ b/pkg/webhook/utils/is_capsule_user.go
@@ -1,21 +1,45 @@
 package utils
 
 import (
+	"context"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
+	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
 	"github.com/clastix/capsule/pkg/utils"
 )
 
-func IsCapsuleUser(req admission.Request, userGroups []string) bool {
+func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client, userGroups []string) bool {
 	groupList := utils.NewUserGroupList(req.UserInfo.Groups)
 	// if the user is a ServiceAccount belonging to the kube-system namespace, definitely, it's not a Capsule user
 	// and we can skip the check in case of Capsule user group assigned to system:authenticated
 	// (ref: https://github.com/clastix/capsule/issues/234)
 	if groupList.Find("system:serviceaccounts:kube-system") {
 		return false
 	}
+	// nolint:nestif
+	if sets.NewString(req.UserInfo.Groups...).Has("system:serviceaccounts") {
+		parts := strings.Split(req.UserInfo.Username, ":")
+
+		targetNamespace := parts[2]
+
+		if len(targetNamespace) > 0 {
+			tl := &capsulev1beta1.TenantList{}
+			if err := clt.List(ctx, tl, client.MatchingFieldsSelector{Selector: fields.OneTermEqualSelector(".status.namespaces", targetNamespace)}); err != nil {
+				return false
+			}
+
+			if len(tl.Items) == 1 {
+				return true
+			}
+		}
+	}
 
 	for _, group := range userGroups {
 		if groupList.Find(group) {
 			return true
 		}

Vulnerable code: 				var selector []string
				for k, v := range tnt.Spec.NodeSelector {
					selector = append(selector, fmt.Sprintf("%s=%s", k, v))
				}
				annotations["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
		route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
		if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
		if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
		return h.setOwnerRef(ctx, req, client, decoder, recorder)
	if tnt.IsCordoned() && utils.IsCapsuleUser(req, h.configuration.UserGroups()) {
		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
		if !IsCapsuleUser(req, h.configuration.UserGroups()) {
func IsCapsuleUser(req admission.Request, userGroups []string) bool {
Fixed code: 	"github.com/clastix/capsule/pkg/utils"
				annotations = utils.BuildNodeSelector(tnt, annotations)
		route.OwnerReference(utils.InCapsuleGroups(cfg, namespacewebhook.OwnerReferenceHandler(), ownerreference.Handler(cfg))),
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package utils

import (
	"fmt"
	"sort"
	"strings"

	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)

const (
	NodeSelectorAnnotation = "scheduler.alpha.kubernetes.io/node-selector"
)

func BuildNodeSelector(tnt *capsulev1beta1.Tenant, nsAnnotations map[string]string) map[string]string {
	if nsAnnotations == nil {
		nsAnnotations = make(map[string]string)
	}

	selector := make([]string, 0, len(tnt.Spec.NodeSelector))

	for k, v := range tnt.Spec.NodeSelector {
		selector = append(selector, fmt.Sprintf("%s=%s", k, v))
	}
	// Sorting the resulting slice: iterating over maps is randomized, and we could end-up
	// in multiple reconciliations upon multiple node selectors.
	sort.Strings(selector)

	nsAnnotations[NodeSelectorAnnotation] = strings.Join(selector, ",")

	return nsAnnotations
}
		if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
		if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package namespace

import (
	"context"
	"fmt"
	"net/http"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	capsulewebhook "github.com/clastix/capsule/pkg/webhook"
	"github.com/clastix/capsule/pkg/webhook/utils"
)

type ownerReferenceHandler struct{}

func OwnerReferenceHandler() capsulewebhook.Handler {
	return &ownerReferenceHandler{}
}

func (r *ownerReferenceHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		return nil
	}
}

func (r *ownerReferenceHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		return nil
	}
}

func (r *ownerReferenceHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		oldNs := &corev1.Namespace{}
		if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
			return utils.ErroredResponse(err)
		}

		newNs := &corev1.Namespace{}
		if err := decoder.Decode(req, newNs); err != nil {
			return utils.ErroredResponse(err)
		}

		if len(newNs.OwnerReferences) == 0 {
			response := admission.Errored(http.StatusBadRequest, fmt.Errorf("the OwnerReference cannot be removed"))

			return &response
		}

		if oldNs.GetOwnerReferences()[0].UID != newNs.GetOwnerReferences()[0].UID {
			response := admission.Errored(http.StatusBadRequest, fmt.Errorf("the OwnerReference cannot be changed"))

			return &response
		}

		return nil
	}
}

		if len(tnt.Spec.NodeSelector) > 0 {
			v, ok := newNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"]
			if !ok {
				response := admission.Denied("the node-selector annotation is enforced, cannot be removed")

				recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodeSelectorDeletion", string(response.Result.Reason))

				return &response
			}

			if v != oldNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"] {
				response := admission.Denied("the the node-selector annotation is enforced, cannot be updated")

				recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodeSelectorUpdate", string(response.Result.Reason))

				return &response
			}
		}

		return nil
	if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, clt, h.configuration.UserGroups()) {
		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
		if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
	"context"
	"strings"

	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/util/sets"
	"sigs.k8s.io/controller-runtime/pkg/client"
	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client, userGroups []string) bool {
	// nolint:nestif
	if sets.NewString(req.UserInfo.Groups...).Has("system:serviceaccounts") {
		parts := strings.Split(req.UserInfo.Username, ":")

		targetNamespace := parts[2]

		if len(targetNamespace) > 0 {
			tl := &capsulev1beta1.TenantList{}
			if err := clt.List(ctx, tl, client.MatchingFieldsSelector{Selector: fields.OneTermEqualSelector(".status.namespaces", targetNamespace)}); err != nil {
				return false
			}

			if len(tl.Items) == 1 {
				return true
			}
		}
	}