1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-13 19:28:55 +00:00

fix: role matching from authentication infos (#6358)

* fix: role matching from authentication infos

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* more tests

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Charles-Edouard Brétéché 2023-02-21 17:57:17 +01:00 committed by GitHub
parent d920f60798
commit ef7265ca6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 417 additions and 274 deletions

View file

@ -2,10 +2,8 @@ package userinfo
import (
"fmt"
"strings"
"github.com/kyverno/kyverno/pkg/config"
"github.com/kyverno/kyverno/pkg/logging"
datautils "github.com/kyverno/kyverno/pkg/utils/data"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
@ -17,8 +15,6 @@ import (
const (
clusterroleKind = "ClusterRole"
roleKind = "Role"
// saPrefix represents service account prefix in admission requests
saPrefix = "system:serviceaccount:"
)
// GetRoleRef gets the list of roles and cluster roles for the incoming api-request
@ -44,62 +40,53 @@ func GetRoleRef(rbLister rbacv1listers.RoleBindingLister, crbLister rbacv1lister
func getRoleRefByRoleBindings(roleBindings []*rbacv1.RoleBinding, userInfo authenticationv1.UserInfo) (roles []string, clusterRoles []string) {
for _, rolebinding := range roleBindings {
for _, subject := range rolebinding.Subjects {
if matchSubjectsMap(subject, userInfo, rolebinding.Namespace) {
switch rolebinding.RoleRef.Kind {
case roleKind:
roles = append(roles, rolebinding.Namespace+":"+rolebinding.RoleRef.Name)
case clusterroleKind:
clusterRoles = append(clusterRoles, rolebinding.RoleRef.Name)
}
if matchBindingSubjects(rolebinding.Subjects, userInfo, rolebinding.Namespace) {
switch rolebinding.RoleRef.Kind {
case roleKind:
roles = append(roles, rolebinding.Namespace+":"+rolebinding.RoleRef.Name)
case clusterroleKind:
clusterRoles = append(clusterRoles, rolebinding.RoleRef.Name)
}
}
}
return roles, clusterRoles
}
// RoleRef in ClusterRoleBindings can only reference a ClusterRole in the global namespace
func getRoleRefByClusterRoleBindings(clusterroleBindings []*rbacv1.ClusterRoleBinding, userInfo authenticationv1.UserInfo) (clusterRoles []string) {
for _, clusterRoleBinding := range clusterroleBindings {
for _, subject := range clusterRoleBinding.Subjects {
if matchSubjectsMap(subject, userInfo, subject.Namespace) {
if clusterRoleBinding.RoleRef.Kind == clusterroleKind {
clusterRoles = append(clusterRoles, clusterRoleBinding.RoleRef.Name)
}
if matchBindingSubjects(clusterRoleBinding.Subjects, userInfo, "") {
if clusterRoleBinding.RoleRef.Kind == clusterroleKind {
clusterRoles = append(clusterRoles, clusterRoleBinding.RoleRef.Name)
}
}
}
return clusterRoles
}
// matchSubjectsMap checks if userInfo found in subject
// return true directly if found a match
// subject.kind can only be ServiceAccount, User and Group
func matchSubjectsMap(subject rbacv1.Subject, userInfo authenticationv1.UserInfo, namespace string) bool {
if strings.Contains(userInfo.Username, saPrefix) {
return matchServiceAccount(subject, userInfo, namespace)
}
return matchUserOrGroup(subject, userInfo)
}
// matchServiceAccount checks if userInfo sa matche the subject sa
// serviceaccount represents as saPrefix:namespace:name in userInfo
func matchServiceAccount(subject rbacv1.Subject, userInfo authenticationv1.UserInfo, namespace string) bool {
subjectServiceAccount := namespace + ":" + subject.Name
if userInfo.Username[len(saPrefix):] != subjectServiceAccount {
return false
}
logging.V(3).Info(fmt.Sprintf("found a matched service account not match: %s", subjectServiceAccount))
return true
}
// matchUserOrGroup checks if userInfo contains user or group info in a subject
func matchUserOrGroup(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
keys := append(userInfo.Groups, userInfo.Username)
for _, key := range keys {
if subject.Name == key {
logging.V(3).Info(fmt.Sprintf("found a matched user/group '%v' in request userInfo: %v", subject.Name, keys))
return true
func matchBindingSubjects(subjects []rbacv1.Subject, userInfo authenticationv1.UserInfo, namespace string) bool {
for _, subject := range subjects {
switch subject.Kind {
case rbacv1.ServiceAccountKind:
ns := subject.Namespace
if ns == "" {
ns = namespace
}
if ns != "" {
username := "system:serviceaccount:" + ns + ":" + subject.Name
if userInfo.Username == username {
return true
}
}
case rbacv1.GroupKind:
for _, group := range userInfo.Groups {
if group == subject.Name {
return true
}
}
case rbacv1.UserKind:
if userInfo.Username == subject.Name {
return true
}
}
}
return false

View file

@ -1,253 +1,409 @@
package userinfo
import (
"reflect"
"testing"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func Test_matchServiceAccount_subject_variants(t *testing.T) {
userInfo := authenticationv1.UserInfo{
Username: "system:serviceaccount:default:saconfig",
}
tests := []struct {
subject rbacv1.Subject
expected bool
}{
{
subject: rbacv1.Subject{},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "serviceaccount",
},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "testnamespace",
},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "1",
},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "testnamespace",
Name: "",
},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "testnamespace",
Name: "testname",
},
expected: false,
},
}
for _, test := range tests {
res := matchServiceAccount(test.subject, userInfo, test.subject.Namespace)
assert.Equal(t, test.expected, res)
}
}
func Test_matchUserOrGroup(t *testing.T) {
group := authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
}
sa := authenticationv1.UserInfo{
Username: "system:serviceaccount:kube-system:deployment-controller",
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:kube-system", "system:authenticated"},
}
user := authenticationv1.UserInfo{
Username: "system:kube-scheduler",
Groups: []string{"system:authenticated"},
}
userContext := rbacv1.Subject{
Kind: "User",
Name: "system:kube-scheduler",
}
groupContext := rbacv1.Subject{
Kind: "Group",
Name: "system:masters",
}
fakegroupContext := rbacv1.Subject{
Kind: "Group",
Name: "fakeGroup",
}
res := matchUserOrGroup(userContext, user)
assert.Assert(t, res)
res = matchUserOrGroup(groupContext, group)
assert.Assert(t, res)
res = matchUserOrGroup(groupContext, sa)
assert.Assert(t, !res)
res = matchUserOrGroup(fakegroupContext, group)
assert.Assert(t, !res)
}
func Test_matchSubjectsMap(t *testing.T) {
sa := authenticationv1.UserInfo{
Username: "system:serviceaccount:default:saconfig",
}
group := authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
}
sasubject := rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "default",
Name: "saconfig",
}
groupsubject := rbacv1.Subject{
Kind: "Group",
Name: "fakeGroup",
}
res := matchSubjectsMap(sasubject, sa, sasubject.Namespace)
assert.Assert(t, res)
res = matchSubjectsMap(groupsubject, group, "")
assert.Assert(t, !res)
}
func newRoleBinding(name, ns string, subjects []rbacv1.Subject, roles rbacv1.RoleRef) *rbacv1.RoleBinding {
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns},
Subjects: subjects,
RoleRef: roles,
}
rb.Kind = "RoleBinding"
rb.APIVersion = "rbac.authorization.k8s.io/v1"
return rb
}
func Test_getRoleRefByRoleBindings(t *testing.T) {
// flag.Parse()
// flag.Set("logtostderr", "true")
// flag.Set("v", "3")
list := make([]*rbacv1.RoleBinding, 2)
list[0] = newRoleBinding("test1", "default",
[]rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: "saconfig",
Namespace: "default",
},
}, rbacv1.RoleRef{
Kind: roleKind,
Name: "myrole",
roleInSameNsImplicit := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "same-ns",
Namespace: "ns-1",
},
)
list[1] = newRoleBinding("test2", "default",
[]rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: "saconfig",
Namespace: "default",
},
}, rbacv1.RoleRef{
Kind: clusterroleKind,
Name: "myclusterrole",
Subjects: []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: "sa",
}},
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: "role-1",
},
)
sa := authenticationv1.UserInfo{
Username: "system:serviceaccount:default:saconfig",
}
expectedrole := []string{list[0].Namespace + ":" + "myrole"}
expectedClusterRole := []string{"myclusterrole"}
roles, clusterroles := getRoleRefByRoleBindings(list, sa)
assert.DeepEqual(t, roles, expectedrole)
assert.DeepEqual(t, clusterroles, expectedClusterRole)
roleInSameNsExplicit := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "same-ns",
Namespace: "ns-1",
},
Subjects: []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: "sa",
Namespace: "ns-1",
}},
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: "role-1",
},
}
roleInAnotherNs := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "different-ns",
Namespace: "ns-2",
},
Subjects: []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: "sa",
Namespace: "ns-1",
}},
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: "role-1",
},
}
clusterRole := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "different-ns",
Namespace: "ns-2",
},
Subjects: []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: "sa",
Namespace: "ns-1",
}},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
Name: "role-1",
},
}
userInfo := authenticationv1.UserInfo{
Username: "system:serviceaccount:ns-1:sa",
}
type args struct {
roleBindings []*rbacv1.RoleBinding
userInfo authenticationv1.UserInfo
}
tests := []struct {
name string
args args
wantRoles []string
wantClusterRoles []string
}{{
name: "service account and role binding explicitely in the same namespace",
args: args{
roleBindings: []*rbacv1.RoleBinding{
roleInSameNsExplicit,
},
userInfo: userInfo,
},
wantRoles: []string{
"ns-1:role-1",
},
}, {
name: "service account and role binding implicitely in the same namespace",
args: args{
roleBindings: []*rbacv1.RoleBinding{
roleInSameNsImplicit,
},
userInfo: userInfo,
},
wantRoles: []string{
"ns-1:role-1",
},
}, {
name: "service account and role binding in the different namespaces",
args: args{
roleBindings: []*rbacv1.RoleBinding{
roleInAnotherNs,
},
userInfo: userInfo,
},
wantRoles: []string{
"ns-2:role-1",
},
}, {
name: "cluster role",
args: args{
roleBindings: []*rbacv1.RoleBinding{
clusterRole,
},
userInfo: userInfo,
},
wantClusterRoles: []string{
"role-1",
},
}, {
name: "all together",
args: args{
roleBindings: []*rbacv1.RoleBinding{
roleInSameNsExplicit,
roleInSameNsImplicit,
roleInAnotherNs,
clusterRole,
},
userInfo: userInfo,
},
wantRoles: []string{
"ns-1:role-1",
"ns-1:role-1",
"ns-2:role-1",
},
wantClusterRoles: []string{
"role-1",
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotRoles, gotClusterRoles := getRoleRefByRoleBindings(tt.args.roleBindings, tt.args.userInfo)
if !reflect.DeepEqual(gotRoles, tt.wantRoles) {
t.Errorf("getRoleRefByRoleBindings() gotRoles = %v, want %v", gotRoles, tt.wantRoles)
}
if !reflect.DeepEqual(gotClusterRoles, tt.wantClusterRoles) {
t.Errorf("getRoleRefByRoleBindings() gotClusterRoles = %v, want %v", gotClusterRoles, tt.wantClusterRoles)
}
})
}
}
func newClusterRoleBinding(name, ns string, subjects []rbacv1.Subject, roles rbacv1.RoleRef) *rbacv1.ClusterRoleBinding {
rb := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns},
Subjects: subjects,
RoleRef: roles,
func Test_matchBindingSubjects(t *testing.T) {
type args struct {
subjects []rbacv1.Subject
userInfo authenticationv1.UserInfo
namespace string
}
tests := []struct {
name string
args args
want bool
}{{
name: "empty subjects",
args: args{
subjects: []rbacv1.Subject{},
userInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount:foo:test",
},
namespace: "",
},
want: false,
}, {
name: "nil subjects",
args: args{
subjects: nil,
userInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount:foo:test",
},
namespace: "",
},
want: false,
}, {
name: "match service account",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.ServiceAccountKind,
Name: "test",
Namespace: "foo",
}},
userInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount:foo:test",
},
namespace: "",
},
want: true,
}, {
name: "match service account with fallback namespace",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.ServiceAccountKind,
Name: "test",
}},
userInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount:foo:test",
},
namespace: "foo",
},
want: true,
}, {
name: "don't match service account",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.ServiceAccountKind,
Name: "test",
Namespace: "foo",
}},
userInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount:bar:test",
},
namespace: "",
},
want: false,
}, {
name: "don't match service account with fallback namespace",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.ServiceAccountKind,
Name: "test",
}},
userInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount:bar:test",
},
namespace: "foo",
},
want: false,
}, {
name: "don't match service account with no namespace and no fallback namespace",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.ServiceAccountKind,
Name: "test",
}},
userInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount::test",
},
namespace: "",
},
want: false,
}, {
name: "match user",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.UserKind,
Name: "someone@company.org",
}},
userInfo: authenticationv1.UserInfo{
Username: "someone@company.org",
},
namespace: "",
},
want: true,
}, {
name: "don't match user",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.UserKind,
Name: "someone@company.org",
}},
userInfo: authenticationv1.UserInfo{
Username: "someone-else@company.org",
},
namespace: "",
},
want: false,
}, {
name: "match group",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.GroupKind,
Name: "admin",
}},
userInfo: authenticationv1.UserInfo{
Username: "someone@company.org",
Groups: []string{
"user",
"dev",
"admin",
},
},
namespace: "",
},
want: true,
}, {
name: "don't match group",
args: args{
subjects: []rbacv1.Subject{{
Kind: rbacv1.GroupKind,
Name: "marketing",
}},
userInfo: authenticationv1.UserInfo{
Username: "someone@company.org",
Groups: []string{
"user",
"dev",
"admin",
},
},
namespace: "",
},
want: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := matchBindingSubjects(tt.args.subjects, tt.args.userInfo, tt.args.namespace); got != tt.want {
t.Errorf("matchBindingSubjects() = %v, want %v", got, tt.want)
}
})
}
rb.Kind = "ClusterRoleBinding"
rb.APIVersion = "rbac.authorization.k8s.io/v1"
return rb
}
func Test_getRoleRefByClusterRoleBindings(t *testing.T) {
list := make([]*rbacv1.ClusterRoleBinding, 2)
list[0] = newClusterRoleBinding("test1", "mynamespace",
[]rbacv1.Subject{
{
Kind: "User",
Name: "kube-scheduler",
},
}, rbacv1.RoleRef{
Kind: clusterroleKind,
Name: "fakeclusterrole",
clusterRole1 := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-role",
},
)
list[1] = newClusterRoleBinding("test2", "mynamespace",
[]rbacv1.Subject{
{
Kind: "Group",
Name: "system:masters",
},
}, rbacv1.RoleRef{
Kind: clusterroleKind,
Name: "myclusterrole",
Subjects: []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: "sa",
Namespace: "foo",
}},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
Name: "role-1",
},
)
group := authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
}
user := authenticationv1.UserInfo{
Username: "system:kube-scheduler",
Groups: []string{"system:authenticated"},
clusterRole2 := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-role",
},
Subjects: []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: "sa",
Namespace: "bar",
}},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
Name: "role-2",
},
}
userInfo := authenticationv1.UserInfo{
Username: "system:serviceaccount:foo:sa",
}
type args struct {
clusterroleBindings []*rbacv1.ClusterRoleBinding
userInfo authenticationv1.UserInfo
}
tests := []struct {
name string
args args
wantClusterRoles []string
}{{
name: "match service account",
args: args{
clusterroleBindings: []*rbacv1.ClusterRoleBinding{
clusterRole1,
},
userInfo: userInfo,
},
wantClusterRoles: []string{
"role-1",
},
}, {
name: "sa in another namespace",
args: args{
clusterroleBindings: []*rbacv1.ClusterRoleBinding{
clusterRole2,
},
userInfo: userInfo,
},
}, {
name: "match service account",
args: args{
clusterroleBindings: []*rbacv1.ClusterRoleBinding{
clusterRole1,
clusterRole2,
},
userInfo: userInfo,
},
wantClusterRoles: []string{
"role-1",
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotClusterRoles := getRoleRefByClusterRoleBindings(tt.args.clusterroleBindings, tt.args.userInfo); !reflect.DeepEqual(gotClusterRoles, tt.wantClusterRoles) {
t.Errorf("getRoleRefByClusterRoleBindings() = %v, want %v", gotClusterRoles, tt.wantClusterRoles)
}
})
}
clusterroles := getRoleRefByClusterRoleBindings(list, group)
assert.DeepEqual(t, clusterroles, []string{"myclusterrole"})
clusterroles = getRoleRefByClusterRoleBindings(list, user)
assert.Equal(t, len(clusterroles), 0)
}