Skip to content

Commit f82c2f4

Browse files
feat(api): add tenant funcs to retrieve subjects based on clusterrole bindings (#1231)
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
1 parent 5143c5c commit f82c2f4

File tree

3 files changed

+333
-0
lines changed

3 files changed

+333
-0
lines changed

api/v1beta2/tenant_func.go

+129
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
package v1beta2
55

66
import (
7+
"slices"
78
"sort"
89

910
corev1 "k8s.io/api/core/v1"
11+
rbacv1 "k8s.io/api/rbac/v1"
12+
13+
"github.com/projectcapsule/capsule/pkg/api"
1014
)
1115

1216
func (in *Tenant) IsFull() bool {
@@ -36,3 +40,128 @@ func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
3640
func (in *Tenant) GetOwnerProxySettings(name string, kind OwnerKind) []ProxySettings {
3741
return in.Spec.Owners.FindOwner(name, kind).ProxyOperations
3842
}
43+
44+
// GetClusterRolePermissions returns a map where the clusterRole is the key
45+
// and the value is a list of permission subjects (kind and name) that reference that role.
46+
// These mappings are gathered from the owners and additionalRolebindings spec.
47+
func (in *Tenant) GetSubjectsByClusterRoles(ignoreOwnerKind []OwnerKind) (rolePerms map[string][]rbacv1.Subject) {
48+
rolePerms = make(map[string][]rbacv1.Subject)
49+
50+
// Helper to add permissions for a given clusterRole
51+
addPermission := func(clusterRole string, permission rbacv1.Subject) {
52+
if _, exists := rolePerms[clusterRole]; !exists {
53+
rolePerms[clusterRole] = []rbacv1.Subject{}
54+
}
55+
56+
rolePerms[clusterRole] = append(rolePerms[clusterRole], permission)
57+
}
58+
59+
// Helper to check if a kind is in the ignoreOwnerKind list
60+
isIgnoredKind := func(kind string) bool {
61+
for _, ignored := range ignoreOwnerKind {
62+
if kind == ignored.String() {
63+
return true
64+
}
65+
}
66+
67+
return false
68+
}
69+
70+
// Process owners
71+
for _, owner := range in.Spec.Owners {
72+
if !isIgnoredKind(owner.Kind.String()) {
73+
for _, clusterRole := range owner.ClusterRoles {
74+
perm := rbacv1.Subject{
75+
Name: owner.Name,
76+
Kind: owner.Kind.String(),
77+
}
78+
addPermission(clusterRole, perm)
79+
}
80+
}
81+
}
82+
83+
// Process additional role bindings
84+
for _, role := range in.Spec.AdditionalRoleBindings {
85+
for _, subject := range role.Subjects {
86+
if !isIgnoredKind(subject.Kind) {
87+
perm := rbacv1.Subject{
88+
Name: subject.Name,
89+
Kind: subject.Kind,
90+
}
91+
addPermission(role.ClusterRoleName, perm)
92+
}
93+
}
94+
}
95+
96+
return
97+
}
98+
99+
// Get the permissions for a tenant ordered by groups and users.
100+
func (in *Tenant) GetClusterRolesBySubject(ignoreOwnerKind []OwnerKind) (maps map[string]map[string]api.TenantSubjectRoles) {
101+
maps = make(map[string]map[string]api.TenantSubjectRoles)
102+
103+
// Initialize a nested map for kind ("User", "Group") and name
104+
initNestedMap := func(kind string) {
105+
if _, exists := maps[kind]; !exists {
106+
maps[kind] = make(map[string]api.TenantSubjectRoles)
107+
}
108+
}
109+
// Helper to check if a kind is in the ignoreOwnerKind list
110+
isIgnoredKind := func(kind string) bool {
111+
for _, ignored := range ignoreOwnerKind {
112+
if kind == ignored.String() {
113+
return true
114+
}
115+
}
116+
117+
return false
118+
}
119+
120+
// Process owners
121+
for _, owner := range in.Spec.Owners {
122+
if !isIgnoredKind(owner.Kind.String()) {
123+
initNestedMap(owner.Kind.String())
124+
125+
if perm, exists := maps[owner.Kind.String()][owner.Name]; exists {
126+
// If the permission entry already exists, append cluster roles
127+
perm.ClusterRoles = append(perm.ClusterRoles, owner.ClusterRoles...)
128+
maps[owner.Kind.String()][owner.Name] = perm
129+
} else {
130+
// Create a new permission entry
131+
maps[owner.Kind.String()][owner.Name] = api.TenantSubjectRoles{
132+
ClusterRoles: owner.ClusterRoles,
133+
}
134+
}
135+
}
136+
}
137+
138+
// Process additional role bindings
139+
for _, role := range in.Spec.AdditionalRoleBindings {
140+
for _, subject := range role.Subjects {
141+
if !isIgnoredKind(subject.Kind) {
142+
initNestedMap(subject.Kind)
143+
144+
if perm, exists := maps[subject.Kind][subject.Name]; exists {
145+
// If the permission entry already exists, append cluster roles
146+
perm.ClusterRoles = append(perm.ClusterRoles, role.ClusterRoleName)
147+
maps[subject.Kind][subject.Name] = perm
148+
} else {
149+
// Create a new permission entry
150+
maps[subject.Kind][subject.Name] = api.TenantSubjectRoles{
151+
ClusterRoles: []string{role.ClusterRoleName},
152+
}
153+
}
154+
}
155+
}
156+
}
157+
158+
// Remove duplicates from cluster roles in both maps
159+
for kind, nameMap := range maps {
160+
for name, perm := range nameMap {
161+
perm.ClusterRoles = slices.Compact(perm.ClusterRoles)
162+
maps[kind][name] = perm
163+
}
164+
}
165+
166+
return maps
167+
}

api/v1beta2/tenant_func_test.go

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2020-2023 Project Capsule Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package v1beta2
5+
6+
import (
7+
"reflect"
8+
"testing"
9+
10+
"github.com/projectcapsule/capsule/pkg/api"
11+
rbacv1 "k8s.io/api/rbac/v1"
12+
)
13+
14+
var tenant = &Tenant{
15+
Spec: TenantSpec{
16+
Owners: []OwnerSpec{
17+
{
18+
Kind: "User",
19+
Name: "user1",
20+
ClusterRoles: []string{"cluster-admin", "read-only"},
21+
},
22+
{
23+
Kind: "Group",
24+
Name: "group1",
25+
ClusterRoles: []string{"edit"},
26+
},
27+
{
28+
Kind: ServiceAccountOwner,
29+
Name: "service",
30+
ClusterRoles: []string{"read-only"},
31+
},
32+
},
33+
AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{
34+
{
35+
ClusterRoleName: "developer",
36+
Subjects: []rbacv1.Subject{
37+
{Kind: "User", Name: "user2"},
38+
{Kind: "Group", Name: "group1"},
39+
},
40+
},
41+
{
42+
ClusterRoleName: "cluster-admin",
43+
Subjects: []rbacv1.Subject{
44+
{
45+
Kind: "User",
46+
Name: "user3",
47+
},
48+
{
49+
Kind: "Group",
50+
Name: "group1",
51+
},
52+
},
53+
},
54+
{
55+
ClusterRoleName: "deployer",
56+
Subjects: []rbacv1.Subject{
57+
{
58+
Kind: "ServiceAccount",
59+
Name: "system:serviceaccount:argocd:argo-operator",
60+
},
61+
},
62+
},
63+
},
64+
},
65+
}
66+
67+
// TestGetClusterRolePermissions tests the GetClusterRolePermissions function
68+
func TestGetSubjectsByClusterRoles(t *testing.T) {
69+
expected := map[string][]rbacv1.Subject{
70+
"cluster-admin": {
71+
{Kind: "User", Name: "user1"},
72+
{Kind: "User", Name: "user3"},
73+
{Kind: "Group", Name: "group1"},
74+
},
75+
"read-only": {
76+
{Kind: "User", Name: "user1"},
77+
{Kind: "ServiceAccount", Name: "service"},
78+
},
79+
"edit": {
80+
{Kind: "Group", Name: "group1"},
81+
},
82+
"developer": {
83+
{Kind: "User", Name: "user2"},
84+
{Kind: "Group", Name: "group1"},
85+
},
86+
"deployer": {
87+
{Kind: "ServiceAccount", Name: "system:serviceaccount:argocd:argo-operator"},
88+
},
89+
}
90+
91+
// Call the function to test
92+
permissions := tenant.GetSubjectsByClusterRoles(nil)
93+
94+
if !reflect.DeepEqual(permissions, expected) {
95+
t.Errorf("Expected %v, but got %v", expected, permissions)
96+
}
97+
98+
// Ignore SubjectTypes (Ignores ServiceAccounts)
99+
ignored := tenant.GetSubjectsByClusterRoles([]OwnerKind{"ServiceAccount"})
100+
expectedIgnored := map[string][]rbacv1.Subject{
101+
"cluster-admin": {
102+
{Kind: "User", Name: "user1"},
103+
{Kind: "User", Name: "user3"},
104+
{Kind: "Group", Name: "group1"},
105+
},
106+
"read-only": {
107+
{Kind: "User", Name: "user1"},
108+
},
109+
"edit": {
110+
{Kind: "Group", Name: "group1"},
111+
},
112+
"developer": {
113+
{Kind: "User", Name: "user2"},
114+
{Kind: "Group", Name: "group1"},
115+
},
116+
}
117+
118+
if !reflect.DeepEqual(ignored, expectedIgnored) {
119+
t.Errorf("Expected %v, but got %v", expectedIgnored, ignored)
120+
}
121+
122+
}
123+
124+
func TestGetClusterRolesBySubject(t *testing.T) {
125+
126+
expected := map[string]map[string]api.TenantSubjectRoles{
127+
"User": {
128+
"user1": {
129+
ClusterRoles: []string{"cluster-admin", "read-only"},
130+
},
131+
"user2": {
132+
ClusterRoles: []string{"developer"},
133+
},
134+
"user3": {
135+
ClusterRoles: []string{"cluster-admin"},
136+
},
137+
},
138+
"Group": {
139+
"group1": {
140+
ClusterRoles: []string{"edit", "developer", "cluster-admin"},
141+
},
142+
},
143+
"ServiceAccount": {
144+
"service": {
145+
ClusterRoles: []string{"read-only"},
146+
},
147+
"system:serviceaccount:argocd:argo-operator": {
148+
ClusterRoles: []string{"deployer"},
149+
},
150+
},
151+
}
152+
153+
permissions := tenant.GetClusterRolesBySubject(nil)
154+
if !reflect.DeepEqual(permissions, expected) {
155+
t.Errorf("Expected %v, but got %v", expected, permissions)
156+
}
157+
158+
delete(expected, "ServiceAccount")
159+
ignored := tenant.GetClusterRolesBySubject([]OwnerKind{"ServiceAccount"})
160+
161+
if !reflect.DeepEqual(ignored, expected) {
162+
t.Errorf("Expected %v, but got %v", expected, ignored)
163+
}
164+
}
165+
166+
// Helper function to run tests
167+
func TestMain(t *testing.M) {
168+
t.Run()
169+
}
170+
171+
// permissionsEqual checks the equality of two TenantPermission structs.
172+
func permissionsEqual(a, b api.TenantSubjectRoles) bool {
173+
if a.Kind != b.Kind {
174+
return false
175+
}
176+
if len(a.ClusterRoles) != len(b.ClusterRoles) {
177+
return false
178+
}
179+
180+
// Create a map to count occurrences of cluster roles
181+
counts := make(map[string]int)
182+
for _, role := range a.ClusterRoles {
183+
counts[role]++
184+
}
185+
for _, role := range b.ClusterRoles {
186+
counts[role]--
187+
if counts[role] < 0 {
188+
return false // More occurrences in b than in a
189+
}
190+
}
191+
return true
192+
}

pkg/api/tenant_roles.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2020-2023 Project Capsule Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package api
5+
6+
// Type to extract all clusterroles for a subject on a tenant
7+
// from the owner and additionalRoleBindings spec.
8+
type TenantSubjectRoles struct {
9+
Kind string
10+
Name string
11+
ClusterRoles []string
12+
}

0 commit comments

Comments
 (0)