Skip to content

Commit

Permalink
feat(api): add tenant funcs to retrieve subjects based on clusterrole…
Browse files Browse the repository at this point in the history
… bindings (#1231)

Signed-off-by: Oliver Bähler <[email protected]>
  • Loading branch information
oliverbaehler authored Oct 23, 2024
1 parent 5143c5c commit f82c2f4
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 0 deletions.
129 changes: 129 additions & 0 deletions api/v1beta2/tenant_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
192 changes: 192 additions & 0 deletions api/v1beta2/tenant_func_test.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 12 additions & 0 deletions pkg/api/tenant_roles.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit f82c2f4

Please sign in to comment.