internal/rbac/rbac_test.go
Rama main · réplica de lectura del repositorio original.
package rbac
import (
"slices"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func role(name, namespace string, rules ...any) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "Role",
"metadata": map[string]any{"name": name, "namespace": namespace},
"rules": rules,
}}
}
func clusterRole(name string, rules ...any) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole",
"metadata": map[string]any{"name": name},
"rules": rules,
}}
}
func rule(verbs, groups, resources []string) map[string]any {
return map[string]any{
"verbs": asAny(verbs),
"apiGroups": asAny(groups),
"resources": asAny(resources),
}
}
func asAny(held []string) []any {
out := make([]any, 0, len(held))
for _, one := range held {
out = append(out, one)
}
return out
}
func binding(name, namespace, roleKind, roleName string, subjects ...any) *unstructured.Unstructured {
kind := "RoleBinding"
meta := map[string]any{"name": name, "namespace": namespace}
if namespace == "" {
kind = "ClusterRoleBinding"
meta = map[string]any{"name": name}
}
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": kind,
"metadata": meta,
"roleRef": map[string]any{"kind": roleKind, "name": roleName},
"subjects": subjects,
}}
}
func account(name, namespace string) map[string]any {
return map[string]any{"kind": "ServiceAccount", "name": name, "namespace": namespace}
}
func user(name string) map[string]any {
return map[string]any{"kind": "User", "name": name}
}
func holderFor(t *testing.T, index Index, label string) Holder {
t.Helper()
for _, one := range index.Holders {
if one.Subject.Label() == label {
return one
}
}
t.Fatalf("no holder called %q in %+v", label, labels(index))
return Holder{}
}
func labels(index Index) []string {
out := make([]string, 0, len(index.Holders))
for _, one := range index.Holders {
out = append(out, one.Subject.Label())
}
return out
}
func TestABindingBecomesASubjectThatHoldsIt(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{role("reader", "web", rule([]string{"get"}, []string{""}, []string{"pods"}))},
Bindings: []*unstructured.Unstructured{binding("read-web", "web", "Role", "reader", account("api", "web"))},
})
held := holderFor(t, index, "system:serviceaccount:web:api")
if len(held.Grants) != 1 {
t.Fatalf("grants = %+v", held.Grants)
}
if held.Grants[0].Role != "reader" || held.Grants[0].Namespace != "web" {
t.Fatalf("the grant came back as %+v", held.Grants[0])
}
}
func TestAServiceAccountWithNoNamespaceBelongsToTheBindings(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{role("reader", "web", rule([]string{"get"}, []string{""}, []string{"pods"}))},
Bindings: []*unstructured.Unstructured{binding("read-web", "web", "Role", "reader", map[string]any{"kind": "ServiceAccount", "name": "api"})},
})
holderFor(t, index, "system:serviceaccount:web:api")
}
func TestAClusterRoleBindingReachesEveryNamespace(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{clusterRole("reader", rule([]string{"get"}, []string{""}, []string{"secrets"}))},
ClusterBinds: []*unstructured.Unstructured{binding("read-all", "", "ClusterRole", "reader", user("ana"))},
})
held := holderFor(t, index, "ana")
if !held.Grants[0].Everywhere() {
t.Fatalf("the grant was confined to %q", held.Grants[0].Namespace)
}
if !held.Grants[0].Reaches(Ask{Verb: "get", Resource: "secrets", Namespace: "anywhere"}) {
t.Fatal("a cluster-wide grant did not reach a namespace")
}
}
func TestARoleBindingToAClusterRoleStaysInItsNamespace(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{clusterRole("reader", rule([]string{"get"}, []string{""}, []string{"secrets"}))},
Bindings: []*unstructured.Unstructured{binding("read-web", "web", "ClusterRole", "reader", user("ana"))},
})
held := holderFor(t, index, "ana")
if held.Grants[0].Everywhere() {
t.Fatal("a role binding to a cluster role reached the whole cluster")
}
if !held.Grants[0].Reaches(Ask{Verb: "get", Resource: "secrets", Namespace: "web"}) {
t.Fatal("it did not reach its own namespace")
}
if held.Grants[0].Reaches(Ask{Verb: "get", Resource: "secrets", Namespace: "other"}) {
t.Fatal("it reached a namespace it was never bound in")
}
}
func TestARoleIsLookedUpInItsOwnNamespace(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{
role("reader", "web", rule([]string{"get"}, []string{""}, []string{"pods"})),
role("reader", "db", rule([]string{"delete"}, []string{""}, []string{"pods"})),
},
Bindings: []*unstructured.Unstructured{binding("read-web", "web", "Role", "reader", user("ana"))},
})
held := holderFor(t, index, "ana")
if held.Grants[0].Reaches(Ask{Verb: "delete", Resource: "pods", Namespace: "web"}) {
t.Fatal("it picked up the other namespace's role of the same name")
}
}
func TestABindingThatNamesARoleNobodyWroteIsReported(t *testing.T) {
index := Build(Held{
Bindings: []*unstructured.Unstructured{binding("read-web", "web", "Role", "gone", user("ana"))},
})
if len(index.Absent) != 1 {
t.Fatalf("absent = %+v", index.Absent)
}
held := holderFor(t, index, "ana")
if !held.Grants[0].Missing {
t.Fatal("the grant did not say its role is missing")
}
if held.Grants[0].Reaches(Ask{Verb: "get", Resource: "pods", Namespace: "web"}) {
t.Fatal("a missing role granted something")
}
}
func TestAWildcardVerbCoversTheOneAsked(t *testing.T) {
one := Rule{Verbs: []string{Anything}, Groups: []string{""}, Resources: []string{"pods"}}
if !one.Allows(Ask{Verb: "delete", Resource: "pods"}) {
t.Fatal("a wildcard verb did not cover delete")
}
}
func TestAWildcardResourceCoversTheOneAsked(t *testing.T) {
one := Rule{Verbs: []string{"get"}, Groups: []string{Anything}, Resources: []string{Anything}}
if !one.Allows(Ask{Verb: "get", Group: "apps", Resource: "deployments"}) {
t.Fatal("a wildcard resource did not cover deployments")
}
}
func TestTheApiGroupHasToMatch(t *testing.T) {
one := Rule{Verbs: []string{"get"}, Groups: []string{""}, Resources: []string{"deployments"}}
if one.Allows(Ask{Verb: "get", Group: "apps", Resource: "deployments"}) {
t.Fatal("a core-group rule answered for an apps-group question")
}
}
func TestPodsDoesNotCoverPodsExec(t *testing.T) {
one := Rule{Verbs: []string{"create"}, Groups: []string{""}, Resources: []string{"pods"}}
if one.Allows(Ask{Verb: "create", Resource: "pods/exec"}) {
t.Fatal("a rule on pods answered for pods/exec")
}
}
func TestASubresourceWildcardCoversIt(t *testing.T) {
one := Rule{Verbs: []string{"create"}, Groups: []string{""}, Resources: []string{"pods/*"}}
if !one.Allows(Ask{Verb: "create", Resource: "pods/exec"}) {
t.Fatal("pods/* did not cover pods/exec")
}
}
func TestARuleWithNoResourcesAllowsNothing(t *testing.T) {
one := Rule{Verbs: []string{Anything}, Groups: []string{Anything}, URLs: []string{"/healthz"}}
if one.Allows(Ask{Verb: "get", Resource: "pods"}) {
t.Fatal("a non-resource rule answered a resource question")
}
}
func TestWhoNamesEverySubjectThatMay(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{
clusterRole("exec", rule([]string{"create"}, []string{""}, []string{"pods/exec"})),
clusterRole("look", rule([]string{"get"}, []string{""}, []string{"pods"})),
},
ClusterBinds: []*unstructured.Unstructured{
binding("can-exec", "", "ClusterRole", "exec", user("ana")),
binding("can-look", "", "ClusterRole", "look", user("bo")),
},
})
found := index.Who(Ask{Verb: "create", Resource: "pods/exec"})
if len(found) != 1 || found[0].Subject.Name != "ana" {
t.Fatalf("who = %+v", labelsOf(found))
}
}
func TestWhoKeepsOnlyTheGrantsThatAnswer(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{
clusterRole("exec", rule([]string{"create"}, []string{""}, []string{"pods/exec"})),
clusterRole("look", rule([]string{"get"}, []string{""}, []string{"pods"})),
},
ClusterBinds: []*unstructured.Unstructured{
binding("can-exec", "", "ClusterRole", "exec", user("ana")),
binding("can-look", "", "ClusterRole", "look", user("ana")),
},
})
found := index.Who(Ask{Verb: "create", Resource: "pods/exec"})
if len(found[0].Grants) != 1 || found[0].Grants[0].Role != "exec" {
t.Fatalf("grants = %+v", found[0].Grants)
}
}
func TestWhoInOneNamespaceLeavesOutAnother(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{
role("reader", "web", rule([]string{"get"}, []string{""}, []string{"secrets"})),
role("reader", "db", rule([]string{"get"}, []string{""}, []string{"secrets"})),
},
Bindings: []*unstructured.Unstructured{
binding("read-web", "web", "Role", "reader", user("ana")),
binding("read-db", "db", "Role", "reader", user("bo")),
},
})
found := index.Who(Ask{Verb: "get", Resource: "secrets", Namespace: "web"})
if len(found) != 1 || found[0].Subject.Name != "ana" {
t.Fatalf("who = %+v", labelsOf(found))
}
}
func labelsOf(held []Holder) []string {
out := make([]string, 0, len(held))
for _, one := range held {
out = append(out, one.Subject.Label())
}
return out
}
func TestClusterAdminIsSaidOnItsOwn(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{
clusterRole("cluster-admin", rule([]string{Anything}, []string{Anything}, []string{Anything})),
},
ClusterBinds: []*unstructured.Unstructured{binding("admins", "", "ClusterRole", "cluster-admin", user("ana"))},
})
held := holderFor(t, index, "ana")
if !slices.Equal(held.Powers, []string{"cluster-admin"}) {
t.Fatalf("powers = %+v, want cluster-admin alone", held.Powers)
}
}
func TestThePowersAreNamedForWhatTheyReallyReach(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{
clusterRole("ops",
rule([]string{"get", "list"}, []string{""}, []string{"secrets"}),
rule([]string{"create"}, []string{""}, []string{"pods/exec"})),
},
ClusterBinds: []*unstructured.Unstructured{binding("ops", "", "ClusterRole", "ops", user("ana"))},
})
held := holderFor(t, index, "ana")
for _, want := range []string{"reads secrets", "lists secrets", "execs into pods"} {
if !slices.Contains(held.Powers, want) {
t.Fatalf("powers = %+v, want %q", held.Powers, want)
}
}
}
func TestASubjectWithNothingWorthNamingSaysNothing(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{role("reader", "web", rule([]string{"get"}, []string{""}, []string{"configmaps"}))},
Bindings: []*unstructured.Unstructured{binding("read-web", "web", "Role", "reader", user("ana"))},
})
held := holderFor(t, index, "ana")
if len(held.Powers) != 0 {
t.Fatalf("powers = %+v", held.Powers)
}
}
func TestTheFurthestReachingSubjectComesFirst(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{role("quiet", "web", rule([]string{"get"}, []string{""}, []string{"configmaps"}))},
ClusterRoles: []*unstructured.Unstructured{clusterRole("admin", rule([]string{Anything}, []string{Anything}, []string{Anything}))},
Bindings: []*unstructured.Unstructured{binding("quiet", "web", "Role", "quiet", user("bo"))},
ClusterBinds: []*unstructured.Unstructured{binding("admin", "", "ClusterRole", "admin", user("ana"))},
})
if index.Holders[0].Subject.Name != "ana" {
t.Fatalf("the order was %+v", labels(index))
}
}
func TestASubjectSaysWhereItsGrantsApply(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{
role("reader", "web", rule([]string{"get"}, []string{""}, []string{"pods"})),
role("reader", "db", rule([]string{"get"}, []string{""}, []string{"pods"})),
},
Bindings: []*unstructured.Unstructured{
binding("web", "web", "Role", "reader", user("ana")),
binding("db", "db", "Role", "reader", user("ana")),
},
})
held := holderFor(t, index, "ana")
if !slices.Equal(held.Namespaces(), []string{"db", "web"}) {
t.Fatalf("namespaces = %+v", held.Namespaces())
}
}
func TestASubjectWithACurrentGrantIsNotConfinedToNamespaces(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{clusterRole("reader", rule([]string{"get"}, []string{""}, []string{"pods"}))},
ClusterBinds: []*unstructured.Unstructured{binding("all", "", "ClusterRole", "reader", user("ana"))},
})
held := holderFor(t, index, "ana")
if held.Namespaces() != nil {
t.Fatalf("a cluster-wide subject was confined to %+v", held.Namespaces())
}
}
func TestAnAggregatedRoleWithNoRulesYetSaysSo(t *testing.T) {
role := clusterRole("view")
role.Object["aggregationRule"] = map[string]any{"clusterRoleSelectors": []any{}}
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{role},
ClusterBinds: []*unstructured.Unstructured{binding("view", "", "ClusterRole", "view", user("ana"))},
})
held := holderFor(t, index, "ana")
if !held.Grants[0].Aggregated {
t.Fatal("an aggregated role did not say it is filled in by a controller")
}
if held.Grants[0].RuleCount != 0 {
t.Fatalf("rules = %d", held.Grants[0].RuleCount)
}
}
func TestABindingWithNoRoleRefIsSkipped(t *testing.T) {
odd := binding("odd", "web", "Role", "reader", user("ana"))
delete(odd.Object, "roleRef")
index := Build(Held{Bindings: []*unstructured.Unstructured{odd}})
if len(index.Holders) != 0 {
t.Fatalf("holders = %+v", labels(index))
}
}
func TestASubjectWithNoNameIsSkipped(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{role("reader", "web", rule([]string{"get"}, []string{""}, []string{"pods"}))},
Bindings: []*unstructured.Unstructured{binding("read", "web", "Role", "reader", map[string]any{"kind": "User"})},
})
if len(index.Holders) != 0 {
t.Fatalf("holders = %+v", labels(index))
}
}
func TestAGroupKeepsItsOwnName(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{clusterRole("reader", rule([]string{"get"}, []string{""}, []string{"pods"}))},
ClusterBinds: []*unstructured.Unstructured{
binding("all", "", "ClusterRole", "reader", map[string]any{"kind": "Group", "name": "system:authenticated"}),
},
})
held := holderFor(t, index, "system:authenticated")
if held.Subject.Kind != KindGroup {
t.Fatalf("the subject came back as %+v", held.Subject)
}
}
func TestOneSubjectBoundTwiceKeepsBothGrants(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{clusterRole("reader", rule([]string{"get"}, []string{""}, []string{"pods"}))},
ClusterBinds: []*unstructured.Unstructured{
binding("one", "", "ClusterRole", "reader", user("ana")),
binding("two", "", "ClusterRole", "reader", user("ana")),
},
})
held := holderFor(t, index, "ana")
if len(held.Grants) != 2 {
t.Fatalf("grants = %+v", held.Grants)
}
}
func TestAClusterBindingWithAMissingRoleIsNamedWithoutANamespace(t *testing.T) {
index := Build(Held{
ClusterBinds: []*unstructured.Unstructured{binding("gone", "", "ClusterRole", "nowhere", user("ana"))},
})
if len(index.Absent) != 1 || index.Absent[0] != "ClusterRoleBinding gone wants ClusterRole nowhere" {
t.Fatalf("absent = %+v", index.Absent)
}
}
func TestARuleThatIsNotAMapIsSkipped(t *testing.T) {
odd := clusterRole("odd", "not a rule")
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{odd},
ClusterBinds: []*unstructured.Unstructured{binding("odd", "", "ClusterRole", "odd", user("ana"))},
})
held := holderFor(t, index, "ana")
if held.Grants[0].RuleCount != 0 {
t.Fatalf("rules = %d", held.Grants[0].RuleCount)
}
}
func TestASubjectThatIsNotAMapIsSkipped(t *testing.T) {
odd := binding("odd", "", "ClusterRole", "reader", "not a subject", user("ana"))
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{clusterRole("reader", rule([]string{"get"}, []string{""}, []string{"pods"}))},
ClusterBinds: []*unstructured.Unstructured{odd},
})
if len(index.Holders) != 1 {
t.Fatalf("holders = %+v", labels(index))
}
}
func TestAVerbListThatIsNotStringsIsIgnored(t *testing.T) {
odd := clusterRole("odd", map[string]any{
"verbs": []any{true},
"apiGroups": []any{""},
"resources": []any{"pods"},
})
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{odd},
ClusterBinds: []*unstructured.Unstructured{binding("odd", "", "ClusterRole", "odd", user("ana"))},
})
held := holderFor(t, index, "ana")
if held.Grants[0].Reaches(Ask{Verb: "get", Resource: "pods"}) {
t.Fatal("a rule with no readable verbs granted something")
}
}
func TestAServiceAccountWithNoNamespaceAnywhereKeepsItsBareName(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{clusterRole("reader", rule([]string{"get"}, []string{""}, []string{"pods"}))},
ClusterBinds: []*unstructured.Unstructured{
binding("all", "", "ClusterRole", "reader", map[string]any{"kind": "ServiceAccount", "name": "orphan"}),
},
})
holderFor(t, index, "orphan")
}
func TestTwoSubjectsWithTheSamePowersAreOrderedByName(t *testing.T) {
index := Build(Held{
ClusterRoles: []*unstructured.Unstructured{clusterRole("reader", rule([]string{"get"}, []string{""}, []string{"configmaps"}))},
ClusterBinds: []*unstructured.Unstructured{
binding("one", "", "ClusterRole", "reader", user("zoe")),
binding("two", "", "ClusterRole", "reader", user("ana")),
},
})
if index.Holders[0].Subject.Name != "ana" {
t.Fatalf("the order was %+v", labels(index))
}
}
func TestSubjectsWithTheSamePowersAreOrderedByGrantCount(t *testing.T) {
holders := map[string]*Holder{
"one": {
Subject: Subject{Kind: KindUser, Name: "one"},
Grants: []Grant{{Role: "reader"}},
},
"two": {
Subject: Subject{Kind: KindUser, Name: "two"},
Grants: []Grant{{Role: "reader"}, {Role: "writer"}},
},
}
got := ordered(holders)
if got[0].Subject.Name != "two" {
t.Fatalf("holders = %+v, want the holder with more grants first", got)
}
}
func namedRule(verbs, groups, resources, names []string) map[string]any {
one := rule(verbs, groups, resources)
one["resourceNames"] = asAny(names)
return one
}
func TestARuleTiedToNamedObjectsDoesNotAnswerForAllOfThem(t *testing.T) {
one := Rule{
Verbs: []string{"get"},
Groups: []string{""},
Resources: []string{"secrets"},
Names: []string{"forgejo-pg-app"},
}
if one.Allows(Ask{Verb: "get", Resource: "secrets"}) {
t.Fatal("a rule that only names one secret answered for every secret, which the apiserver denies")
}
}
func TestARuleWithNoNamesStillAnswers(t *testing.T) {
one := Rule{Verbs: []string{"get"}, Groups: []string{""}, Resources: []string{"secrets"}}
if !one.Allows(Ask{Verb: "get", Resource: "secrets"}) {
t.Fatal("an unrestricted rule stopped answering")
}
}
func TestASubjectHeldToNamedSecretsIsNotSaidToReadSecrets(t *testing.T) {
index := Build(Held{
Roles: []*unstructured.Unstructured{
role("pg", "forgejo",
namedRule([]string{"get"}, []string{""}, []string{"secrets"}, []string{"forgejo-pg-app"})),
},
Bindings: []*unstructured.Unstructured{
binding("pg", "forgejo", "Role", "pg", account("forgejo-pg", "forgejo")),
},
})
held := holderFor(t, index, "system:serviceaccount:forgejo:forgejo-pg")
if slices.Contains(held.Powers, "reads secrets") {
t.Fatalf("powers = %+v, want no claim the apiserver would deny", held.Powers)
}
if len(index.Who(Ask{Verb: "get", Resource: "secrets", Namespace: "forgejo"})) != 0 {
t.Fatal("a subject held to one named secret came back as able to get secrets")
}
}