From f82c2f468bcee5881849edbe99d5933307433d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20B=C3=A4hler?= Date: Wed, 23 Oct 2024 11:17:23 +0200 Subject: [PATCH] feat(api): add tenant funcs to retrieve subjects based on clusterrole bindings (#1231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Bähler --- api/v1beta2/tenant_func.go | 129 +++++++++++++++++++++ api/v1beta2/tenant_func_test.go | 192 ++++++++++++++++++++++++++++++++ pkg/api/tenant_roles.go | 12 ++ 3 files changed, 333 insertions(+) create mode 100644 api/v1beta2/tenant_func_test.go create mode 100644 pkg/api/tenant_roles.go diff --git a/api/v1beta2/tenant_func.go b/api/v1beta2/tenant_func.go index 08754ad9..9c7932ce 100644 --- a/api/v1beta2/tenant_func.go +++ b/api/v1beta2/tenant_func.go @@ -4,9 +4,13 @@ package v1beta2 import ( + "slices" "sort" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + + "github.com/projectcapsule/capsule/pkg/api" ) func (in *Tenant) IsFull() bool { @@ -36,3 +40,128 @@ func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) { func (in *Tenant) GetOwnerProxySettings(name string, kind OwnerKind) []ProxySettings { return in.Spec.Owners.FindOwner(name, kind).ProxyOperations } + +// GetClusterRolePermissions returns a map where the clusterRole is the key +// and the value is a list of permission subjects (kind and name) that reference that role. +// These mappings are gathered from the owners and additionalRolebindings spec. +func (in *Tenant) GetSubjectsByClusterRoles(ignoreOwnerKind []OwnerKind) (rolePerms map[string][]rbacv1.Subject) { + rolePerms = make(map[string][]rbacv1.Subject) + + // Helper to add permissions for a given clusterRole + addPermission := func(clusterRole string, permission rbacv1.Subject) { + if _, exists := rolePerms[clusterRole]; !exists { + rolePerms[clusterRole] = []rbacv1.Subject{} + } + + rolePerms[clusterRole] = append(rolePerms[clusterRole], permission) + } + + // Helper to check if a kind is in the ignoreOwnerKind list + isIgnoredKind := func(kind string) bool { + for _, ignored := range ignoreOwnerKind { + if kind == ignored.String() { + return true + } + } + + return false + } + + // Process owners + for _, owner := range in.Spec.Owners { + if !isIgnoredKind(owner.Kind.String()) { + for _, clusterRole := range owner.ClusterRoles { + perm := rbacv1.Subject{ + Name: owner.Name, + Kind: owner.Kind.String(), + } + addPermission(clusterRole, perm) + } + } + } + + // Process additional role bindings + for _, role := range in.Spec.AdditionalRoleBindings { + for _, subject := range role.Subjects { + if !isIgnoredKind(subject.Kind) { + perm := rbacv1.Subject{ + Name: subject.Name, + Kind: subject.Kind, + } + addPermission(role.ClusterRoleName, perm) + } + } + } + + return +} + +// Get the permissions for a tenant ordered by groups and users. +func (in *Tenant) GetClusterRolesBySubject(ignoreOwnerKind []OwnerKind) (maps map[string]map[string]api.TenantSubjectRoles) { + maps = make(map[string]map[string]api.TenantSubjectRoles) + + // Initialize a nested map for kind ("User", "Group") and name + initNestedMap := func(kind string) { + if _, exists := maps[kind]; !exists { + maps[kind] = make(map[string]api.TenantSubjectRoles) + } + } + // Helper to check if a kind is in the ignoreOwnerKind list + isIgnoredKind := func(kind string) bool { + for _, ignored := range ignoreOwnerKind { + if kind == ignored.String() { + return true + } + } + + return false + } + + // Process owners + for _, owner := range in.Spec.Owners { + if !isIgnoredKind(owner.Kind.String()) { + initNestedMap(owner.Kind.String()) + + if perm, exists := maps[owner.Kind.String()][owner.Name]; exists { + // If the permission entry already exists, append cluster roles + perm.ClusterRoles = append(perm.ClusterRoles, owner.ClusterRoles...) + maps[owner.Kind.String()][owner.Name] = perm + } else { + // Create a new permission entry + maps[owner.Kind.String()][owner.Name] = api.TenantSubjectRoles{ + ClusterRoles: owner.ClusterRoles, + } + } + } + } + + // Process additional role bindings + for _, role := range in.Spec.AdditionalRoleBindings { + for _, subject := range role.Subjects { + if !isIgnoredKind(subject.Kind) { + initNestedMap(subject.Kind) + + if perm, exists := maps[subject.Kind][subject.Name]; exists { + // If the permission entry already exists, append cluster roles + perm.ClusterRoles = append(perm.ClusterRoles, role.ClusterRoleName) + maps[subject.Kind][subject.Name] = perm + } else { + // Create a new permission entry + maps[subject.Kind][subject.Name] = api.TenantSubjectRoles{ + ClusterRoles: []string{role.ClusterRoleName}, + } + } + } + } + } + + // Remove duplicates from cluster roles in both maps + for kind, nameMap := range maps { + for name, perm := range nameMap { + perm.ClusterRoles = slices.Compact(perm.ClusterRoles) + maps[kind][name] = perm + } + } + + return maps +} diff --git a/api/v1beta2/tenant_func_test.go b/api/v1beta2/tenant_func_test.go new file mode 100644 index 00000000..5b675970 --- /dev/null +++ b/api/v1beta2/tenant_func_test.go @@ -0,0 +1,192 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package v1beta2 + +import ( + "reflect" + "testing" + + "github.com/projectcapsule/capsule/pkg/api" + rbacv1 "k8s.io/api/rbac/v1" +) + +var tenant = &Tenant{ + Spec: TenantSpec{ + Owners: []OwnerSpec{ + { + Kind: "User", + Name: "user1", + ClusterRoles: []string{"cluster-admin", "read-only"}, + }, + { + Kind: "Group", + Name: "group1", + ClusterRoles: []string{"edit"}, + }, + { + Kind: ServiceAccountOwner, + Name: "service", + ClusterRoles: []string{"read-only"}, + }, + }, + AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{ + { + ClusterRoleName: "developer", + Subjects: []rbacv1.Subject{ + {Kind: "User", Name: "user2"}, + {Kind: "Group", Name: "group1"}, + }, + }, + { + ClusterRoleName: "cluster-admin", + Subjects: []rbacv1.Subject{ + { + Kind: "User", + Name: "user3", + }, + { + Kind: "Group", + Name: "group1", + }, + }, + }, + { + ClusterRoleName: "deployer", + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: "system:serviceaccount:argocd:argo-operator", + }, + }, + }, + }, + }, +} + +// TestGetClusterRolePermissions tests the GetClusterRolePermissions function +func TestGetSubjectsByClusterRoles(t *testing.T) { + expected := map[string][]rbacv1.Subject{ + "cluster-admin": { + {Kind: "User", Name: "user1"}, + {Kind: "User", Name: "user3"}, + {Kind: "Group", Name: "group1"}, + }, + "read-only": { + {Kind: "User", Name: "user1"}, + {Kind: "ServiceAccount", Name: "service"}, + }, + "edit": { + {Kind: "Group", Name: "group1"}, + }, + "developer": { + {Kind: "User", Name: "user2"}, + {Kind: "Group", Name: "group1"}, + }, + "deployer": { + {Kind: "ServiceAccount", Name: "system:serviceaccount:argocd:argo-operator"}, + }, + } + + // Call the function to test + permissions := tenant.GetSubjectsByClusterRoles(nil) + + if !reflect.DeepEqual(permissions, expected) { + t.Errorf("Expected %v, but got %v", expected, permissions) + } + + // Ignore SubjectTypes (Ignores ServiceAccounts) + ignored := tenant.GetSubjectsByClusterRoles([]OwnerKind{"ServiceAccount"}) + expectedIgnored := map[string][]rbacv1.Subject{ + "cluster-admin": { + {Kind: "User", Name: "user1"}, + {Kind: "User", Name: "user3"}, + {Kind: "Group", Name: "group1"}, + }, + "read-only": { + {Kind: "User", Name: "user1"}, + }, + "edit": { + {Kind: "Group", Name: "group1"}, + }, + "developer": { + {Kind: "User", Name: "user2"}, + {Kind: "Group", Name: "group1"}, + }, + } + + if !reflect.DeepEqual(ignored, expectedIgnored) { + t.Errorf("Expected %v, but got %v", expectedIgnored, ignored) + } + +} + +func TestGetClusterRolesBySubject(t *testing.T) { + + expected := map[string]map[string]api.TenantSubjectRoles{ + "User": { + "user1": { + ClusterRoles: []string{"cluster-admin", "read-only"}, + }, + "user2": { + ClusterRoles: []string{"developer"}, + }, + "user3": { + ClusterRoles: []string{"cluster-admin"}, + }, + }, + "Group": { + "group1": { + ClusterRoles: []string{"edit", "developer", "cluster-admin"}, + }, + }, + "ServiceAccount": { + "service": { + ClusterRoles: []string{"read-only"}, + }, + "system:serviceaccount:argocd:argo-operator": { + ClusterRoles: []string{"deployer"}, + }, + }, + } + + permissions := tenant.GetClusterRolesBySubject(nil) + if !reflect.DeepEqual(permissions, expected) { + t.Errorf("Expected %v, but got %v", expected, permissions) + } + + delete(expected, "ServiceAccount") + ignored := tenant.GetClusterRolesBySubject([]OwnerKind{"ServiceAccount"}) + + if !reflect.DeepEqual(ignored, expected) { + t.Errorf("Expected %v, but got %v", expected, ignored) + } +} + +// Helper function to run tests +func TestMain(t *testing.M) { + t.Run() +} + +// permissionsEqual checks the equality of two TenantPermission structs. +func permissionsEqual(a, b api.TenantSubjectRoles) bool { + if a.Kind != b.Kind { + return false + } + if len(a.ClusterRoles) != len(b.ClusterRoles) { + return false + } + + // Create a map to count occurrences of cluster roles + counts := make(map[string]int) + for _, role := range a.ClusterRoles { + counts[role]++ + } + for _, role := range b.ClusterRoles { + counts[role]-- + if counts[role] < 0 { + return false // More occurrences in b than in a + } + } + return true +} diff --git a/pkg/api/tenant_roles.go b/pkg/api/tenant_roles.go new file mode 100644 index 00000000..e3bb81a4 --- /dev/null +++ b/pkg/api/tenant_roles.go @@ -0,0 +1,12 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package api + +// Type to extract all clusterroles for a subject on a tenant +// from the owner and additionalRoleBindings spec. +type TenantSubjectRoles struct { + Kind string + Name string + ClusterRoles []string +}