mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-14 09:59:57 +00:00
* chore: improve dev targets Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(controller): implement deterministic rolebinding reflection Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(controller): capsule users are determined from configuration status Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(tenantowners): added agreggate option - tenantowners are always considered capsule users Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(tenantowner): add implicit aggregation for tenants Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: remove helm flags Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix(config): remove usergroups default Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
356 lines
9.1 KiB
Go
356 lines
9.1 KiB
Go
// Copyright 2020-2025 Project Capsule Authors
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
package api_test
|
||
|
||
import (
|
||
"math/rand"
|
||
"reflect"
|
||
"sort"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/projectcapsule/capsule/pkg/api"
|
||
)
|
||
|
||
func slowIsOwner(o api.OwnerStatusListSpec, name string, groups []string) bool {
|
||
for _, owner := range o {
|
||
switch owner.Kind {
|
||
case api.UserOwner, api.ServiceAccountOwner:
|
||
if name == owner.Name {
|
||
return true
|
||
}
|
||
case api.GroupOwner:
|
||
for _, group := range groups {
|
||
if group == owner.Name {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// linearFind is the obvious, slow, but correct reference implementation.
|
||
func linearFind(o api.OwnerStatusListSpec, name string, kind api.OwnerKind) (api.CoreOwnerSpec, bool) {
|
||
for _, x := range o {
|
||
if x.Kind == kind && x.Name == name {
|
||
return x, true
|
||
}
|
||
}
|
||
return api.CoreOwnerSpec{}, false
|
||
}
|
||
|
||
// randomName generates a simple lowercase name of length n.
|
||
func randomName(rnd *rand.Rand, n int) string {
|
||
const letters = "abcdefghijklmnopqrstuvwxyz"
|
||
b := make([]byte, n)
|
||
for i := range b {
|
||
b[i] = letters[rnd.Intn(len(letters))]
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
func TestUpsert_AddsNewOwnerToEmptyList(t *testing.T) {
|
||
var list api.OwnerStatusListSpec
|
||
|
||
list.Upsert(api.CoreOwnerSpec{
|
||
UserSpec: api.UserSpec{
|
||
Kind: api.UserOwner,
|
||
Name: "alice",
|
||
},
|
||
ClusterRoles: []string{"admin"},
|
||
})
|
||
|
||
if len(list) != 1 {
|
||
t.Fatalf("expected 1 owner, got %d", len(list))
|
||
}
|
||
got := list[0]
|
||
if got.Kind != api.UserOwner || got.Name != "alice" {
|
||
t.Fatalf("unexpected owner: %+v", got)
|
||
}
|
||
if !reflect.DeepEqual(got.ClusterRoles, []string{"admin"}) {
|
||
t.Fatalf("unexpected roles: %#v", got.ClusterRoles)
|
||
}
|
||
}
|
||
|
||
func TestUpsert_MergesClusterRolesForExistingOwner(t *testing.T) {
|
||
list := api.OwnerStatusListSpec{
|
||
{
|
||
UserSpec: api.UserSpec{
|
||
Kind: api.UserOwner,
|
||
Name: "alice",
|
||
},
|
||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||
},
|
||
}
|
||
|
||
list.Upsert(api.CoreOwnerSpec{
|
||
UserSpec: api.UserSpec{
|
||
Kind: api.UserOwner,
|
||
Name: "alice",
|
||
},
|
||
ClusterRoles: []string{"extra-sad"},
|
||
})
|
||
|
||
if len(list) != 1 {
|
||
t.Fatalf("expected 1 owner, got %d", len(list))
|
||
}
|
||
got := list[0]
|
||
if got.Kind != api.UserOwner || got.Name != "alice" {
|
||
t.Fatalf("unexpected owner: %+v", got)
|
||
}
|
||
|
||
// Roles should be union of both sets, order: existing roles first, then new ones
|
||
expected := []string{"admin", "capsule-namespace-deleter", "extra-sad"}
|
||
if !reflect.DeepEqual(got.ClusterRoles, expected) {
|
||
t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles)
|
||
}
|
||
}
|
||
|
||
func TestUpsert_DeduplicatesClusterRoles(t *testing.T) {
|
||
list := api.OwnerStatusListSpec{
|
||
{
|
||
UserSpec: api.UserSpec{
|
||
Kind: api.UserOwner,
|
||
Name: "alice",
|
||
},
|
||
ClusterRoles: []string{"admin", "viewer"},
|
||
},
|
||
}
|
||
|
||
list.Upsert(api.CoreOwnerSpec{
|
||
UserSpec: api.UserSpec{
|
||
Kind: api.UserOwner,
|
||
Name: "alice",
|
||
},
|
||
ClusterRoles: []string{"viewer", "editor"},
|
||
})
|
||
|
||
if len(list) != 1 {
|
||
t.Fatalf("expected 1 owner, got %d", len(list))
|
||
}
|
||
got := list[0]
|
||
|
||
expected := []string{"admin", "editor", "viewer"}
|
||
if !reflect.DeepEqual(got.ClusterRoles, expected) {
|
||
t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles)
|
||
}
|
||
}
|
||
|
||
func TestUpsert_KeepsListSortedAndMergesIntoExistingInUnsortedInitialSlice(t *testing.T) {
|
||
// Start with an unsorted slice, as could come from API/server
|
||
list := api.OwnerStatusListSpec{
|
||
{
|
||
UserSpec: api.UserSpec{
|
||
Kind: api.UserOwner,
|
||
Name: "bob",
|
||
},
|
||
ClusterRoles: []string{"bob-role"},
|
||
},
|
||
{
|
||
UserSpec: api.UserSpec{
|
||
Kind: api.UserOwner,
|
||
Name: "alice",
|
||
},
|
||
ClusterRoles: []string{"admin"},
|
||
},
|
||
}
|
||
|
||
// Upsert another alice
|
||
list.Upsert(api.CoreOwnerSpec{
|
||
UserSpec: api.UserSpec{
|
||
Kind: api.UserOwner,
|
||
Name: "alice",
|
||
},
|
||
ClusterRoles: []string{"extra"},
|
||
})
|
||
|
||
if len(list) != 2 {
|
||
t.Fatalf("expected 2 owners (alice, bob), got %d", len(list))
|
||
}
|
||
|
||
// Ensure sorted by Kind.Name: alice before bob
|
||
// (relies on ByKindAndName order)
|
||
sorted := make(api.OwnerStatusListSpec, len(list))
|
||
copy(sorted, list)
|
||
sort.Sort(api.GetByKindAndName(sorted))
|
||
|
||
if !reflect.DeepEqual(list, sorted) {
|
||
t.Fatalf("expected list to be sorted by kind+name, got %#v", list)
|
||
}
|
||
|
||
// Find alice and check roles
|
||
var alice *api.CoreOwnerSpec
|
||
for i := range list {
|
||
if list[i].Name == "alice" {
|
||
alice = &list[i]
|
||
break
|
||
}
|
||
}
|
||
if alice == nil {
|
||
t.Fatalf("alice not found in list")
|
||
}
|
||
|
||
expectedRoles := []string{"admin", "extra"}
|
||
if !reflect.DeepEqual(alice.ClusterRoles, expectedRoles) {
|
||
t.Fatalf("expected alice roles %v, got %v", expectedRoles, alice.ClusterRoles)
|
||
}
|
||
}
|
||
|
||
func TestGetByKindAndNameOrdering(t *testing.T) {
|
||
o := api.OwnerStatusListSpec{
|
||
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "b", Kind: api.ServiceAccountOwner}},
|
||
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "z", Kind: api.UserOwner}},
|
||
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "a", Kind: api.GroupOwner}},
|
||
api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "a", Kind: api.UserOwner}},
|
||
}
|
||
|
||
// Sort using production ordering
|
||
got := append(api.OwnerStatusListSpec(nil), o...)
|
||
sort.Sort(api.GetByKindAndName(got))
|
||
|
||
// Manually sorted expectation using the same logic.
|
||
want := append(api.OwnerStatusListSpec(nil), o...)
|
||
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 TestFindOwner_Randomized(t *testing.T) {
|
||
rnd := rand.New(rand.NewSource(42)) // fixed seed for deterministic test runs
|
||
|
||
ownerKinds := []api.OwnerKind{
|
||
api.GroupOwner,
|
||
api.UserOwner,
|
||
api.ServiceAccountOwner,
|
||
}
|
||
|
||
const (
|
||
numLists = 200
|
||
maxLength = 40
|
||
numLookupsPerList = 80
|
||
)
|
||
|
||
for listIdx := 0; listIdx < numLists; listIdx++ {
|
||
var list api.OwnerStatusListSpec
|
||
n := rnd.Intn(maxLength)
|
||
for i := 0; i < n; i++ {
|
||
k := ownerKinds[rnd.Intn(len(ownerKinds))]
|
||
list = append(list, api.CoreOwnerSpec{
|
||
UserSpec: api.UserSpec{
|
||
Name: randomName(rnd, 3+rnd.Intn(4)), // length 3–6
|
||
Kind: k,
|
||
},
|
||
})
|
||
}
|
||
|
||
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
|
||
var qName string
|
||
var qKind api.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(api.OwnerStatusListSpec(nil), list...)
|
||
gotOwner, gotFound := listCopy.FindOwner(qName, qKind)
|
||
wantOwner, wantFound := linearFind(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(gotOwner, wantOwner) {
|
||
t.Fatalf("list=%d lookup=%d: owner mismatch for (%q,%v):\n got= %+v\nwant= %+v",
|
||
listIdx, lookupIdx, qName, qKind, gotOwner, wantOwner)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestIsOwner_RandomizedMatchesSlowImplementation(t *testing.T) {
|
||
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
|
||
ownerKinds := []api.OwnerKind{
|
||
api.UserOwner,
|
||
api.GroupOwner,
|
||
api.ServiceAccountOwner,
|
||
}
|
||
|
||
const (
|
||
numLists = 200
|
||
maxOwnersPerList = 30
|
||
numLookupsPerList = 80
|
||
maxGroupsPerUser = 10
|
||
)
|
||
|
||
for listIdx := 0; listIdx < numLists; listIdx++ {
|
||
// Generate a random owner list (possibly with duplicates).
|
||
var owners api.OwnerStatusListSpec
|
||
nOwners := rnd.Intn(maxOwnersPerList)
|
||
for i := 0; i < nOwners; i++ {
|
||
kind := ownerKinds[rnd.Intn(len(ownerKinds))]
|
||
owners = append(owners, api.CoreOwnerSpec{
|
||
UserSpec: api.UserSpec{
|
||
Name: randomName(rnd, 3+rnd.Intn(4)), // length 3–6
|
||
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(owners) > 0 && rnd.Float64() < 0.5 {
|
||
pick := owners[rnd.Intn(len(owners))]
|
||
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(owners) > 0 && rnd.Float64() < 0.5 {
|
||
pick := owners[rnd.Intn(len(owners))]
|
||
groups = append(groups, pick.Name)
|
||
} else {
|
||
groups = append(groups, randomName(rnd, 3+rnd.Intn(4)))
|
||
}
|
||
}
|
||
|
||
got := owners.IsOwner(userName, groups)
|
||
want := slowIsOwner(owners, userName, groups)
|
||
|
||
if got != want {
|
||
t.Fatalf("list=%d lookup=%d: mismatch\n owners=%v\n user=%q\n groups=%v\n optimized=%v\n slow=%v",
|
||
listIdx, lookupIdx, owners, userName, groups, got, want)
|
||
}
|
||
}
|
||
}
|
||
}
|