mirror of
https://github.com/rancher/steve.git
synced 2025-06-24 22:12:02 +00:00
refactor(accesscontrol): deterministic cache key hashing (#292)
* refactor(accesscontrol): make addAccess directly accept PolicyRules * refactor(accesscontrol): add new types for encapsulating all needed data * refactor(accesscontrol): make getRules return resource version * refactor(accesscontrol): add new getRoleRefs to policyRuleIndex * refactor(accesscontrol): make accessStore use the new types and method * cleanup(accesscontrol): remove unused code * cleanup(accesscontrol): adapt tests * cleanup(accesscontrol): add some comments and remove unused function * refactor(accesscontrol): rework indexer to make it more readable and testable * Fix typo * test: consistent use of t.Error * test: refactor policyRulesMock to just use a map * misc: rename toUserInfo function * refactor: consistent sort by UID
This commit is contained in:
parent
06c2eb50d1
commit
fd9a516ecb
@ -2,15 +2,12 @@ package accesscontrol
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"slices"
|
||||||
"encoding/hex"
|
|
||||||
"hash"
|
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
|
v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/util/cache"
|
"k8s.io/apimachinery/pkg/util/cache"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
)
|
)
|
||||||
@ -23,13 +20,7 @@ type AccessSetLookup interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type policyRules interface {
|
type policyRules interface {
|
||||||
get(string) *AccessSet
|
getRoleRefs(subjectName string) subjectGrants
|
||||||
getRoleBindings(string) []*rbacv1.RoleBinding
|
|
||||||
getClusterRoleBindings(string) []*rbacv1.ClusterRoleBinding
|
|
||||||
}
|
|
||||||
|
|
||||||
type roleRevisions interface {
|
|
||||||
roleRevision(string, string) string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// accessStoreCache is a subset of the methods implemented by LRUExpireCache
|
// accessStoreCache is a subset of the methods implemented by LRUExpireCache
|
||||||
@ -42,21 +33,14 @@ type accessStoreCache interface {
|
|||||||
type AccessStore struct {
|
type AccessStore struct {
|
||||||
usersPolicyRules policyRules
|
usersPolicyRules policyRules
|
||||||
groupsPolicyRules policyRules
|
groupsPolicyRules policyRules
|
||||||
roles roleRevisions
|
|
||||||
cache accessStoreCache
|
cache accessStoreCache
|
||||||
concurrentAccessFor *singleflight.Group
|
concurrentAccessFor *singleflight.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
type roleKey struct {
|
func NewAccessStore(_ context.Context, cacheResults bool, rbac v1.Interface) *AccessStore {
|
||||||
namespace string
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAccessStore(ctx context.Context, cacheResults bool, rbac v1.Interface) *AccessStore {
|
|
||||||
as := &AccessStore{
|
as := &AccessStore{
|
||||||
usersPolicyRules: newPolicyRuleIndex(true, rbac),
|
usersPolicyRules: newPolicyRuleIndex(true, rbac),
|
||||||
groupsPolicyRules: newPolicyRuleIndex(false, rbac),
|
groupsPolicyRules: newPolicyRuleIndex(false, rbac),
|
||||||
roles: newRoleRevision(ctx, rbac),
|
|
||||||
concurrentAccessFor: new(singleflight.Group),
|
concurrentAccessFor: new(singleflight.Group),
|
||||||
}
|
}
|
||||||
if cacheResults {
|
if cacheResults {
|
||||||
@ -66,11 +50,12 @@ func NewAccessStore(ctx context.Context, cacheResults bool, rbac v1.Interface) *
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessStore) AccessFor(user user.Info) *AccessSet {
|
func (l *AccessStore) AccessFor(user user.Info) *AccessSet {
|
||||||
|
info := l.userGrantsFor(user)
|
||||||
if l.cache == nil {
|
if l.cache == nil {
|
||||||
return l.newAccessSet(user)
|
return l.newAccessSet(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheKey := l.CacheKey(user)
|
cacheKey := info.hash()
|
||||||
|
|
||||||
res, _, _ := l.concurrentAccessFor.Do(cacheKey, func() (interface{}, error) {
|
res, _, _ := l.concurrentAccessFor.Do(cacheKey, func() (interface{}, error) {
|
||||||
if val, ok := l.cache.Get(cacheKey); ok {
|
if val, ok := l.cache.Get(cacheKey); ok {
|
||||||
@ -78,7 +63,7 @@ func (l *AccessStore) AccessFor(user user.Info) *AccessSet {
|
|||||||
return as, nil
|
return as, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := l.newAccessSet(user)
|
result := l.newAccessSet(info)
|
||||||
result.ID = cacheKey
|
result.ID = cacheKey
|
||||||
l.cache.Add(cacheKey, result, 24*time.Hour)
|
l.cache.Add(cacheKey, result, 24*time.Hour)
|
||||||
|
|
||||||
@ -87,10 +72,10 @@ func (l *AccessStore) AccessFor(user user.Info) *AccessSet {
|
|||||||
return res.(*AccessSet)
|
return res.(*AccessSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessStore) newAccessSet(user user.Info) *AccessSet {
|
func (l *AccessStore) newAccessSet(info userGrants) *AccessSet {
|
||||||
result := l.usersPolicyRules.get(user.GetName())
|
result := info.user.toAccessSet()
|
||||||
for _, group := range user.GetGroups() {
|
for _, group := range info.groups {
|
||||||
result.Merge(l.groupsPolicyRules.get(group))
|
result.Merge(group.toAccessSet())
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -99,33 +84,17 @@ func (l *AccessStore) PurgeUserData(id string) {
|
|||||||
l.cache.Remove(id)
|
l.cache.Remove(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessStore) CacheKey(user user.Info) string {
|
// userGrantsFor retrieves all the access information for a user
|
||||||
d := sha256.New()
|
func (l *AccessStore) userGrantsFor(user user.Info) userGrants {
|
||||||
|
var res userGrants
|
||||||
|
|
||||||
groupBase := user.GetGroups()
|
groups := slices.Clone(user.GetGroups())
|
||||||
groups := make([]string, len(groupBase))
|
|
||||||
copy(groups, groupBase)
|
|
||||||
sort.Strings(groups)
|
sort.Strings(groups)
|
||||||
|
|
||||||
l.addRolesToHash(d, user.GetName(), l.usersPolicyRules)
|
res.user = l.usersPolicyRules.getRoleRefs(user.GetName())
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
l.addRolesToHash(d, group, l.groupsPolicyRules)
|
res.groups = append(res.groups, l.groupsPolicyRules.getRoleRefs(group))
|
||||||
}
|
}
|
||||||
|
|
||||||
return hex.EncodeToString(d.Sum(nil))
|
return res
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AccessStore) addRolesToHash(digest hash.Hash, subjectName string, rules policyRules) {
|
|
||||||
for _, crb := range rules.getClusterRoleBindings(subjectName) {
|
|
||||||
digest.Write([]byte(crb.RoleRef.Name))
|
|
||||||
digest.Write([]byte(l.roles.roleRevision("", crb.RoleRef.Name)))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rb := range rules.getRoleBindings(subjectName) {
|
|
||||||
digest.Write([]byte(rb.RoleRef.Name))
|
|
||||||
if rb.Namespace != "" {
|
|
||||||
digest.Write([]byte(rb.Namespace))
|
|
||||||
}
|
|
||||||
digest.Write([]byte(l.roles.roleRevision(rb.Namespace, rb.RoleRef.Name)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package accesscontrol
|
package accesscontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@ -11,12 +10,19 @@ import (
|
|||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/util/cache"
|
"k8s.io/apimachinery/pkg/util/cache"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAccessStore_CacheKey(t *testing.T) {
|
type policyRulesMock struct {
|
||||||
|
roleRefs map[string]subjectGrants
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p policyRulesMock) getRoleRefs(s string) subjectGrants {
|
||||||
|
return p.roleRefs[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessStore_userGrantsFor(t *testing.T) {
|
||||||
testUser := &user.DefaultInfo{
|
testUser := &user.DefaultInfo{
|
||||||
Name: "user-12345",
|
Name: "user-12345",
|
||||||
Groups: []string{
|
Groups: []string{
|
||||||
@ -34,27 +40,24 @@ func TestAccessStore_CacheKey(t *testing.T) {
|
|||||||
name: "consistently produces the same value",
|
name: "consistently produces the same value",
|
||||||
store: &AccessStore{
|
store: &AccessStore{
|
||||||
usersPolicyRules: &policyRulesMock{
|
usersPolicyRules: &policyRulesMock{
|
||||||
getRBFunc: func(s string) []*rbacv1.RoleBinding {
|
roleRefs: map[string]subjectGrants{
|
||||||
return []*rbacv1.RoleBinding{
|
testUser.Name: {
|
||||||
makeRB("testns", "testrb", testUser.Name, "testrole"),
|
roleBindings: []roleRef{
|
||||||
}
|
{namespace: "testns", roleName: "testrole", resourceVersion: "testrolev1"},
|
||||||
},
|
},
|
||||||
getCRBFunc: func(s string) []*rbacv1.ClusterRoleBinding {
|
clusterRoleBindings: []roleRef{
|
||||||
return []*rbacv1.ClusterRoleBinding{
|
{roleName: "testclusterrole", resourceVersion: "testclusterrolev1"},
|
||||||
makeCRB("testcrb", testUser.Name, "testclusterrole"),
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
groupsPolicyRules: &policyRulesMock{},
|
groupsPolicyRules: &policyRulesMock{},
|
||||||
roles: roleRevisionsMock(func(ns, name string) string {
|
|
||||||
return fmt.Sprintf("%s%srev", ns, name)
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
verify: func(t *testing.T, store *AccessStore, res string) {
|
verify: func(t *testing.T, store *AccessStore, res string) {
|
||||||
// iterate enough times to make possibly random iterators repeating order by coincidence
|
// iterate enough times to make possibly random iterators repeating order by coincidence
|
||||||
for range 5 {
|
for range 5 {
|
||||||
if res != store.CacheKey(testUser) {
|
if res != store.userGrantsFor(testUser).hash() {
|
||||||
t.Fatal("CacheKey is not the same on consecutive runs")
|
t.Fatal("hash is not the same on consecutive runs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -64,27 +67,26 @@ func TestAccessStore_CacheKey(t *testing.T) {
|
|||||||
store: &AccessStore{
|
store: &AccessStore{
|
||||||
usersPolicyRules: &policyRulesMock{},
|
usersPolicyRules: &policyRulesMock{},
|
||||||
groupsPolicyRules: &policyRulesMock{
|
groupsPolicyRules: &policyRulesMock{
|
||||||
getRBFunc: func(s string) []*rbacv1.RoleBinding {
|
roleRefs: map[string]subjectGrants{
|
||||||
return []*rbacv1.RoleBinding{
|
testUser.Groups[0]: {
|
||||||
makeRB("testns", "testrb", testUser.Name, "testrole"),
|
roleBindings: []roleRef{
|
||||||
}
|
{namespace: "testns", roleName: "testrole", resourceVersion: "testrolegroup0"},
|
||||||
},
|
},
|
||||||
getCRBFunc: func(s string) []*rbacv1.ClusterRoleBinding {
|
},
|
||||||
return []*rbacv1.ClusterRoleBinding{
|
testUser.Groups[1]: {
|
||||||
makeCRB("testcrb", testUser.Name, "testclusterrole"),
|
clusterRoleBindings: []roleRef{
|
||||||
}
|
{roleName: "testclusterrole", resourceVersion: "testclusterrolegroup1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
roles: roleRevisionsMock(func(ns, name string) string {
|
|
||||||
return fmt.Sprintf("%s%srev", ns, name)
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
verify: func(t *testing.T, store *AccessStore, res string) {
|
verify: func(t *testing.T, store *AccessStore, res string) {
|
||||||
// remove users
|
// remove groups
|
||||||
testUserAlt := *testUser
|
testUserAlt := *testUser
|
||||||
testUserAlt.Groups = []string{}
|
testUserAlt.Groups = []string{}
|
||||||
|
|
||||||
if store.CacheKey(&testUserAlt) == res {
|
if store.userGrantsFor(&testUserAlt).hash() == res {
|
||||||
t.Fatal("CacheKey does not use groups for hashing")
|
t.Fatal("CacheKey does not use groups for hashing")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -94,33 +96,26 @@ func TestAccessStore_CacheKey(t *testing.T) {
|
|||||||
store: &AccessStore{
|
store: &AccessStore{
|
||||||
usersPolicyRules: &policyRulesMock{},
|
usersPolicyRules: &policyRulesMock{},
|
||||||
groupsPolicyRules: &policyRulesMock{
|
groupsPolicyRules: &policyRulesMock{
|
||||||
getRBFunc: func(s string) []*rbacv1.RoleBinding {
|
roleRefs: map[string]subjectGrants{
|
||||||
if s == testUser.Groups[0] {
|
testUser.Groups[0]: {
|
||||||
return []*rbacv1.RoleBinding{
|
roleBindings: []roleRef{
|
||||||
makeRB("testns", "testrb", testUser.Name, "testrole"),
|
{namespace: "testns", roleName: "testrole", resourceVersion: "testrolegroup0"},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
return nil
|
testUser.Groups[0]: {
|
||||||
},
|
clusterRoleBindings: []roleRef{
|
||||||
getCRBFunc: func(s string) []*rbacv1.ClusterRoleBinding {
|
{roleName: "testclusterrole", resourceVersion: "testclusterrolegroup1"},
|
||||||
if s == testUser.Groups[1] {
|
},
|
||||||
return []*rbacv1.ClusterRoleBinding{
|
},
|
||||||
makeCRB("testcrb", testUser.Name, "testclusterrole"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
roles: roleRevisionsMock(func(ns, name string) string {
|
|
||||||
return fmt.Sprintf("%s%srev", ns, name)
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
verify: func(t *testing.T, store *AccessStore, res string) {
|
verify: func(t *testing.T, store *AccessStore, res string) {
|
||||||
// swap order of groups
|
// swap order of groups
|
||||||
testUserAlt := &user.DefaultInfo{Name: testUser.Name, Groups: slices.Clone(testUser.Groups)}
|
testUserAlt := &user.DefaultInfo{Name: testUser.Name, Groups: slices.Clone(testUser.Groups)}
|
||||||
slices.Reverse(testUserAlt.Groups)
|
slices.Reverse(testUserAlt.Groups)
|
||||||
|
|
||||||
if store.CacheKey(testUserAlt) != res {
|
if store.userGrantsFor(testUserAlt).hash() != res {
|
||||||
t.Fatal("CacheKey varies depending on groups order")
|
t.Fatal("CacheKey varies depending on groups order")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -129,23 +124,28 @@ func TestAccessStore_CacheKey(t *testing.T) {
|
|||||||
name: "role changes produce a different value",
|
name: "role changes produce a different value",
|
||||||
store: &AccessStore{
|
store: &AccessStore{
|
||||||
usersPolicyRules: &policyRulesMock{
|
usersPolicyRules: &policyRulesMock{
|
||||||
getRBFunc: func(s string) []*rbacv1.RoleBinding {
|
roleRefs: map[string]subjectGrants{
|
||||||
return []*rbacv1.RoleBinding{
|
testUser.Name: {
|
||||||
makeRB("testns", "testrb", testUser.Name, "testrole"),
|
roleBindings: []roleRef{
|
||||||
}
|
{namespace: "testns", roleName: "testrole", resourceVersion: "testrolev1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
groupsPolicyRules: &policyRulesMock{},
|
groupsPolicyRules: &policyRulesMock{},
|
||||||
roles: roleRevisionsMock(func(ns, name string) string {
|
|
||||||
return "rev1"
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
verify: func(t *testing.T, store *AccessStore, res string) {
|
verify: func(t *testing.T, store *AccessStore, res string) {
|
||||||
store.roles = roleRevisionsMock(func(ns, name string) string {
|
// new mock returns different resource version
|
||||||
return "rev2"
|
store.usersPolicyRules = &policyRulesMock{
|
||||||
|
roleRefs: map[string]subjectGrants{
|
||||||
})
|
testUser.Name: {
|
||||||
if store.CacheKey(testUser) == res {
|
roleBindings: []roleRef{
|
||||||
|
{namespace: "testns", roleName: "testrole", resourceVersion: "testrolev2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if store.userGrantsFor(testUser).hash() == res {
|
||||||
t.Fatal("CacheKey did not change when on role change")
|
t.Fatal("CacheKey did not change when on role change")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -155,30 +155,26 @@ func TestAccessStore_CacheKey(t *testing.T) {
|
|||||||
store: &AccessStore{
|
store: &AccessStore{
|
||||||
usersPolicyRules: &policyRulesMock{},
|
usersPolicyRules: &policyRulesMock{},
|
||||||
groupsPolicyRules: &policyRulesMock{
|
groupsPolicyRules: &policyRulesMock{
|
||||||
getRBFunc: func(s string) []*rbacv1.RoleBinding {
|
roleRefs: map[string]subjectGrants{
|
||||||
return []*rbacv1.RoleBinding{
|
testUser.Groups[0]: {
|
||||||
makeRB("testns", "testrb", testUser.Name, "testrole"),
|
roleBindings: []roleRef{
|
||||||
}
|
{namespace: "testns", roleName: "testrole", resourceVersion: "testrolegroup0"},
|
||||||
},
|
},
|
||||||
getCRBFunc: func(s string) []*rbacv1.ClusterRoleBinding {
|
},
|
||||||
if s == "newgroup" {
|
"newgroup": {
|
||||||
return []*rbacv1.ClusterRoleBinding{
|
clusterRoleBindings: []roleRef{
|
||||||
makeCRB("testcrb", testUser.Name, "testclusterrole"),
|
{roleName: "testclusterrole", resourceVersion: "testclusterrolegroup1"},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
roles: roleRevisionsMock(func(ns, name string) string {
|
|
||||||
return fmt.Sprintf("%s%srev", ns, name)
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
verify: func(t *testing.T, store *AccessStore, res string) {
|
verify: func(t *testing.T, store *AccessStore, res string) {
|
||||||
testUserAlt := &user.DefaultInfo{
|
testUserAlt := &user.DefaultInfo{
|
||||||
Name: testUser.Name,
|
Name: testUser.Name,
|
||||||
Groups: append(slices.Clone(testUser.Groups), "newgroup"),
|
Groups: append(slices.Clone(testUser.Groups), "newgroup"),
|
||||||
}
|
}
|
||||||
if store.CacheKey(testUserAlt) == res {
|
if store.userGrantsFor(testUserAlt).hash() == res {
|
||||||
t.Fatal("CacheKey did not change when new group was added")
|
t.Fatal("CacheKey did not change when new group was added")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -186,7 +182,7 @@ func TestAccessStore_CacheKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tt.verify(t, tt.store, tt.store.CacheKey(testUser))
|
tt.verify(t, tt.store, tt.store.userGrantsFor(testUser).hash())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,43 +196,39 @@ func TestAccessStore_AccessFor(t *testing.T) {
|
|||||||
store := &AccessStore{
|
store := &AccessStore{
|
||||||
concurrentAccessFor: new(singleflight.Group),
|
concurrentAccessFor: new(singleflight.Group),
|
||||||
usersPolicyRules: &policyRulesMock{
|
usersPolicyRules: &policyRulesMock{
|
||||||
getRBFunc: func(s string) []*rbacv1.RoleBinding {
|
roleRefs: map[string]subjectGrants{
|
||||||
return []*rbacv1.RoleBinding{
|
testUser.Name: {
|
||||||
makeRB("testns", "testrb", testUser.Name, "testrole"),
|
clusterRoleBindings: []roleRef{
|
||||||
}
|
{
|
||||||
},
|
roleName: "testclusterrole", resourceVersion: "testclusterrolev1",
|
||||||
getFunc: func(_ string) *AccessSet {
|
rules: []rbacv1.PolicyRule{{
|
||||||
return &AccessSet{
|
Verbs: []string{"get"},
|
||||||
set: map[key]resourceAccessSet{
|
APIGroups: []string{corev1.GroupName},
|
||||||
{"get", corev1.Resource("ConfigMap")}: map[Access]bool{
|
Resources: []string{"ConfigMap"},
|
||||||
{Namespace: All, ResourceName: All}: true,
|
ResourceNames: []string{All},
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
groupsPolicyRules: &policyRulesMock{
|
groupsPolicyRules: &policyRulesMock{
|
||||||
getCRBFunc: func(s string) []*rbacv1.ClusterRoleBinding {
|
roleRefs: map[string]subjectGrants{
|
||||||
if s == "mygroup" {
|
testUser.Groups[0]: {
|
||||||
return []*rbacv1.ClusterRoleBinding{
|
roleBindings: []roleRef{
|
||||||
makeCRB("testcrb", testUser.Name, "testclusterrole"),
|
{
|
||||||
}
|
namespace: "testns", roleName: "testrole", resourceVersion: "testrolev1",
|
||||||
}
|
rules: []rbacv1.PolicyRule{{
|
||||||
return nil
|
Verbs: []string{"list"},
|
||||||
},
|
APIGroups: []string{appsv1.GroupName},
|
||||||
getFunc: func(_ string) *AccessSet {
|
Resources: []string{"Deployment"},
|
||||||
return &AccessSet{
|
ResourceNames: []string{All},
|
||||||
set: map[key]resourceAccessSet{
|
}},
|
||||||
{"list", appsv1.Resource("Deployment")}: map[Access]bool{
|
|
||||||
{Namespace: "testns", ResourceName: All}: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
roles: roleRevisionsMock(func(ns, name string) string {
|
|
||||||
return fmt.Sprintf("%s%srev", ns, name)
|
|
||||||
}),
|
|
||||||
cache: asCache,
|
cache: asCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,12 +239,14 @@ func TestAccessStore_AccessFor(t *testing.T) {
|
|||||||
if as.ID == "" {
|
if as.ID == "" {
|
||||||
t.Fatal("AccessSet has empty ID")
|
t.Fatal("AccessSet has empty ID")
|
||||||
}
|
}
|
||||||
if !as.Grants("get", corev1.Resource("ConfigMap"), "default", "cm") ||
|
if !as.Grants("get", corev1.Resource("ConfigMap"), "anyns", "cm") ||
|
||||||
!as.Grants("list", appsv1.Resource("Deployment"), "testns", "deploy") {
|
!as.Grants("list", appsv1.Resource("Deployment"), "testns", "deploy") {
|
||||||
t.Error("AccessSet does not grant desired permissions")
|
t.Error("AccessSet does not grant desired permissions")
|
||||||
}
|
}
|
||||||
// wrong verbs
|
// wrong ns
|
||||||
if as.Grants("delete", corev1.Resource("ConfigMap"), "default", "cm") ||
|
if as.Grants("list", appsv1.Resource("Deployment"), "default", "deploy") ||
|
||||||
|
// wrong verbs
|
||||||
|
as.Grants("delete", corev1.Resource("ConfigMap"), "default", "cm") ||
|
||||||
as.Grants("get", appsv1.Resource("Deployment"), "testns", "deploy") ||
|
as.Grants("get", appsv1.Resource("Deployment"), "testns", "deploy") ||
|
||||||
// wrong resource
|
// wrong resource
|
||||||
as.Grants("get", corev1.Resource("Secret"), "testns", "s") {
|
as.Grants("get", corev1.Resource("Secret"), "testns", "s") {
|
||||||
@ -307,23 +301,13 @@ func TestAccessStore_AccessFor_concurrent(t *testing.T) {
|
|||||||
asCache := &spyCache{accessStoreCache: cache.NewLRUExpireCache(100)}
|
asCache := &spyCache{accessStoreCache: cache.NewLRUExpireCache(100)}
|
||||||
store := &AccessStore{
|
store := &AccessStore{
|
||||||
concurrentAccessFor: new(singleflight.Group),
|
concurrentAccessFor: new(singleflight.Group),
|
||||||
roles: roleRevisionsMock(func(ns, name string) string {
|
|
||||||
return fmt.Sprintf("%s%srev", ns, name)
|
|
||||||
}),
|
|
||||||
usersPolicyRules: &policyRulesMock{
|
usersPolicyRules: &policyRulesMock{
|
||||||
getRBFunc: func(s string) []*rbacv1.RoleBinding {
|
roleRefs: map[string]subjectGrants{
|
||||||
return []*rbacv1.RoleBinding{
|
testUser.Name: {
|
||||||
makeRB("testns", "testrb", testUser.Name, "testrole"),
|
roleBindings: []roleRef{
|
||||||
}
|
{namespace: "testns", roleName: "testrole", resourceVersion: "testrolev1"},
|
||||||
},
|
|
||||||
getFunc: func(_ string) *AccessSet {
|
|
||||||
return &AccessSet{
|
|
||||||
set: map[key]resourceAccessSet{
|
|
||||||
{"get", corev1.Resource("ConfigMap")}: map[Access]bool{
|
|
||||||
{Namespace: All, ResourceName: All}: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cache: asCache,
|
cache: asCache,
|
||||||
@ -352,56 +336,3 @@ func TestAccessStore_AccessFor_concurrent(t *testing.T) {
|
|||||||
t.Errorf("Unexpected number of calls to cache.Set(): got %d, want %d", got, want)
|
t.Errorf("Unexpected number of calls to cache.Set(): got %d, want %d", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeRB(ns, name, user, role string) *rbacv1.RoleBinding {
|
|
||||||
return &rbacv1.RoleBinding{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name},
|
|
||||||
Subjects: []rbacv1.Subject{
|
|
||||||
{Name: user},
|
|
||||||
},
|
|
||||||
RoleRef: rbacv1.RoleRef{Name: role},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeCRB(name, user, role string) *rbacv1.ClusterRoleBinding {
|
|
||||||
return &rbacv1.ClusterRoleBinding{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: name},
|
|
||||||
Subjects: []rbacv1.Subject{
|
|
||||||
{Name: user},
|
|
||||||
},
|
|
||||||
RoleRef: rbacv1.RoleRef{Name: role},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type policyRulesMock struct {
|
|
||||||
getFunc func(string) *AccessSet
|
|
||||||
getRBFunc func(string) []*rbacv1.RoleBinding
|
|
||||||
getCRBFunc func(string) []*rbacv1.ClusterRoleBinding
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p policyRulesMock) get(s string) *AccessSet {
|
|
||||||
if p.getFunc == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p.getFunc(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p policyRulesMock) getRoleBindings(s string) []*rbacv1.RoleBinding {
|
|
||||||
if p.getRBFunc == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p.getRBFunc(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p policyRulesMock) getClusterRoleBindings(s string) []*rbacv1.ClusterRoleBinding {
|
|
||||||
if p.getCRBFunc == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p.getCRBFunc(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
type roleRevisionsMock func(ns, name string) string
|
|
||||||
|
|
||||||
func (fn roleRevisionsMock) roleRevision(ns, name string) string {
|
|
||||||
return fn(ns, name)
|
|
||||||
}
|
|
||||||
|
@ -4,33 +4,35 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
|
rbacv1controllers "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rbacGroup = "rbac.authorization.k8s.io"
|
rbacGroup = rbacv1.GroupName
|
||||||
All = "*"
|
All = "*"
|
||||||
|
|
||||||
|
groupKind = rbacv1.GroupKind
|
||||||
|
userKind = rbacv1.UserKind
|
||||||
|
svcAccountKind = rbacv1.ServiceAccountKind
|
||||||
)
|
)
|
||||||
|
|
||||||
type policyRuleIndex struct {
|
type policyRuleIndex struct {
|
||||||
crCache v1.ClusterRoleCache
|
crCache rbacv1controllers.ClusterRoleCache
|
||||||
rCache v1.RoleCache
|
rCache rbacv1controllers.RoleCache
|
||||||
crbCache v1.ClusterRoleBindingCache
|
crbCache rbacv1controllers.ClusterRoleBindingCache
|
||||||
rbCache v1.RoleBindingCache
|
rbCache rbacv1controllers.RoleBindingCache
|
||||||
kind string
|
|
||||||
roleIndexKey string
|
roleIndexKey string
|
||||||
clusterRoleIndexKey string
|
clusterRoleIndexKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPolicyRuleIndex(user bool, rbac v1.Interface) *policyRuleIndex {
|
func newPolicyRuleIndex(user bool, rbac rbacv1controllers.Interface) *policyRuleIndex {
|
||||||
key := "Group"
|
key := groupKind
|
||||||
if user {
|
if user {
|
||||||
key = "User"
|
key = userKind
|
||||||
}
|
}
|
||||||
pi := &policyRuleIndex{
|
pi := &policyRuleIndex{
|
||||||
kind: key,
|
|
||||||
crCache: rbac.ClusterRole().Cache(),
|
crCache: rbac.ClusterRole().Cache(),
|
||||||
rCache: rbac.Role().Cache(),
|
rCache: rbac.Role().Cache(),
|
||||||
crbCache: rbac.ClusterRoleBinding().Cache(),
|
crbCache: rbac.ClusterRoleBinding().Cache(),
|
||||||
@ -39,52 +41,51 @@ func newPolicyRuleIndex(user bool, rbac v1.Interface) *policyRuleIndex {
|
|||||||
roleIndexKey: "rb" + key,
|
roleIndexKey: "rb" + key,
|
||||||
}
|
}
|
||||||
|
|
||||||
pi.crbCache.AddIndexer(pi.clusterRoleIndexKey, pi.clusterRoleBindingBySubjectIndexer)
|
pi.crbCache.AddIndexer(pi.clusterRoleIndexKey, clusterRoleBindingBySubjectIndexer(key))
|
||||||
pi.rbCache.AddIndexer(pi.roleIndexKey, pi.roleBindingBySubject)
|
pi.rbCache.AddIndexer(pi.roleIndexKey, roleBindingBySubjectIndexer(key))
|
||||||
|
|
||||||
return pi
|
return pi
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *policyRuleIndex) clusterRoleBindingBySubjectIndexer(crb *rbacv1.ClusterRoleBinding) (result []string, err error) {
|
func clusterRoleBindingBySubjectIndexer(kind string) func(crb *rbacv1.ClusterRoleBinding) ([]string, error) {
|
||||||
for _, subject := range crb.Subjects {
|
return func(crb *rbacv1.ClusterRoleBinding) ([]string, error) {
|
||||||
if subject.APIGroup == rbacGroup && subject.Kind == p.kind && crb.RoleRef.Kind == "ClusterRole" {
|
if crb.RoleRef.Kind != "ClusterRole" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return indexSubjects(kind, crb.Subjects), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roleBindingBySubjectIndexer(key string) func(rb *rbacv1.RoleBinding) ([]string, error) {
|
||||||
|
return func(rb *rbacv1.RoleBinding) ([]string, error) {
|
||||||
|
return indexSubjects(key, rb.Subjects), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexSubjects(kind string, subjects []rbacv1.Subject) []string {
|
||||||
|
var result []string
|
||||||
|
for _, subject := range subjects {
|
||||||
|
if subjectIs(kind, subject) {
|
||||||
result = append(result, subject.Name)
|
result = append(result, subject.Name)
|
||||||
} else if subject.APIGroup == "" && p.kind == "User" && subject.Kind == "ServiceAccount" && subject.Namespace != "" && crb.RoleRef.Kind == "ClusterRole" {
|
} else if kind == userKind && subjectIsServiceAccount(subject) {
|
||||||
// Index is for Users and this references a service account
|
// Index is for Users and this references a service account
|
||||||
result = append(result, fmt.Sprintf("serviceaccount:%s:%s", subject.Namespace, subject.Name))
|
result = append(result, fmt.Sprintf("serviceaccount:%s:%s", subject.Namespace, subject.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *policyRuleIndex) roleBindingBySubject(rb *rbacv1.RoleBinding) (result []string, err error) {
|
|
||||||
for _, subject := range rb.Subjects {
|
|
||||||
if subject.APIGroup == rbacGroup && subject.Kind == p.kind {
|
|
||||||
result = append(result, subject.Name)
|
|
||||||
} else if subject.APIGroup == "" && p.kind == "User" && subject.Kind == "ServiceAccount" && subject.Namespace != "" {
|
|
||||||
// Index is for Users and this references a service account
|
|
||||||
result = append(result, fmt.Sprintf("serviceaccount:%s:%s", subject.Namespace, subject.Name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *policyRuleIndex) get(subjectName string) *AccessSet {
|
|
||||||
result := &AccessSet{}
|
|
||||||
|
|
||||||
for _, binding := range p.getRoleBindings(subjectName) {
|
|
||||||
p.addAccess(result, binding.Namespace, binding.RoleRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, binding := range p.getClusterRoleBindings(subjectName) {
|
|
||||||
p.addAccess(result, All, binding.RoleRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *policyRuleIndex) addAccess(accessSet *AccessSet, namespace string, roleRef rbacv1.RoleRef) {
|
func subjectIs(kind string, subject rbacv1.Subject) bool {
|
||||||
for _, rule := range p.getRules(namespace, roleRef) {
|
return subject.APIGroup == rbacGroup && subject.Kind == kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func subjectIsServiceAccount(subject rbacv1.Subject) bool {
|
||||||
|
return subject.APIGroup == "" && subject.Kind == svcAccountKind && subject.Namespace != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// addAccess appends a set of PolicyRules to a given AccessSet
|
||||||
|
func addAccess(accessSet *AccessSet, namespace string, rules []rbacv1.PolicyRule) {
|
||||||
|
for _, rule := range rules {
|
||||||
for _, group := range rule.APIGroups {
|
for _, group := range rule.APIGroups {
|
||||||
for _, resource := range rule.Resources {
|
for _, resource := range rule.Resources {
|
||||||
names := rule.ResourceNames
|
names := rule.ResourceNames
|
||||||
@ -108,23 +109,24 @@ func (p *policyRuleIndex) addAccess(accessSet *AccessSet, namespace string, role
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *policyRuleIndex) getRules(namespace string, roleRef rbacv1.RoleRef) []rbacv1.PolicyRule {
|
// getRules obtain the actual Role or ClusterRole pointed at by a RoleRef, and returns PolicyRules and the resource version
|
||||||
|
func (p *policyRuleIndex) getRules(namespace string, roleRef rbacv1.RoleRef) ([]rbacv1.PolicyRule, string) {
|
||||||
switch roleRef.Kind {
|
switch roleRef.Kind {
|
||||||
case "ClusterRole":
|
case "ClusterRole":
|
||||||
role, err := p.crCache.Get(roleRef.Name)
|
role, err := p.crCache.Get(roleRef.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, ""
|
||||||
}
|
}
|
||||||
return role.Rules
|
return role.Rules, role.ResourceVersion
|
||||||
case "Role":
|
case "Role":
|
||||||
role, err := p.rCache.Get(namespace, roleRef.Name)
|
role, err := p.rCache.Get(namespace, roleRef.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, ""
|
||||||
}
|
}
|
||||||
return role.Rules
|
return role.Rules, role.ResourceVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *policyRuleIndex) getClusterRoleBindings(subjectName string) []*rbacv1.ClusterRoleBinding {
|
func (p *policyRuleIndex) getClusterRoleBindings(subjectName string) []*rbacv1.ClusterRoleBinding {
|
||||||
@ -133,7 +135,7 @@ func (p *policyRuleIndex) getClusterRoleBindings(subjectName string) []*rbacv1.C
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sort.Slice(result, func(i, j int) bool {
|
sort.Slice(result, func(i, j int) bool {
|
||||||
return result[i].Name < result[j].Name
|
return result[i].UID < result[j].UID
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -148,3 +150,32 @@ func (p *policyRuleIndex) getRoleBindings(subjectName string) []*rbacv1.RoleBind
|
|||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRoleRefs gathers rules from roles granted to a given subject through RoleBindings and ClusterRoleBindings
|
||||||
|
func (p *policyRuleIndex) getRoleRefs(subjectName string) subjectGrants {
|
||||||
|
var clusterRoleBindings []roleRef
|
||||||
|
for _, crb := range p.getClusterRoleBindings(subjectName) {
|
||||||
|
rules, resourceVersion := p.getRules(All, crb.RoleRef)
|
||||||
|
clusterRoleBindings = append(clusterRoleBindings, roleRef{
|
||||||
|
roleName: crb.RoleRef.Name,
|
||||||
|
resourceVersion: resourceVersion,
|
||||||
|
rules: rules,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var roleBindings []roleRef
|
||||||
|
for _, rb := range p.getRoleBindings(subjectName) {
|
||||||
|
rules, resourceVersion := p.getRules(rb.Namespace, rb.RoleRef)
|
||||||
|
roleBindings = append(roleBindings, roleRef{
|
||||||
|
roleName: rb.RoleRef.Name,
|
||||||
|
namespace: rb.Namespace,
|
||||||
|
resourceVersion: resourceVersion,
|
||||||
|
rules: rules,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return subjectGrants{
|
||||||
|
roleBindings: roleBindings,
|
||||||
|
clusterRoleBindings: clusterRoleBindings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
238
pkg/accesscontrol/policy_rule_index_test.go
Normal file
238
pkg/accesscontrol/policy_rule_index_test.go
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_policyRuleIndex_roleBindingBySubject(t *testing.T) {
|
||||||
|
roleRef := rbacv1.RoleRef{Kind: "Role", Name: "testrole"}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
kind string
|
||||||
|
rb *rbacv1.RoleBinding
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "indexes users",
|
||||||
|
kind: "User",
|
||||||
|
rb: makeRB("testns", "testrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "User",
|
||||||
|
Name: "myuser",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{"myuser"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "indexes multiple subjects",
|
||||||
|
kind: "Group",
|
||||||
|
rb: makeRB("testns", "testrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "Group",
|
||||||
|
Name: "mygroup1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "Group",
|
||||||
|
Name: "mygroup2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{"mygroup1", "mygroup2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "indexes svcaccounts in user mode",
|
||||||
|
kind: "User",
|
||||||
|
rb: makeRB("testns", "testrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: "",
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Name: "mysvcaccount",
|
||||||
|
Namespace: "testns",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{"serviceaccount:testns:mysvcaccount"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignores svcaccounts in group mode",
|
||||||
|
kind: "Group",
|
||||||
|
rb: makeRB("testns", "testrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: "",
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Name: "mysvcaccount",
|
||||||
|
Namespace: "testns",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignores unknown subjects",
|
||||||
|
kind: "Group",
|
||||||
|
rb: makeRB("testns", "testrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "User",
|
||||||
|
Name: "myuser",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "Group",
|
||||||
|
Name: "mygroup1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIGroup: "custom.api.group",
|
||||||
|
Kind: "CustomGroup",
|
||||||
|
Name: "mygroup2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{"mygroup1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
indexFunc := roleBindingBySubjectIndexer(tt.kind)
|
||||||
|
if got, err := indexFunc(tt.rb); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if !slices.Equal(got, tt.want) {
|
||||||
|
t.Errorf("roleBindingBySubjectIndexer() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_policyRuleIndex_clusterRoleBindingBySubject(t *testing.T) {
|
||||||
|
roleRef := rbacv1.RoleRef{Kind: "ClusterRole", Name: "testclusterrole"}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
kind string
|
||||||
|
crb *rbacv1.ClusterRoleBinding
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ignores if RoleRef is a Role",
|
||||||
|
kind: "User",
|
||||||
|
crb: makeCRB("testcrb", rbacv1.RoleRef{Kind: "Role", Name: "testrole"}, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "User",
|
||||||
|
Name: "myuser",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "indexes users",
|
||||||
|
kind: "User",
|
||||||
|
crb: makeCRB("testcrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "User",
|
||||||
|
Name: "myuser",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{"myuser"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "indexes multiple subjects",
|
||||||
|
kind: "Group",
|
||||||
|
crb: makeCRB("testcrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "Group",
|
||||||
|
Name: "mygroup1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "Group",
|
||||||
|
Name: "mygroup2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{"mygroup1", "mygroup2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "indexes svcaccounts in user mode",
|
||||||
|
kind: "User",
|
||||||
|
crb: makeCRB("testcrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: "",
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Name: "mysvcaccount",
|
||||||
|
Namespace: "testns",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{"serviceaccount:testns:mysvcaccount"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignores svcaccounts in group mode",
|
||||||
|
kind: "Group",
|
||||||
|
crb: makeCRB("testcrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: "",
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Name: "mysvcaccount",
|
||||||
|
Namespace: "testns",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignores unknown subjects",
|
||||||
|
kind: "Group",
|
||||||
|
crb: makeCRB("testcrb", roleRef, []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "User",
|
||||||
|
Name: "myuser",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIGroup: rbacGroup,
|
||||||
|
Kind: "Group",
|
||||||
|
Name: "mygroup1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIGroup: "custom.api.group",
|
||||||
|
Kind: "CustomGroup",
|
||||||
|
Name: "mygroup2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []string{"mygroup1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
indexFunc := clusterRoleBindingBySubjectIndexer(tt.kind)
|
||||||
|
if got, err := indexFunc(tt.crb); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if !slices.Equal(got, tt.want) {
|
||||||
|
t.Errorf("clusterRoleBindingBySubjectIndexer() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRB(namespace, name string, roleRef rbacv1.RoleRef, subjects []rbacv1.Subject) *rbacv1.RoleBinding {
|
||||||
|
return &rbacv1.RoleBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
RoleRef: roleRef,
|
||||||
|
Subjects: subjects,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCRB(name string, roleRef rbacv1.RoleRef, subjects []rbacv1.Subject) *rbacv1.ClusterRoleBinding {
|
||||||
|
return &rbacv1.ClusterRoleBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
RoleRef: roleRef,
|
||||||
|
Subjects: subjects,
|
||||||
|
}
|
||||||
|
}
|
@ -1,59 +0,0 @@
|
|||||||
package accesscontrol
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
rbac "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/kv"
|
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type roleRevisionIndex struct {
|
|
||||||
roleRevisions sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRoleRevision(ctx context.Context, rbac rbac.Interface) *roleRevisionIndex {
|
|
||||||
r := &roleRevisionIndex{}
|
|
||||||
rbac.Role().OnChange(ctx, "role-revision-indexer", r.onRoleChanged)
|
|
||||||
rbac.ClusterRole().OnChange(ctx, "role-revision-indexer", r.onClusterRoleChanged)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *roleRevisionIndex) roleRevision(namespace, name string) string {
|
|
||||||
val, _ := r.roleRevisions.Load(roleKey{
|
|
||||||
name: name,
|
|
||||||
namespace: namespace,
|
|
||||||
})
|
|
||||||
revision, _ := val.(string)
|
|
||||||
return revision
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *roleRevisionIndex) onClusterRoleChanged(key string, cr *rbacv1.ClusterRole) (role *rbacv1.ClusterRole, err error) {
|
|
||||||
if cr == nil {
|
|
||||||
r.roleRevisions.Delete(roleKey{
|
|
||||||
name: key,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
r.roleRevisions.Store(roleKey{
|
|
||||||
name: key,
|
|
||||||
}, cr.ResourceVersion)
|
|
||||||
}
|
|
||||||
return cr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *roleRevisionIndex) onRoleChanged(key string, cr *rbacv1.Role) (role *rbacv1.Role, err error) {
|
|
||||||
if cr == nil {
|
|
||||||
namespace, name := kv.Split(key, "/")
|
|
||||||
r.roleRevisions.Delete(roleKey{
|
|
||||||
name: name,
|
|
||||||
namespace: namespace,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
r.roleRevisions.Store(roleKey{
|
|
||||||
name: cr.Name,
|
|
||||||
namespace: cr.Namespace,
|
|
||||||
}, cr.ResourceVersion)
|
|
||||||
}
|
|
||||||
return cr, nil
|
|
||||||
}
|
|
71
pkg/accesscontrol/user_grants.go
Normal file
71
pkg/accesscontrol/user_grants.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"hash"
|
||||||
|
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// userGrants is a complete snapshot of all rules granted to a user, including those through groups memberships
|
||||||
|
type userGrants struct {
|
||||||
|
user subjectGrants
|
||||||
|
groups []subjectGrants
|
||||||
|
}
|
||||||
|
|
||||||
|
// subjectGrants defines role references granted to a given subject through RoleBindings and ClusterRoleBindings
|
||||||
|
type subjectGrants struct {
|
||||||
|
roleBindings []roleRef
|
||||||
|
clusterRoleBindings []roleRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// roleRef contains information from a Role or ClusterRole
|
||||||
|
type roleRef struct {
|
||||||
|
namespace, roleName, resourceVersion string
|
||||||
|
rules []rbacv1.PolicyRule
|
||||||
|
}
|
||||||
|
|
||||||
|
// hash calculates a unique identifier from all the grants for a user
|
||||||
|
func (u userGrants) hash() string {
|
||||||
|
d := sha256.New()
|
||||||
|
u.user.writeTo(d)
|
||||||
|
for _, group := range u.groups {
|
||||||
|
group.writeTo(d)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(d.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTo appends a subject's grants information to a given hash
|
||||||
|
func (b subjectGrants) writeTo(digest hash.Hash) {
|
||||||
|
for _, rb := range b.roleBindings {
|
||||||
|
rb.writeTo(digest)
|
||||||
|
}
|
||||||
|
for _, crb := range b.clusterRoleBindings {
|
||||||
|
crb.writeTo(digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toAccessSet produces a new AccessSet from the rules in the inner roles references
|
||||||
|
func (b subjectGrants) toAccessSet() *AccessSet {
|
||||||
|
result := new(AccessSet)
|
||||||
|
|
||||||
|
for _, binding := range b.roleBindings {
|
||||||
|
addAccess(result, binding.namespace, binding.rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, binding := range b.clusterRoleBindings {
|
||||||
|
addAccess(result, All, binding.rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTo appends a single role information to a given hash
|
||||||
|
func (r roleRef) writeTo(digest hash.Hash) {
|
||||||
|
digest.Write([]byte(r.roleName))
|
||||||
|
if r.namespace != "" {
|
||||||
|
digest.Write([]byte(r.namespace))
|
||||||
|
}
|
||||||
|
digest.Write([]byte(r.resourceVersion))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user