Files
capsule/pkg/api/rbac/users_list_test.go
cc4fb45d70 feat: upstream enterprise preview (#1841)
feat: upstream enterprise preview

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
2026-05-28 00:58:58 +02:00

462 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rbac_test
import (
"math/rand"
"reflect"
"sort"
"testing"
"time"
"github.com/projectcapsule/capsule/pkg/api/rbac"
)
func linearFindUser(list rbac.UserListSpec, name string, kind rbac.OwnerKind) (rbac.UserSpec, bool) {
for _, u := range list {
if u.Kind == kind && u.Name == name {
return u, true
}
}
return rbac.UserSpec{}, false
}
func slowIsPresent(u rbac.UserListSpec, name string, groups []string) bool {
for _, user := range u {
switch user.Kind {
case rbac.UserOwner, rbac.ServiceAccountOwner:
if name == user.Name {
return true
}
case rbac.GroupOwner:
for _, group := range groups {
if group == user.Name {
return true
}
}
}
}
return false
}
func TestByKindNameOrdering_UserListSpec(t *testing.T) {
u := rbac.UserListSpec{
rbac.UserSpec{Name: "b", Kind: rbac.ServiceAccountOwner},
rbac.UserSpec{Name: "z", Kind: rbac.UserOwner},
rbac.UserSpec{Name: "a", Kind: rbac.GroupOwner},
rbac.UserSpec{Name: "a", Kind: rbac.UserOwner},
}
// Sort using production ordering
got := append(rbac.UserListSpec(nil), u...)
sort.Sort(rbac.ByKindName(got))
// Manually sorted expectation using the same logic.
want := append(rbac.UserListSpec(nil), u...)
sort.Slice(want, func(i, j int) bool {
if want[i].Kind.String() != want[j].Kind.String() {
return want[i].Kind.String() < want[j].Kind.String()
}
return want[i].Name < want[j].Name
})
if len(got) != len(want) {
t.Fatalf("length mismatch: got %d, want %d", len(got), len(want))
}
for i := range got {
if !reflect.DeepEqual(got[i], want[i]) {
t.Fatalf("ordering mismatch at %d: got %+v, want %+v", i, got[i], want[i])
}
}
}
func TestFindUser_Randomized(t *testing.T) {
rnd := rand.New(rand.NewSource(42))
ownerKinds := []rbac.OwnerKind{
rbac.GroupOwner,
rbac.UserOwner,
rbac.ServiceAccountOwner,
}
const (
numLists = 200
maxLength = 40
numLookupsPerList = 80
)
for listIdx := 0; listIdx < numLists; listIdx++ {
var list rbac.UserListSpec
n := rnd.Intn(maxLength)
for i := 0; i < n; i++ {
k := ownerKinds[rnd.Intn(len(ownerKinds))]
list = append(list, rbac.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // length 36
Kind: k,
})
}
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
var qName string
var qKind rbac.OwnerKind
if len(list) > 0 && rnd.Float64() < 0.6 {
// 60% of lookups: pick a real element, must be found
pick := list[rnd.Intn(len(list))]
qName = pick.Name
qKind = pick.Kind
} else {
// 40%: random query, may or may not exist
qName = randomName(rnd, 3+rnd.Intn(4))
qKind = ownerKinds[rnd.Intn(len(ownerKinds))]
}
listCopy := append(rbac.UserListSpec(nil), list...) // FindUser sorts in-place
gotUser, gotFound := listCopy.FindUser(qName, qKind)
wantUser, wantFound := linearFindUser(list, qName, qKind)
if gotFound != wantFound {
t.Fatalf("list=%d lookup=%d: found mismatch for (%q,%v): got=%v, want=%v",
listIdx, lookupIdx, qName, qKind, gotFound, wantFound)
}
if gotFound && !reflect.DeepEqual(gotUser, wantUser) {
t.Fatalf("list=%d lookup=%d: user mismatch for (%q,%v):\n got= %+v\nwant= %+v",
listIdx, lookupIdx, qName, qKind, gotUser, wantUser)
}
}
}
}
func TestIsPresent_RandomizedMatchesSlowImplementation(t *testing.T) {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
ownerKinds := []rbac.OwnerKind{
rbac.UserOwner,
rbac.GroupOwner,
rbac.ServiceAccountOwner,
}
const (
numLists = 200
maxOwnersPerList = 30
numLookupsPerList = 80
maxGroupsPerUser = 10
)
for listIdx := 0; listIdx < numLists; listIdx++ {
// Generate a random user list (possibly with duplicates).
var users rbac.UserListSpec
nOwners := rnd.Intn(maxOwnersPerList)
for i := 0; i < nOwners; i++ {
kind := ownerKinds[rnd.Intn(len(ownerKinds))]
users = append(users, rbac.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // length 36
Kind: kind,
})
}
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
// Generate a random userName and groups,
// sometimes biased to hit existing owners/groups.
var userName string
var groups []string
// 50% of the time: pick an existing owner name as userName
if len(users) > 0 && rnd.Float64() < 0.5 {
pick := users[rnd.Intn(len(users))]
userName = pick.Name
} else {
userName = randomName(rnd, 3+rnd.Intn(4))
}
// Random groups, sometimes including owner names
nGroups := rnd.Intn(maxGroupsPerUser)
for i := 0; i < nGroups; i++ {
if len(users) > 0 && rnd.Float64() < 0.5 {
pick := users[rnd.Intn(len(users))]
groups = append(groups, pick.Name)
} else {
groups = append(groups, randomName(rnd, 3+rnd.Intn(4)))
}
}
got := users.IsPresent(userName, groups)
want := slowIsPresent(users, userName, groups)
if got != want {
t.Fatalf("list=%d lookup=%d: mismatch\n users=%v\n user=%q\n groups=%v\n optimized=%v\n slow=%v",
listIdx, lookupIdx, users, userName, groups, got, want)
}
}
}
}
func TestGetByKinds_Basic(t *testing.T) {
users := rbac.UserListSpec{
rbac.UserSpec{Name: "alice", Kind: rbac.UserOwner},
rbac.UserSpec{Name: "svc-1", Kind: rbac.ServiceAccountOwner},
rbac.UserSpec{Name: "team-a", Kind: rbac.GroupOwner},
rbac.UserSpec{Name: "bob", Kind: rbac.UserOwner},
rbac.UserSpec{Name: "team-b", Kind: rbac.GroupOwner},
}
eqStrings := func(got, want []string) bool {
if got == nil && want == nil {
return true
}
if len(got) != len(want) {
return false
}
sort.Strings(got)
sort.Strings(want)
for i := range got {
if got[i] != want[i] {
return false
}
}
return true
}
// Single kind: UserOwner
gotUsers := users.GetByKinds([]rbac.OwnerKind{rbac.UserOwner})
wantUsers := []string{"alice", "bob"}
if !eqStrings(gotUsers, wantUsers) {
t.Fatalf("GetByKinds([UserOwner]) = %v, want %v", gotUsers, wantUsers)
}
// Single kind: GroupOwner
gotGroups := users.GetByKinds([]rbac.OwnerKind{rbac.GroupOwner})
wantGroups := []string{"team-a", "team-b"}
if !eqStrings(gotGroups, wantGroups) {
t.Fatalf("GetByKinds([GroupOwner]) = %v, want %v", gotGroups, wantGroups)
}
// Multiple kinds: UserOwner + ServiceAccountOwner
gotUsersAndSAs := users.GetByKinds([]rbac.OwnerKind{rbac.UserOwner, rbac.ServiceAccountOwner})
wantUsersAndSAs := []string{"alice", "bob", "svc-1"}
if !eqStrings(gotUsersAndSAs, wantUsersAndSAs) {
t.Fatalf("GetByKinds([UserOwner,ServiceAccountOwner]) = %v, want %v",
gotUsersAndSAs, wantUsersAndSAs)
}
// No kinds → nil
gotNone := users.GetByKinds(nil)
if gotNone != nil {
t.Fatalf("GetByKinds(nil) = %v, want nil", gotNone)
}
// Kind not present at all
gotUnknown := users.GetByKinds([]rbac.OwnerKind{rbac.OwnerKind("does-not-exist")})
if gotUnknown != nil {
t.Fatalf("GetByKinds([unknown]) = %v, want nil", gotUnknown)
}
}
func TestGetByKinds_Randomized(t *testing.T) {
rnd := rand.New(rand.NewSource(123))
ownerKinds := []rbac.OwnerKind{
rbac.UserOwner,
rbac.GroupOwner,
rbac.ServiceAccountOwner,
}
const (
numLists = 200
maxOwnersPerList = 50
)
for listIdx := 0; listIdx < numLists; listIdx++ {
var users rbac.UserListSpec
n := rnd.Intn(maxOwnersPerList)
for i := 0; i < n; i++ {
k := ownerKinds[rnd.Intn(len(ownerKinds))]
users = append(users, rbac.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // reuse your helper
Kind: k,
})
}
// Try several random kind-subsets per list
for subsetIdx := 0; subsetIdx < 10; subsetIdx++ {
// Build a random subset of kinds
var kinds []rbac.OwnerKind
for _, k := range ownerKinds {
if rnd.Float64() < 0.5 {
kinds = append(kinds, k)
}
}
got := users.GetByKinds(kinds)
// Reference implementation: filter + sort
kindSet := make(map[rbac.OwnerKind]struct{}, len(kinds))
for _, k := range kinds {
kindSet[k] = struct{}{}
}
var want []string
if len(kinds) > 0 {
for _, u := range users {
if _, ok := kindSet[u.Kind]; ok {
want = append(want, u.Name)
}
}
}
if len(want) == 0 {
if got != nil {
t.Fatalf("list=%d subset=%d: expected nil, got %v (kinds=%v, users=%v)",
listIdx, subsetIdx, got, kinds, users)
}
continue
}
sort.Strings(want)
sort.Strings(got)
if !reflect.DeepEqual(got, want) {
t.Fatalf("list=%d subset=%d: GetByKinds mismatch\n kinds=%v\n got= %v\n want= %v",
listIdx, subsetIdx, kinds, got, want)
}
}
}
}
func TestUserListSpec_SplitUsersAndGroups(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in rbac.UserListSpec
wantUsers []string
wantGroups []string
}{
{
name: "nil",
in: nil,
wantUsers: nil,
wantGroups: nil,
},
{
name: "empty",
in: rbac.UserListSpec{},
wantUsers: nil,
wantGroups: nil,
},
{
name: "users_only_sorted_and_deduped",
in: rbac.UserListSpec{
{Kind: rbac.UserOwner, Name: "zara"},
{Kind: rbac.UserOwner, Name: "alice"},
{Kind: rbac.UserOwner, Name: "alice"},
{Kind: rbac.UserOwner, Name: "bob"},
},
wantUsers: []string{"alice", "bob", "zara"},
wantGroups: nil,
},
{
name: "groups_only_sorted_and_deduped",
in: rbac.UserListSpec{
{Kind: rbac.GroupOwner, Name: "team-b"},
{Kind: rbac.GroupOwner, Name: "team-a"},
{Kind: rbac.GroupOwner, Name: "team-a"},
},
wantUsers: nil,
wantGroups: []string{"team-a", "team-b"},
},
{
name: "mix_users_groups_and_serviceaccounts",
in: rbac.UserListSpec{
{Kind: rbac.GroupOwner, Name: "ops"},
{Kind: rbac.ServiceAccountOwner, Name: "system:serviceaccount:ns:sa"},
{Kind: rbac.UserOwner, Name: "alice"},
{Kind: rbac.GroupOwner, Name: "dev"},
},
wantUsers: []string{"alice", "system:serviceaccount:ns:sa"},
wantGroups: []string{"dev", "ops"},
},
{
name: "ignore_empty_names",
in: rbac.UserListSpec{
{Kind: rbac.UserOwner, Name: ""},
{Kind: rbac.GroupOwner, Name: ""},
{Kind: rbac.ServiceAccountOwner, Name: ""},
{Kind: rbac.UserOwner, Name: "alice"},
},
wantUsers: []string{"alice"},
wantGroups: nil,
},
{
name: "all_kinds",
in: rbac.UserListSpec{
{Kind: rbac.ServiceAccountOwner, Name: "x"},
{Kind: rbac.UserOwner, Name: "alice"},
{Kind: rbac.GroupOwner, Name: "dev"},
},
wantUsers: []string{"alice", "x"},
wantGroups: []string{"dev"},
},
{
name: "same_name_in_user_and_group_goes_to_respective_buckets",
in: rbac.UserListSpec{
{Kind: rbac.UserOwner, Name: "same"},
{Kind: rbac.GroupOwner, Name: "same"},
},
wantUsers: []string{"same"},
wantGroups: []string{"same"},
},
{
name: "deterministic_ordering_with_many_values",
in: rbac.UserListSpec{
{Kind: rbac.UserOwner, Name: "c"},
{Kind: rbac.UserOwner, Name: "a"},
{Kind: rbac.UserOwner, Name: "b"},
{Kind: rbac.GroupOwner, Name: "g2"},
{Kind: rbac.GroupOwner, Name: "g1"},
},
wantUsers: []string{"a", "b", "c"},
wantGroups: []string{"g1", "g2"},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotUsers, gotGroups := tt.in.SplitUsersAndGroups()
if !reflect.DeepEqual(gotUsers, tt.wantUsers) {
t.Fatalf("users = %#v, want %#v", gotUsers, tt.wantUsers)
}
if !reflect.DeepEqual(gotGroups, tt.wantGroups) {
t.Fatalf("groups = %#v, want %#v", gotGroups, tt.wantGroups)
}
})
}
}
func TestUserListSpec_SplitUsersAndGroups_Idempotent(t *testing.T) {
t.Parallel()
in := rbac.UserListSpec{
{Kind: rbac.GroupOwner, Name: "team-b"},
{Kind: rbac.UserOwner, Name: "zara"},
{Kind: rbac.UserOwner, Name: "alice"},
{Kind: rbac.GroupOwner, Name: "team-a"},
{Kind: rbac.UserOwner, Name: "alice"},
}
u1, g1 := in.SplitUsersAndGroups()
u2, g2 := in.SplitUsersAndGroups()
if !reflect.DeepEqual(u1, u2) {
t.Fatalf("users not deterministic: first=%#v second=%#v", u1, u2)
}
if !reflect.DeepEqual(g1, g2) {
t.Fatalf("groups not deterministic: first=%#v second=%#v", g1, g2)
}
}