mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-22 22:16:46 +00:00
* 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>
195 lines
5.4 KiB
Go
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{})
|
|
}
|