feat(config): add usernames property identify specific users as capsule users (#1606)

* feat(config): add usernames property identify specific users as capsule users

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(helm): improve admission configurations

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(helm): improve admission configurations

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(config): add usernames property identify specific users as capsule users

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(config): add usernames property identify specific users as capsule users

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
Oliver Bähler
2025-08-22 15:03:50 +02:00
committed by GitHub
parent 8ba8aa7ecc
commit cb029a1d70
16 changed files with 94 additions and 145 deletions
+3 -1
View File
@@ -11,7 +11,9 @@ import (
// CapsuleConfigurationSpec defines the Capsule configuration.
type CapsuleConfigurationSpec struct {
// Names of the groups for Capsule users.
// Names of the users considered as Capsule users.
UserNames []string `json:"userNames,omitempty"`
// Names of the groups considered as Capsule users.
// +kubebuilder:default={capsule.clastix.io}
UserGroups []string `json:"userGroups,omitempty"`
// Define groups which when found in the request of a user will be ignored by the Capsule
+5
View File
@@ -117,6 +117,11 @@ func (in *CapsuleConfigurationList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CapsuleConfigurationSpec) DeepCopyInto(out *CapsuleConfigurationSpec) {
*out = *in
if in.UserNames != nil {
in, out := &in.UserNames, &out.UserNames
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.UserGroups != nil {
in, out := &in.UserGroups, &out.UserGroups
*out = make([]string, len(*in))
+3 -67
View File
@@ -2,20 +2,6 @@
Use the Capsule Operator for easily implementing, managing, and maintaining multitenancy and access control in Kubernetes.
## Requirements
* [Helm 3](https://github.com/helm/helm/releases) is required when installing the Capsule Operator chart. Follow Helms official [steps](https://helm.sh/docs/intro/install/) for installing helm on your particular operating system.
* A Kubernetes cluster 1.16+ with following [Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) enabled:
* PodNodeSelector
* LimitRanger
* ResourceQuota
* MutatingAdmissionWebhook
* ValidatingAdmissionWebhook
* A [`kubeconfig`](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) file accessing the Kubernetes cluster with cluster admin permissions.
## Major Changes
In the following sections you see actions which are required when you are upgrading to a specific version.
@@ -33,58 +19,7 @@ The following Values have changed key or Value:
* `mutatingWebhooksTimeoutSeconds` has moved to `webhooks.mutatingWebhooksTimeoutSeconds`
* `validatingWebhooksTimeoutSeconds` has moved to `webhooks.validatingWebhooksTimeoutSeconds`
## Installation
**When using OCI we recommend our dedicated [OCI Repository](https://artifacthub.io/packages/helm/capsule/capsule) for this chart**
The Capsule Operator requires it's CRDs to be installed before the operator itself. Since the Helm CRD lifecycle has limitations, we recommend to install the CRDs separately. Our chart supports the installation of crds via a dedicated Release.
The Capsule Operator Chart can be used to instantly deploy the Capsule Operator on your Kubernetes cluster.
1. Add this repository:
$ helm repo add projectcapsule https://projectcapsule.github.io/charts
2. Install Capsule:
$ helm install capsule projectcapsule/capsule --version 0.7.0 -n capsule-system --create-namespace
or
$ helm install capsule oci://ghcr.io/projectcapsule/charts/capsule --version 0.7.0 -n capsule-system --create-namespace
3. Show the status:
$ helm status capsule -n capsule-system
4. Upgrade the Chart
$ helm upgrade capsule projectcapsule/capsule -n capsule-system
or
$ helm upgrade capsule oci://ghcr.io/projectcapsule/charts/capsule --version 0.4.7
5. Uninstall the Chart
$ helm uninstall capsule -n capsule-system
## Customize the installation
There are two methods for specifying overrides of values during chart installation: `--values` and `--set`.
The `--values` option is the preferred method because it allows you to keep your overrides in a YAML file, rather than specifying them all on the command line. Create a copy of the YAML file `values.yaml` and add your overrides to it.
Specify your overrides file when you install the chart:
$ helm install capsule capsule-helm-chart --values myvalues.yaml -n capsule-system
The values in your overrides file `myvalues.yaml` will override their counterparts in the chart's values.yaml file. Any values in `values.yaml` that werent overridden will keep their defaults.
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
$ helm install capsule capsule-helm-chart --set manager.options.forceTenantPrefix=false -n capsule-system
Here the values you can override:
## Values
### CustomResourceDefinition Lifecycle
@@ -162,13 +97,14 @@ Here the values you can override:
| manager.kind | string | `"Deployment"` | Set the controller deployment mode as `Deployment` or `DaemonSet`. |
| manager.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":10080}}` | Configure the liveness probe using Deployment probe spec |
| manager.options.capsuleConfiguration | string | `"default"` | Change the default name of the capsule configuration name |
| manager.options.capsuleUserGroups | list | `["projectcapsule.dev"]` | Override the Capsule user groups |
| manager.options.capsuleUserGroups | list | `["projectcapsule.dev"]` | Names of the groups considered as Capsule users. |
| manager.options.forceTenantPrefix | bool | `false` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash |
| manager.options.generateCertificates | bool | `true` | Specifies whether capsule webhooks certificates should be generated by capsule operator |
| manager.options.ignoreUserWithGroups | list | `[]` | Define groups which when found in the request of a user will be ignored by the Capsule this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups. |
| manager.options.logLevel | string | `"4"` | Set the log verbosity of the capsule with a value from 1 to 10 |
| manager.options.nodeMetadata | object | `{"forbiddenAnnotations":{"denied":[],"deniedRegex":""},"forbiddenLabels":{"denied":[],"deniedRegex":""}}` | Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant |
| manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp |
| manager.options.userNames | list | `[]` | Names of the users considered as Capsule users. |
| manager.rbac.create | bool | `true` | Specifies whether RBAC resources should be created. |
| manager.rbac.existingClusterRoles | list | `[]` | Specifies further cluster roles to be added to the Capsule manager service account. |
| manager.rbac.existingRoles | list | `[]` | Specifies further cluster roles to be added to the Capsule manager service account. |
+1 -67
View File
@@ -2,20 +2,6 @@
Use the Capsule Operator for easily implementing, managing, and maintaining multitenancy and access control in Kubernetes.
## Requirements
* [Helm 3](https://github.com/helm/helm/releases) is required when installing the Capsule Operator chart. Follow Helms official [steps](https://helm.sh/docs/intro/install/) for installing helm on your particular operating system.
* A Kubernetes cluster 1.16+ with following [Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) enabled:
* PodNodeSelector
* LimitRanger
* ResourceQuota
* MutatingAdmissionWebhook
* ValidatingAdmissionWebhook
* A [`kubeconfig`](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) file accessing the Kubernetes cluster with cluster admin permissions.
## Major Changes
In the following sections you see actions which are required when you are upgrading to a specific version.
@@ -33,59 +19,7 @@ The following Values have changed key or Value:
* `mutatingWebhooksTimeoutSeconds` has moved to `webhooks.mutatingWebhooksTimeoutSeconds`
* `validatingWebhooksTimeoutSeconds` has moved to `webhooks.validatingWebhooksTimeoutSeconds`
## Installation
**When using OCI we recommend our dedicated [OCI Repository](https://artifacthub.io/packages/helm/capsule/capsule) for this chart**
The Capsule Operator requires it's CRDs to be installed before the operator itself. Since the Helm CRD lifecycle has limitations, we recommend to install the CRDs separately. Our chart supports the installation of crds via a dedicated Release.
The Capsule Operator Chart can be used to instantly deploy the Capsule Operator on your Kubernetes cluster.
1. Add this repository:
$ helm repo add projectcapsule https://projectcapsule.github.io/charts
2. Install Capsule:
$ helm install capsule projectcapsule/capsule --version 0.7.0 -n capsule-system --create-namespace
or
$ helm install capsule oci://ghcr.io/projectcapsule/charts/capsule --version 0.7.0 -n capsule-system --create-namespace
3. Show the status:
$ helm status capsule -n capsule-system
4. Upgrade the Chart
$ helm upgrade capsule projectcapsule/capsule -n capsule-system
or
$ helm upgrade capsule oci://ghcr.io/projectcapsule/charts/capsule --version 0.4.7
5. Uninstall the Chart
$ helm uninstall capsule -n capsule-system
## Customize the installation
There are two methods for specifying overrides of values during chart installation: `--values` and `--set`.
The `--values` option is the preferred method because it allows you to keep your overrides in a YAML file, rather than specifying them all on the command line. Create a copy of the YAML file `values.yaml` and add your overrides to it.
Specify your overrides file when you install the chart:
$ helm install capsule capsule-helm-chart --values myvalues.yaml -n capsule-system
The values in your overrides file `myvalues.yaml` will override their counterparts in the chart's values.yaml file. Any values in `values.yaml` that werent overridden will keep their defaults.
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
$ helm install capsule capsule-helm-chart --set manager.options.forceTenantPrefix=false -n capsule-system
Here the values you can override:
## Values
### CustomResourceDefinition Lifecycle
@@ -127,7 +127,12 @@ spec:
userGroups:
default:
- capsule.clastix.io
description: Names of the groups for Capsule users.
description: Names of the groups considered as Capsule users.
items:
type: string
type: array
userNames:
description: Names of the users considered as Capsule users.
items:
type: string
type: array
@@ -20,6 +20,10 @@ spec:
{{- range .Values.manager.options.capsuleUserGroups }}
- {{ . }}
{{- end }}
userNames:
{{- range .Values.manager.options.userNames }}
- {{ . }}
{{- end }}
ignoreUserWithGroups:
{{- range .Values.manager.options.ignoreUserWithGroups }}
- {{ . }}
+5 -1
View File
@@ -249,7 +249,7 @@
"type": "string"
},
"capsuleUserGroups": {
"description": "Override the Capsule user groups",
"description": "Names of the groups considered as Capsule users.",
"type": "array",
"items": {
"type": "string"
@@ -302,6 +302,10 @@
"protectedNamespaceRegex": {
"description": "If specified, disallows creation of namespaces matching the passed regexp",
"type": "string"
},
"userNames": {
"description": "Names of the users considered as Capsule users.",
"type": "array"
}
}
},
+3 -1
View File
@@ -141,7 +141,9 @@ manager:
logLevel: '4'
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
forceTenantPrefix: false
# -- Override the Capsule user groups
# -- Names of the users considered as Capsule users.
userNames: []
# -- Names of the groups considered as Capsule users.
capsuleUserGroups: ["projectcapsule.dev"]
# -- Define groups which when found in the request of a user will be ignored by the Capsule
# this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
+7
View File
@@ -104,6 +104,13 @@ func (r *Manager) EnsureClusterRoleBindings(ctx context.Context) (err error) {
})
}
for _, user := range r.Configuration.UserNames() {
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
Kind: "User",
Name: user,
})
}
return
})
+40
View File
@@ -27,6 +27,10 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
Name: "alice",
Kind: "User",
},
{
Name: "bob",
Kind: "User",
},
},
},
}
@@ -96,4 +100,40 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
})
It("should succeed and be available in Tenant namespaces list with default single user", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{}
configuration.Spec.IgnoreUserWithGroups = []string{}
configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name}
})
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
})
It("should succeed and be available in Tenant namespaces list with default single user", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{}
configuration.Spec.IgnoreUserWithGroups = []string{}
configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name}
})
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[1], defaultTimeoutInterval).ShouldNot(Succeed())
})
It("should fail when group is ignored", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{}
configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name}
configuration.Spec.IgnoreUserWithGroups = []string{"projectcapsule.dev"}
})
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
})
})
+4
View File
@@ -85,6 +85,10 @@ func (c *capsuleConfiguration) UserGroups() []string {
return c.retrievalFn().Spec.UserGroups
}
func (c *capsuleConfiguration) UserNames() []string {
return c.retrievalFn().Spec.UserNames
}
func (c *capsuleConfiguration) IgnoreUserWithGroups() []string {
return c.retrievalFn().Spec.IgnoreUserWithGroups
}
+1
View File
@@ -23,6 +23,7 @@ type Configuration interface {
MutatingWebhookConfigurationName() string
ValidatingWebhookConfigurationName() string
TenantCRDName() string
UserNames() []string
UserGroups() []string
IgnoreUserWithGroups() []string
ForbiddenUserNodeLabels() *capsuleapi.ForbiddenListSpec
+2 -2
View File
@@ -74,7 +74,7 @@ func (r *freezedHandler) OnDelete(c client.Client, _ admission.Decoder, recorder
tnt := tntList.Items[0]
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups(), r.configuration.IgnoreUserWithGroups()) {
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserNames(), r.configuration.UserGroups(), r.configuration.IgnoreUserWithGroups()) {
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")
@@ -106,7 +106,7 @@ func (r *freezedHandler) OnUpdate(c client.Client, decoder admission.Decoder, re
tnt := tntList.Items[0]
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups(), r.configuration.IgnoreUserWithGroups()) {
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserNames(), r.configuration.UserGroups(), r.configuration.IgnoreUserWithGroups()) {
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")
+1 -1
View File
@@ -62,7 +62,7 @@ func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client,
}
tnt := tntList.Items[0]
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, clt, h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, clt, h.configuration.UserNames(), h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
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()))
+3 -3
View File
@@ -29,7 +29,7 @@ type handler struct {
//nolint:dupl
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(ctx, req, client, h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
if !IsCapsuleUser(ctx, req, client, h.configuration.UserNames(), h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
return nil
}
@@ -46,7 +46,7 @@ func (h *handler) OnCreate(client client.Client, decoder admission.Decoder, reco
//nolint:dupl
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(ctx, req, client, h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
if !IsCapsuleUser(ctx, req, client, h.configuration.UserNames(), h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
return nil
}
@@ -63,7 +63,7 @@ func (h *handler) OnDelete(client client.Client, decoder admission.Decoder, reco
//nolint:dupl
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(ctx, req, client, h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
if !IsCapsuleUser(ctx, req, client, h.configuration.UserNames(), h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
return nil
}
+6 -1
View File
@@ -16,7 +16,7 @@ import (
"github.com/projectcapsule/capsule/pkg/utils"
)
func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client, userGroups []string, ignoreGroups []string) bool {
func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client, users []string, userGroups []string, ignoreGroups []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
@@ -24,6 +24,7 @@ func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client
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, ":")
@@ -56,5 +57,9 @@ func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client
}
}
if len(users) > 0 && sets.New[string](users...).Has(req.UserInfo.Username) {
return true
}
return false
}