diff --git a/pkg/apis/rbac/validation/rulevalidation.go b/pkg/apis/rbac/validation/rulevalidation.go index 4ed3a910322..c61e921321c 100644 --- a/pkg/apis/rbac/validation/rulevalidation.go +++ b/pkg/apis/rbac/validation/rulevalidation.go @@ -190,28 +190,29 @@ func appliesToUser(user user.Info, subject rbac.Subject, namespace string) bool } // NewTestRuleResolver returns a rule resolver from lists of role objects. -func NewTestRuleResolver(roles []*rbac.Role, roleBindings []*rbac.RoleBinding, clusterRoles []*rbac.ClusterRole, clusterRoleBindings []*rbac.ClusterRoleBinding) AuthorizationRuleResolver { - r := staticRoles{ +func NewTestRuleResolver(roles []*rbac.Role, roleBindings []*rbac.RoleBinding, clusterRoles []*rbac.ClusterRole, clusterRoleBindings []*rbac.ClusterRoleBinding) (AuthorizationRuleResolver, *StaticRoles) { + r := StaticRoles{ roles: roles, roleBindings: roleBindings, clusterRoles: clusterRoles, clusterRoleBindings: clusterRoleBindings, } - return newMockRuleResolver(&r) + return newMockRuleResolver(&r), &r } -func newMockRuleResolver(r *staticRoles) AuthorizationRuleResolver { +func newMockRuleResolver(r *StaticRoles) AuthorizationRuleResolver { return NewDefaultRuleResolver(r, r, r, r) } -type staticRoles struct { +// StaticRoles is a rule resolver that resolves from lists of role objects. +type StaticRoles struct { roles []*rbac.Role roleBindings []*rbac.RoleBinding clusterRoles []*rbac.ClusterRole clusterRoleBindings []*rbac.ClusterRoleBinding } -func (r *staticRoles) GetRole(namespace, name string) (*rbac.Role, error) { +func (r *StaticRoles) GetRole(namespace, name string) (*rbac.Role, error) { if len(namespace) == 0 { return nil, errors.New("must provide namespace when getting role") } @@ -223,7 +224,7 @@ func (r *staticRoles) GetRole(namespace, name string) (*rbac.Role, error) { return nil, errors.New("role not found") } -func (r *staticRoles) GetClusterRole(name string) (*rbac.ClusterRole, error) { +func (r *StaticRoles) GetClusterRole(name string) (*rbac.ClusterRole, error) { for _, clusterRole := range r.clusterRoles { if clusterRole.Name == name { return clusterRole, nil @@ -232,7 +233,7 @@ func (r *staticRoles) GetClusterRole(name string) (*rbac.ClusterRole, error) { return nil, errors.New("role not found") } -func (r *staticRoles) ListRoleBindings(namespace string) ([]*rbac.RoleBinding, error) { +func (r *StaticRoles) ListRoleBindings(namespace string) ([]*rbac.RoleBinding, error) { if len(namespace) == 0 { return nil, errors.New("must provide namespace when listing role bindings") } @@ -248,6 +249,6 @@ func (r *staticRoles) ListRoleBindings(namespace string) ([]*rbac.RoleBinding, e return roleBindingList, nil } -func (r *staticRoles) ListClusterRoleBindings() ([]*rbac.ClusterRoleBinding, error) { +func (r *StaticRoles) ListClusterRoleBindings() ([]*rbac.ClusterRoleBinding, error) { return r.clusterRoleBindings, nil } diff --git a/pkg/apis/rbac/validation/rulevalidation_test.go b/pkg/apis/rbac/validation/rulevalidation_test.go index 96b9725b8d4..679c70a3a5b 100644 --- a/pkg/apis/rbac/validation/rulevalidation_test.go +++ b/pkg/apis/rbac/validation/rulevalidation_test.go @@ -72,7 +72,7 @@ func TestDefaultRuleResolver(t *testing.T) { Resources: []string{"*"}, } - staticRoles1 := staticRoles{ + staticRoles1 := StaticRoles{ roles: []*rbac.Role{ { ObjectMeta: api.ObjectMeta{Namespace: "namespace1", Name: "readthings"}, @@ -111,7 +111,7 @@ func TestDefaultRuleResolver(t *testing.T) { } tests := []struct { - staticRoles + StaticRoles // For a given context, what are the rules that apply? user user.Info @@ -119,32 +119,32 @@ func TestDefaultRuleResolver(t *testing.T) { effectiveRules []rbac.PolicyRule }{ { - staticRoles: staticRoles1, + StaticRoles: staticRoles1, user: &user.DefaultInfo{Name: "foobar"}, namespace: "namespace1", effectiveRules: []rbac.PolicyRule{ruleReadPods, ruleReadServices}, }, { - staticRoles: staticRoles1, + StaticRoles: staticRoles1, user: &user.DefaultInfo{Name: "foobar"}, namespace: "namespace2", effectiveRules: []rbac.PolicyRule{}, }, { - staticRoles: staticRoles1, + StaticRoles: staticRoles1, // Same as above but without a namespace. Only cluster rules should apply. user: &user.DefaultInfo{Name: "foobar", Groups: []string{"admin"}}, effectiveRules: []rbac.PolicyRule{ruleAdmin}, }, { - staticRoles: staticRoles1, + StaticRoles: staticRoles1, user: &user.DefaultInfo{}, effectiveRules: []rbac.PolicyRule{}, }, } for i, tc := range tests { - ruleResolver := newMockRuleResolver(&tc.staticRoles) + ruleResolver := newMockRuleResolver(&tc.StaticRoles) rules, err := ruleResolver.RulesFor(tc.user, tc.namespace) if err != nil { t.Errorf("case %d: GetEffectivePolicyRules(context)=%v", i, err) diff --git a/plugin/pkg/auth/authorizer/rbac/BUILD b/plugin/pkg/auth/authorizer/rbac/BUILD index 4fc0bf74573..c6dc244f82c 100644 --- a/plugin/pkg/auth/authorizer/rbac/BUILD +++ b/plugin/pkg/auth/authorizer/rbac/BUILD @@ -12,19 +12,26 @@ load( go_library( name = "go_default_library", - srcs = ["rbac.go"], + srcs = [ + "rbac.go", + "subject_locator.go", + ], tags = ["automanaged"], deps = [ "//pkg/apis/rbac:go_default_library", "//pkg/apis/rbac/validation:go_default_library", "//pkg/auth/authorizer:go_default_library", "//pkg/auth/user:go_default_library", + "//pkg/util/errors:go_default_library", ], ) go_test( name = "go_default_test", - srcs = ["rbac_test.go"], + srcs = [ + "rbac_test.go", + "subject_locator_test.go", + ], library = "go_default_library", tags = ["automanaged"], deps = [ diff --git a/plugin/pkg/auth/authorizer/rbac/rbac_test.go b/plugin/pkg/auth/authorizer/rbac/rbac_test.go index 76e179742cd..a0721e562a6 100644 --- a/plugin/pkg/auth/authorizer/rbac/rbac_test.go +++ b/plugin/pkg/auth/authorizer/rbac/rbac_test.go @@ -221,7 +221,7 @@ func TestAuthorizer(t *testing.T) { }, } for i, tt := range tests { - ruleResolver := validation.NewTestRuleResolver(tt.roles, tt.roleBindings, tt.clusterRoles, tt.clusterRoleBindings) + ruleResolver, _ := validation.NewTestRuleResolver(tt.roles, tt.roleBindings, tt.clusterRoles, tt.clusterRoleBindings) a := RBACAuthorizer{tt.superUser, ruleResolver} for _, attr := range tt.shouldPass { if authorized, _, _ := a.Authorize(attr); !authorized { diff --git a/plugin/pkg/auth/authorizer/rbac/subject_locator.go b/plugin/pkg/auth/authorizer/rbac/subject_locator.go new file mode 100644 index 00000000000..a9470a238da --- /dev/null +++ b/plugin/pkg/auth/authorizer/rbac/subject_locator.go @@ -0,0 +1,117 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package rbac implements the authorizer.Authorizer interface using roles base access control. +package rbac + +import ( + "k8s.io/kubernetes/pkg/apis/rbac" + "k8s.io/kubernetes/pkg/apis/rbac/validation" + "k8s.io/kubernetes/pkg/auth/authorizer" + "k8s.io/kubernetes/pkg/auth/user" + utilerrors "k8s.io/kubernetes/pkg/util/errors" +) + +type RoleToRuleMapper interface { + // GetRoleReferenceRules attempts to resolve the role reference of a RoleBinding or ClusterRoleBinding. The passed namespace should be the namespace + // of the role binding, the empty string if a cluster role binding. + GetRoleReferenceRules(roleRef rbac.RoleRef, namespace string) ([]rbac.PolicyRule, error) +} + +type SubjectAccessEvaluator struct { + superUser string + + roleBindingLister validation.RoleBindingLister + clusterRoleBindingLister validation.ClusterRoleBindingLister + roleToRuleMapper RoleToRuleMapper +} + +func NewSubjectAccessEvaluator(roles validation.RoleGetter, roleBindings validation.RoleBindingLister, clusterRoles validation.ClusterRoleGetter, clusterRoleBindings validation.ClusterRoleBindingLister, superUser string) *SubjectAccessEvaluator { + subjectLocator := &SubjectAccessEvaluator{ + superUser: superUser, + roleBindingLister: roleBindings, + clusterRoleBindingLister: clusterRoleBindings, + roleToRuleMapper: validation.NewDefaultRuleResolver( + roles, roleBindings, clusterRoles, clusterRoleBindings, + ), + } + return subjectLocator +} + +// AllowedSubjects returns the subjects that can perform an action and any errors encountered while computing the list. +// It is possible to have both subjects and errors returned if some rolebindings couldn't be resolved, but others could be. +func (r *SubjectAccessEvaluator) AllowedSubjects(requestAttributes authorizer.Attributes) ([]rbac.Subject, error) { + subjects := []rbac.Subject{{Kind: rbac.GroupKind, Name: user.SystemPrivilegedGroup}} + if len(r.superUser) > 0 { + subjects = append(subjects, rbac.Subject{Kind: rbac.UserKind, APIVersion: "v1alpha1", Name: r.superUser}) + } + errorlist := []error{} + + if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil { + errorlist = append(errorlist, err) + + } else { + for _, clusterRoleBinding := range clusterRoleBindings { + rules, err := r.roleToRuleMapper.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "") + if err != nil { + // if we have an error, just keep track of it and keep processing. Since rules are additive, + // missing a reference is bad, but we can continue with other rolebindings and still have a list + // that does not contain any invalid values + errorlist = append(errorlist, err) + } + if RulesAllow(requestAttributes, rules...) { + subjects = append(subjects, clusterRoleBinding.Subjects...) + } + } + } + + if namespace := requestAttributes.GetNamespace(); len(namespace) > 0 { + if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil { + errorlist = append(errorlist, err) + + } else { + for _, roleBinding := range roleBindings { + rules, err := r.roleToRuleMapper.GetRoleReferenceRules(roleBinding.RoleRef, namespace) + if err != nil { + // if we have an error, just keep track of it and keep processing. Since rules are additive, + // missing a reference is bad, but we can continue with other rolebindings and still have a list + // that does not contain any invalid values + errorlist = append(errorlist, err) + } + if RulesAllow(requestAttributes, rules...) { + subjects = append(subjects, roleBinding.Subjects...) + } + } + } + } + + dedupedSubjects := []rbac.Subject{} + for _, subject := range subjects { + found := false + for _, curr := range dedupedSubjects { + if curr == subject { + found = true + break + } + } + + if !found { + dedupedSubjects = append(dedupedSubjects, subject) + } + } + + return subjects, utilerrors.NewAggregate(errorlist) +} diff --git a/plugin/pkg/auth/authorizer/rbac/subject_locator_test.go b/plugin/pkg/auth/authorizer/rbac/subject_locator_test.go new file mode 100644 index 00000000000..f828bd7d268 --- /dev/null +++ b/plugin/pkg/auth/authorizer/rbac/subject_locator_test.go @@ -0,0 +1,151 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rbac + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/apis/rbac" + "k8s.io/kubernetes/pkg/apis/rbac/validation" + "k8s.io/kubernetes/pkg/auth/authorizer" + "k8s.io/kubernetes/pkg/auth/user" +) + +func TestSubjectLocator(t *testing.T) { + type actionToSubjects struct { + action authorizer.Attributes + subjects []rbac.Subject + } + + tests := []struct { + name string + roles []*rbac.Role + roleBindings []*rbac.RoleBinding + clusterRoles []*rbac.ClusterRole + clusterRoleBindings []*rbac.ClusterRoleBinding + + superUser string + + actionsToSubjects []actionToSubjects + }{ + { + name: "no super user, star matches star", + clusterRoles: []*rbac.ClusterRole{ + newClusterRole("admin", newRule("*", "*", "*", "*")), + }, + clusterRoleBindings: []*rbac.ClusterRoleBinding{ + newClusterRoleBinding("admin", "User:super-admin", "Group:super-admins"), + }, + roleBindings: []*rbac.RoleBinding{ + newRoleBinding("ns1", "admin", bindToClusterRole, "User:admin", "Group:admins"), + }, + actionsToSubjects: []actionToSubjects{ + { + &defaultAttributes{"", "", "get", "Pods", "", "ns1", ""}, + []rbac.Subject{ + {Kind: rbac.GroupKind, Name: user.SystemPrivilegedGroup}, + {Kind: rbac.UserKind, Name: "super-admin"}, + {Kind: rbac.GroupKind, Name: "super-admins"}, + {Kind: rbac.UserKind, Name: "admin"}, + {Kind: rbac.GroupKind, Name: "admins"}, + }, + }, + { + // cluster role matches star in namespace + &defaultAttributes{"", "", "*", "Pods", "", "*", ""}, + []rbac.Subject{ + {Kind: rbac.GroupKind, Name: user.SystemPrivilegedGroup}, + {Kind: rbac.UserKind, Name: "super-admin"}, + {Kind: rbac.GroupKind, Name: "super-admins"}, + }, + }, + { + // empty ns + &defaultAttributes{"", "", "*", "Pods", "", "", ""}, + []rbac.Subject{ + {Kind: rbac.GroupKind, Name: user.SystemPrivilegedGroup}, + {Kind: rbac.UserKind, Name: "super-admin"}, + {Kind: rbac.GroupKind, Name: "super-admins"}, + }, + }, + }, + }, + { + name: "super user, local roles work", + superUser: "foo", + clusterRoles: []*rbac.ClusterRole{ + newClusterRole("admin", newRule("*", "*", "*", "*")), + }, + clusterRoleBindings: []*rbac.ClusterRoleBinding{ + newClusterRoleBinding("admin", "User:super-admin", "Group:super-admins"), + }, + roles: []*rbac.Role{ + newRole("admin", "ns1", newRule("get", "*", "Pods", "*")), + }, + roleBindings: []*rbac.RoleBinding{ + newRoleBinding("ns1", "admin", bindToRole, "User:admin", "Group:admins"), + }, + actionsToSubjects: []actionToSubjects{ + { + &defaultAttributes{"", "", "get", "Pods", "", "ns1", ""}, + []rbac.Subject{ + {Kind: rbac.GroupKind, Name: user.SystemPrivilegedGroup}, + {Kind: rbac.UserKind, APIVersion: "v1alpha1", Name: "foo"}, + {Kind: rbac.UserKind, Name: "super-admin"}, + {Kind: rbac.GroupKind, Name: "super-admins"}, + {Kind: rbac.UserKind, Name: "admin"}, + {Kind: rbac.GroupKind, Name: "admins"}, + }, + }, + { + // verb matchies correctly + &defaultAttributes{"", "", "create", "Pods", "", "ns1", ""}, + []rbac.Subject{ + {Kind: rbac.GroupKind, Name: user.SystemPrivilegedGroup}, + {Kind: rbac.UserKind, APIVersion: "v1alpha1", Name: "foo"}, + {Kind: rbac.UserKind, Name: "super-admin"}, + {Kind: rbac.GroupKind, Name: "super-admins"}, + }, + }, + { + // binding only works in correct ns + &defaultAttributes{"", "", "get", "Pods", "", "ns2", ""}, + []rbac.Subject{ + {Kind: rbac.GroupKind, Name: user.SystemPrivilegedGroup}, + {Kind: rbac.UserKind, APIVersion: "v1alpha1", Name: "foo"}, + {Kind: rbac.UserKind, Name: "super-admin"}, + {Kind: rbac.GroupKind, Name: "super-admins"}, + }, + }, + }, + }, + } + for _, tt := range tests { + ruleResolver, lister := validation.NewTestRuleResolver(tt.roles, tt.roleBindings, tt.clusterRoles, tt.clusterRoleBindings) + a := SubjectAccessEvaluator{tt.superUser, lister, lister, ruleResolver} + for i, action := range tt.actionsToSubjects { + actualSubjects, err := a.AllowedSubjects(action.action) + if err != nil { + t.Errorf("case %q %d: error %v", tt.name, i, err) + } + if !reflect.DeepEqual(actualSubjects, action.subjects) { + t.Errorf("case %q %d: expected %v actual %v", tt.name, i, action.subjects, actualSubjects) + } + } + } +}