Files
Oliver BählerandGitHub 9ee5468f47 feat: add rolebindings to rules api (#2032)
* feat: add rolebindings to rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add rolebindings to rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add rolebindings to rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add rolebindings to rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add rolebindings to rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: implement review

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-07-15 10:35:36 +02:00

195 lines
5.4 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package tenant
import (
"context"
"fmt"
"maps"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/utils"
)
// Sync the dynamic Tenant Owner specific cluster-roles and additional Role Bindings, which can be used in many ways:
// applying Pod Security Policies or giving access to CRDs or specific API groups.
func (r *Manager) syncRoleBindings(ctx context.Context, log logr.Logger, tenant *capsulev1beta2.Tenant) (err error) {
namespaceBindings := map[string]map[string]rbac.AdditionalRoleBindingsSpec{}
for _, ns := range tenant.Status.Spaces {
namespace := ns.Name
if _, ok := namespaceBindings[namespace]; !ok {
namespaceBindings[namespace] = map[string]rbac.AdditionalRoleBindingsSpec{}
}
}
for _, binding := range tenant.GetRoleBindings() {
hash := utils.RoleBindingHashFunc(binding)
for namespace := range namespaceBindings {
namespaceBindings[namespace][hash] = binding
}
}
nsCache := make(map[string]*corev1.Namespace, len(namespaceBindings))
for i, rule := range tenant.Spec.Rules {
if rule == nil || len(rule.Permissions.Bindings) == 0 {
continue
}
// A rule without a selector applies to every tenant namespace and does not
// require resolving Namespace objects.
if rule.NamespaceSelector == nil {
for namespace := range namespaceBindings {
for _, binding := range rule.Permissions.Bindings {
hash := utils.RoleBindingHashFunc(binding)
namespaceBindings[namespace][hash] = binding
}
}
continue
}
for namespace := range namespaceBindings {
ns, ok := nsCache[namespace]
if !ok {
ns = &corev1.Namespace{}
if err := r.Get(ctx, client.ObjectKey{Name: namespace}, ns); err != nil {
if apierrors.IsNotFound(err) {
// Cache missing namespaces as well to avoid repeating the GET for
// subsequent selector-based rules in this reconciliation.
nsCache[namespace] = nil
continue
}
return fmt.Errorf("get namespace %q for rules[%d]: %w", namespace, i, err)
}
nsCache[namespace] = ns
}
if ns == nil {
continue
}
matches, err := utils.IsNamespaceSelectedBySelector(ns, rule.NamespaceSelector)
if err != nil {
return fmt.Errorf("invalid namespaceSelector in rules[%d]: %w", i, err)
}
if !matches {
continue
}
for _, binding := range rule.Permissions.Bindings {
hash := utils.RoleBindingHashFunc(binding)
namespaceBindings[namespace][hash] = binding
}
}
}
// Does not target all namespaces
for _, promotion := range tenant.GetPromotionRoleBindings() {
namespace := string(promotion.Namespace)
if _, ok := namespaceBindings[namespace]; !ok {
// Ignore namespaces that are not part of the tenant.
continue
}
binding := promotion.AdditionalRoleBindingsSpec
hash := utils.RoleBindingHashFunc(binding)
namespaceBindings[namespace][hash] = binding
}
return runForTenantNamespaces(ctx, tenant, func(ctx context.Context, namespace string) error {
return r.syncAdditionalRoleBinding(ctx, log, tenant, namespace, namespaceBindings[namespace])
})
}
func (r *Manager) syncAdditionalRoleBinding(
ctx context.Context,
log logr.Logger,
tenant *capsulev1beta2.Tenant,
ns string,
bindings map[string]rbac.AdditionalRoleBindingsSpec,
) (err error) {
keys := []string{}
for hash, roleBinding := range bindings {
name := meta.NameForManagedRoleBindings(hash)
keys = append(keys, hash)
target := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() error {
target.Labels = map[string]string{}
target.Annotations = map[string]string{}
maps.Copy(target.Labels, roleBinding.Labels)
target.Labels[meta.NewTenantLabel] = tenant.Name
target.Labels[meta.RolebindingLabel] = hash
target.Labels[meta.NewManagedByCapsuleLabel] = meta.ValueController
// Remove Legacy labels
delete(target.Labels, meta.TenantLabel)
maps.Copy(target.Annotations, roleBinding.Annotations)
target.RoleRef = rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: roleBinding.ClusterRoleName,
}
target.Subjects = roleBinding.Subjects
return controllerutil.SetControllerReference(tenant, target, r.Scheme())
})
if err != nil {
if apierrors.HasStatusCause(err, corev1.NamespaceTerminatingCause) {
log.V(4).Info(
"skipping RoleBinding sync because namespace is terminating",
"name", target.Name,
"namespace", target.Namespace,
"clusterRole", roleBinding.ClusterRoleName,
)
continue
}
return fmt.Errorf("%w (role: %s)", err, roleBinding.ClusterRoleName)
}
log.V(4).Info(fmt.Sprintf("roleBinding sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
}
// Prune at finish to prevent gaps
return r.pruningResources(ctx, ns, keys, &rbacv1.RoleBinding{})
}