diff --git a/hack/.golint_failures b/hack/.golint_failures index 0a5ba6fea1..66fb728748 100644 --- a/hack/.golint_failures +++ b/hack/.golint_failures @@ -311,7 +311,6 @@ pkg/registry/networking/rest pkg/registry/policy/poddisruptionbudget pkg/registry/policy/poddisruptionbudget/storage pkg/registry/policy/rest -pkg/registry/rbac pkg/registry/rbac/clusterrole pkg/registry/rbac/clusterrole/policybased pkg/registry/rbac/clusterrolebinding diff --git a/pkg/kubectl/cmd/create/create_role.go b/pkg/kubectl/cmd/create/create_role.go index d01014b45f..c9ecd1cb23 100644 --- a/pkg/kubectl/cmd/create/create_role.go +++ b/pkg/kubectl/cmd/create/create_role.go @@ -54,7 +54,7 @@ var ( kubectl create role foo --verb=get,list,watch --resource=pods,pods/status`)) // Valid resource verb list for validation. - validResourceVerbs = []string{"*", "get", "delete", "list", "create", "update", "patch", "watch", "proxy", "deletecollection", "use", "bind", "impersonate"} + validResourceVerbs = []string{"*", "get", "delete", "list", "create", "update", "patch", "watch", "proxy", "deletecollection", "use", "bind", "escalate", "impersonate"} // Specialized verbs and GroupResources specialVerbs = map[string][]schema.GroupResource{ @@ -74,6 +74,16 @@ var ( Resource: "clusterroles", }, }, + "escalate": { + { + Group: "rbac.authorization.k8s.io", + Resource: "roles", + }, + { + Group: "rbac.authorization.k8s.io", + Resource: "clusterroles", + }, + }, "impersonate": { { Group: "", diff --git a/pkg/registry/rbac/BUILD b/pkg/registry/rbac/BUILD index 0c3d4ebd1b..2a35f5d764 100644 --- a/pkg/registry/rbac/BUILD +++ b/pkg/registry/rbac/BUILD @@ -18,6 +18,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", diff --git a/pkg/registry/rbac/clusterrole/policybased/BUILD b/pkg/registry/rbac/clusterrole/policybased/BUILD index 77afcb6cff..ecb781eea1 100644 --- a/pkg/registry/rbac/clusterrole/policybased/BUILD +++ b/pkg/registry/rbac/clusterrole/policybased/BUILD @@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( @@ -16,6 +17,7 @@ go_library( "//pkg/registry/rbac/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", ], ) @@ -32,3 +34,22 @@ filegroup( srcs = [":package-srcs"], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["storage_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/rbac:go_default_library", + "//pkg/apis/rbac/install:go_default_library", + "//pkg/registry/rbac/validation:go_default_library", + "//vendor/k8s.io/api/rbac/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", + "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", + ], +) diff --git a/pkg/registry/rbac/clusterrole/policybased/storage.go b/pkg/registry/rbac/clusterrole/policybased/storage.go index 767673d252..3f0c37b77e 100644 --- a/pkg/registry/rbac/clusterrole/policybased/storage.go +++ b/pkg/registry/rbac/clusterrole/policybased/storage.go @@ -23,6 +23,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/rest" kapihelper "k8s.io/kubernetes/pkg/apis/core/helper" "k8s.io/kubernetes/pkg/apis/rbac" @@ -35,11 +36,13 @@ var groupResource = rbac.Resource("clusterroles") type Storage struct { rest.StandardStorage + authorizer authorizer.Authorizer + ruleResolver rbacregistryvalidation.AuthorizationRuleResolver } -func NewStorage(s rest.StandardStorage, ruleResolver rbacregistryvalidation.AuthorizationRuleResolver) *Storage { - return &Storage{s, ruleResolver} +func NewStorage(s rest.StandardStorage, authorizer authorizer.Authorizer, ruleResolver rbacregistryvalidation.AuthorizationRuleResolver) *Storage { + return &Storage{s, authorizer, ruleResolver} } func (r *Storage) NamespaceScoped() bool { @@ -52,7 +55,7 @@ var fullAuthority = []rbac.PolicyRule{ } func (s *Storage) Create(ctx context.Context, obj runtime.Object, createValidatingAdmission rest.ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) { - if rbacregistry.EscalationAllowed(ctx) { + if rbacregistry.EscalationAllowed(ctx) || rbacregistry.RoleEscalationAuthorized(ctx, s.authorizer) { return s.StandardStorage.Create(ctx, obj, createValidatingAdmission, includeUninitialized) } @@ -72,7 +75,7 @@ func (s *Storage) Create(ctx context.Context, obj runtime.Object, createValidati } func (s *Storage) Update(ctx context.Context, name string, obj rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) { - if rbacregistry.EscalationAllowed(ctx) { + if rbacregistry.EscalationAllowed(ctx) || rbacregistry.RoleEscalationAuthorized(ctx, s.authorizer) { return s.StandardStorage.Update(ctx, name, obj, createValidation, updateValidation) } diff --git a/pkg/registry/rbac/clusterrole/policybased/storage_test.go b/pkg/registry/rbac/clusterrole/policybased/storage_test.go new file mode 100644 index 0000000000..e1eecdc258 --- /dev/null +++ b/pkg/registry/rbac/clusterrole/policybased/storage_test.go @@ -0,0 +1,195 @@ +/* +Copyright 2018 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 policybased + +import ( + "context" + "testing" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/rbac" + _ "k8s.io/kubernetes/pkg/apis/rbac/install" + "k8s.io/kubernetes/pkg/registry/rbac/validation" +) + +func TestEscalation(t *testing.T) { + createContext := request.WithRequestInfo(request.WithNamespace(context.TODO(), ""), &request.RequestInfo{ + IsResourceRequest: true, + Verb: "create", + APIGroup: "rbac.authorization.k8s.io", + APIVersion: "v1", + Resource: "clusterroles", + Name: "", + }) + updateContext := request.WithRequestInfo(request.WithNamespace(context.TODO(), ""), &request.RequestInfo{ + IsResourceRequest: true, + Verb: "update", + APIGroup: "rbac.authorization.k8s.io", + APIVersion: "v1", + Resource: "clusterroles", + Name: "myrole", + }) + + superuser := &user.DefaultInfo{Name: "superuser", Groups: []string{"system:masters"}} + bob := &user.DefaultInfo{Name: "bob"} + steve := &user.DefaultInfo{Name: "steve"} + alice := &user.DefaultInfo{Name: "alice"} + + authzCalled := 0 + fakeStorage := &fakeStorage{} + fakeAuthorizer := authorizer.AuthorizerFunc(func(attr authorizer.Attributes) (authorizer.Decision, string, error) { + authzCalled++ + if attr.GetUser().GetName() == "steve" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionNoOpinion, "", nil + }) + fakeRuleResolver, _ := validation.NewTestRuleResolver( + nil, + nil, + []*rbacv1.ClusterRole{{ObjectMeta: metav1.ObjectMeta{Name: "alice-role"}, Rules: []rbacv1.PolicyRule{{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}}}}, + []*rbacv1.ClusterRoleBinding{{RoleRef: rbacv1.RoleRef{Name: "alice-role", APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole"}, Subjects: []rbacv1.Subject{{Name: "alice", Kind: "User", APIGroup: "rbac.authorization.k8s.io"}}}}, + ) + + role := &rbac.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Name: "myrole", Namespace: ""}, + Rules: []rbac.PolicyRule{{APIGroups: []string{""}, Verbs: []string{"get"}, Resources: []string{"pods"}}}, + } + + s := NewStorage(fakeStorage, fakeAuthorizer, fakeRuleResolver) + + testcases := []struct { + name string + user user.Info + expectAllowed bool + expectAuthz bool + }{ + // superuser doesn't even trigger an authz check, and is allowed + { + name: "superuser", + user: superuser, + expectAuthz: false, + expectAllowed: true, + }, + // bob triggers an authz check, is disallowed by the authorizer, and has no RBAC permissions, so is not allowed + { + name: "bob", + user: bob, + expectAuthz: true, + expectAllowed: false, + }, + // steve triggers an authz check, is allowed by the authorizer, and has no RBAC permissions, but is still allowed + { + name: "steve", + user: steve, + expectAuthz: true, + expectAllowed: true, + }, + // alice triggers an authz check, is denied by the authorizer, but has RBAC permissions in the fakeRuleResolver, so is allowed + { + name: "alice", + user: alice, + expectAuthz: true, + expectAllowed: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + authzCalled, fakeStorage.created, fakeStorage.updated = 0, 0, 0 + _, err := s.Create(request.WithUser(createContext, tc.user), role, nil, false) + + if tc.expectAllowed { + if err != nil { + t.Error(err) + return + } + if fakeStorage.created != 1 { + t.Errorf("unexpected calls to underlying storage.Create: %d", fakeStorage.created) + return + } + } else { + if !errors.IsForbidden(err) { + t.Errorf("expected forbidden, got %v", err) + return + } + if fakeStorage.created != 0 { + t.Errorf("unexpected calls to underlying storage.Create: %d", fakeStorage.created) + return + } + } + + if tc.expectAuthz != (authzCalled > 0) { + t.Fatalf("expected authz=%v, saw %d calls", tc.expectAuthz, authzCalled) + } + + authzCalled, fakeStorage.created, fakeStorage.updated = 0, 0, 0 + _, _, err = s.Update(request.WithUser(updateContext, tc.user), role.Name, rest.DefaultUpdatedObjectInfo(role), nil, nil) + + if tc.expectAllowed { + if err != nil { + t.Error(err) + return + } + if fakeStorage.updated != 1 { + t.Errorf("unexpected calls to underlying storage.Update: %d", fakeStorage.updated) + return + } + } else { + if !errors.IsForbidden(err) { + t.Errorf("expected forbidden, got %v", err) + return + } + if fakeStorage.updated != 0 { + t.Errorf("unexpected calls to underlying storage.Update: %d", fakeStorage.updated) + return + } + } + + if tc.expectAuthz != (authzCalled > 0) { + t.Fatalf("expected authz=%v, saw %d calls", tc.expectAuthz, authzCalled) + } + }) + } +} + +type fakeStorage struct { + updated int + created int + rest.StandardStorage +} + +func (f *fakeStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) { + f.created++ + return nil, nil +} + +func (f *fakeStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) { + obj, err := objInfo.UpdatedObject(ctx, &rbac.ClusterRole{}) + if err != nil { + return obj, false, err + } + f.updated++ + return nil, false, nil +} diff --git a/pkg/registry/rbac/escalation_check.go b/pkg/registry/rbac/escalation_check.go index 74d6aff7cd..5294ecea04 100644 --- a/pkg/registry/rbac/escalation_check.go +++ b/pkg/registry/rbac/escalation_check.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" @@ -27,6 +28,7 @@ import ( "k8s.io/kubernetes/pkg/apis/rbac" ) +// EscalationAllowed checks if the user associated with the context is a superuser func EscalationAllowed(ctx context.Context) bool { u, ok := genericapirequest.UserFrom(ctx) if !ok { @@ -44,6 +46,56 @@ func EscalationAllowed(ctx context.Context) bool { return false } +var roleResources = map[schema.GroupResource]bool{ + rbac.SchemeGroupVersion.WithResource("clusterroles").GroupResource(): true, + rbac.SchemeGroupVersion.WithResource("roles").GroupResource(): true, +} + +// RoleEscalationAuthorized checks if the user associated with the context is explicitly authorized to escalate the role resource associated with the context +func RoleEscalationAuthorized(ctx context.Context, a authorizer.Authorizer) bool { + if a == nil { + return false + } + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return false + } + + requestInfo, ok := genericapirequest.RequestInfoFrom(ctx) + if !ok { + return false + } + + if !requestInfo.IsResourceRequest { + return false + } + + requestResource := schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource} + if !roleResources[requestResource] { + return false + } + + attrs := authorizer.AttributesRecord{ + User: user, + Verb: "escalate", + APIGroup: requestInfo.APIGroup, + Resource: requestInfo.Resource, + Name: requestInfo.Name, + Namespace: requestInfo.Namespace, + ResourceRequest: true, + } + + decision, _, err := a.Authorize(attrs) + if err != nil { + utilruntime.HandleError(fmt.Errorf( + "error authorizing user %#v to escalate %#v named %q in namespace %q: %v", + user, requestResource, requestInfo.Name, requestInfo.Namespace, err, + )) + } + return decision == authorizer.DecisionAllow +} + // BindingAuthorized returns true if the user associated with the context is explicitly authorized to bind the specified roleRef func BindingAuthorized(ctx context.Context, roleRef rbac.RoleRef, bindingNamespace string, a authorizer.Authorizer) bool { if a == nil { diff --git a/pkg/registry/rbac/rest/storage_rbac.go b/pkg/registry/rbac/rest/storage_rbac.go index 53ace70eb9..6568688e94 100644 --- a/pkg/registry/rbac/rest/storage_rbac.go +++ b/pkg/registry/rbac/rest/storage_rbac.go @@ -98,13 +98,13 @@ func (p RESTStorageProvider) storage(version schema.GroupVersion, apiResourceCon ) // roles - storage["roles"] = rolepolicybased.NewStorage(rolesStorage, authorizationRuleResolver) + storage["roles"] = rolepolicybased.NewStorage(rolesStorage, p.Authorizer, authorizationRuleResolver) // rolebindings storage["rolebindings"] = rolebindingpolicybased.NewStorage(roleBindingsStorage, p.Authorizer, authorizationRuleResolver) // clusterroles - storage["clusterroles"] = clusterrolepolicybased.NewStorage(clusterRolesStorage, authorizationRuleResolver) + storage["clusterroles"] = clusterrolepolicybased.NewStorage(clusterRolesStorage, p.Authorizer, authorizationRuleResolver) // clusterrolebindings storage["clusterrolebindings"] = clusterrolebindingpolicybased.NewStorage(clusterRoleBindingsStorage, p.Authorizer, authorizationRuleResolver) diff --git a/pkg/registry/rbac/role/policybased/BUILD b/pkg/registry/rbac/role/policybased/BUILD index 0c1e560666..79e112558b 100644 --- a/pkg/registry/rbac/role/policybased/BUILD +++ b/pkg/registry/rbac/role/policybased/BUILD @@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( @@ -16,6 +17,7 @@ go_library( "//pkg/registry/rbac/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", ], ) @@ -32,3 +34,22 @@ filegroup( srcs = [":package-srcs"], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["storage_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/rbac:go_default_library", + "//pkg/apis/rbac/install:go_default_library", + "//pkg/registry/rbac/validation:go_default_library", + "//vendor/k8s.io/api/rbac/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", + "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", + ], +) diff --git a/pkg/registry/rbac/role/policybased/storage.go b/pkg/registry/rbac/role/policybased/storage.go index 447cd1178a..13ffac4099 100644 --- a/pkg/registry/rbac/role/policybased/storage.go +++ b/pkg/registry/rbac/role/policybased/storage.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/rest" kapihelper "k8s.io/kubernetes/pkg/apis/core/helper" "k8s.io/kubernetes/pkg/apis/rbac" @@ -34,11 +35,13 @@ var groupResource = rbac.Resource("roles") type Storage struct { rest.StandardStorage + authorizer authorizer.Authorizer + ruleResolver rbacregistryvalidation.AuthorizationRuleResolver } -func NewStorage(s rest.StandardStorage, ruleResolver rbacregistryvalidation.AuthorizationRuleResolver) *Storage { - return &Storage{s, ruleResolver} +func NewStorage(s rest.StandardStorage, authorizer authorizer.Authorizer, ruleResolver rbacregistryvalidation.AuthorizationRuleResolver) *Storage { + return &Storage{s, authorizer, ruleResolver} } func (r *Storage) NamespaceScoped() bool { @@ -46,7 +49,7 @@ func (r *Storage) NamespaceScoped() bool { } func (s *Storage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) { - if rbacregistry.EscalationAllowed(ctx) { + if rbacregistry.EscalationAllowed(ctx) || rbacregistry.RoleEscalationAuthorized(ctx, s.authorizer) { return s.StandardStorage.Create(ctx, obj, createValidation, includeUninitialized) } @@ -59,7 +62,7 @@ func (s *Storage) Create(ctx context.Context, obj runtime.Object, createValidati } func (s *Storage) Update(ctx context.Context, name string, obj rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) { - if rbacregistry.EscalationAllowed(ctx) { + if rbacregistry.EscalationAllowed(ctx) || rbacregistry.RoleEscalationAuthorized(ctx, s.authorizer) { return s.StandardStorage.Update(ctx, name, obj, createValidation, updateValidation) } diff --git a/pkg/registry/rbac/role/policybased/storage_test.go b/pkg/registry/rbac/role/policybased/storage_test.go new file mode 100644 index 0000000000..fae709b16a --- /dev/null +++ b/pkg/registry/rbac/role/policybased/storage_test.go @@ -0,0 +1,197 @@ +/* +Copyright 2018 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 policybased + +import ( + "context" + "testing" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/rbac" + _ "k8s.io/kubernetes/pkg/apis/rbac/install" + "k8s.io/kubernetes/pkg/registry/rbac/validation" +) + +func TestEscalation(t *testing.T) { + createContext := request.WithRequestInfo(request.WithNamespace(context.TODO(), "myns"), &request.RequestInfo{ + IsResourceRequest: true, + Verb: "create", + APIGroup: "rbac.authorization.k8s.io", + APIVersion: "v1", + Namespace: "myns", + Resource: "roles", + Name: "", + }) + updateContext := request.WithRequestInfo(request.WithNamespace(context.TODO(), "myns"), &request.RequestInfo{ + IsResourceRequest: true, + Verb: "update", + APIGroup: "rbac.authorization.k8s.io", + APIVersion: "v1", + Namespace: "myns", + Resource: "roles", + Name: "myrole", + }) + + superuser := &user.DefaultInfo{Name: "superuser", Groups: []string{"system:masters"}} + bob := &user.DefaultInfo{Name: "bob"} + steve := &user.DefaultInfo{Name: "steve"} + alice := &user.DefaultInfo{Name: "alice"} + + authzCalled := 0 + fakeStorage := &fakeStorage{} + fakeAuthorizer := authorizer.AuthorizerFunc(func(attr authorizer.Attributes) (authorizer.Decision, string, error) { + authzCalled++ + if attr.GetUser().GetName() == "steve" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionNoOpinion, "", nil + }) + fakeRuleResolver, _ := validation.NewTestRuleResolver( + nil, + nil, + []*rbacv1.ClusterRole{{ObjectMeta: metav1.ObjectMeta{Name: "alice-role"}, Rules: []rbacv1.PolicyRule{{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}}}}, + []*rbacv1.ClusterRoleBinding{{RoleRef: rbacv1.RoleRef{Name: "alice-role", APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole"}, Subjects: []rbacv1.Subject{{Name: "alice", Kind: "User", APIGroup: "rbac.authorization.k8s.io"}}}}, + ) + + role := &rbac.Role{ + ObjectMeta: metav1.ObjectMeta{Name: "myrole", Namespace: "myns"}, + Rules: []rbac.PolicyRule{{APIGroups: []string{""}, Verbs: []string{"get"}, Resources: []string{"pods"}}}, + } + + s := NewStorage(fakeStorage, fakeAuthorizer, fakeRuleResolver) + + testcases := []struct { + name string + user user.Info + expectAllowed bool + expectAuthz bool + }{ + // superuser doesn't even trigger an authz check, and is allowed + { + name: "superuser", + user: superuser, + expectAuthz: false, + expectAllowed: true, + }, + // bob triggers an authz check, is disallowed by the authorizer, and has no RBAC permissions, so is not allowed + { + name: "bob", + user: bob, + expectAuthz: true, + expectAllowed: false, + }, + // steve triggers an authz check, is allowed by the authorizer, and has no RBAC permissions, but is still allowed + { + name: "steve", + user: steve, + expectAuthz: true, + expectAllowed: true, + }, + // alice triggers an authz check, is denied by the authorizer, but has RBAC permissions in the fakeRuleResolver, so is allowed + { + name: "alice", + user: alice, + expectAuthz: true, + expectAllowed: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + authzCalled, fakeStorage.created, fakeStorage.updated = 0, 0, 0 + _, err := s.Create(request.WithUser(createContext, tc.user), role, nil, false) + + if tc.expectAllowed { + if err != nil { + t.Error(err) + return + } + if fakeStorage.created != 1 { + t.Errorf("unexpected calls to underlying storage.Create: %d", fakeStorage.created) + return + } + } else { + if !errors.IsForbidden(err) { + t.Errorf("expected forbidden, got %v", err) + return + } + if fakeStorage.created != 0 { + t.Errorf("unexpected calls to underlying storage.Create: %d", fakeStorage.created) + return + } + } + + if tc.expectAuthz != (authzCalled > 0) { + t.Fatalf("expected authz=%v, saw %d calls", tc.expectAuthz, authzCalled) + } + + authzCalled, fakeStorage.created, fakeStorage.updated = 0, 0, 0 + _, _, err = s.Update(request.WithUser(updateContext, tc.user), role.Name, rest.DefaultUpdatedObjectInfo(role), nil, nil) + + if tc.expectAllowed { + if err != nil { + t.Error(err) + return + } + if fakeStorage.updated != 1 { + t.Errorf("unexpected calls to underlying storage.Update: %d", fakeStorage.updated) + return + } + } else { + if !errors.IsForbidden(err) { + t.Errorf("expected forbidden, got %v", err) + return + } + if fakeStorage.updated != 0 { + t.Errorf("unexpected calls to underlying storage.Update: %d", fakeStorage.updated) + return + } + } + + if tc.expectAuthz != (authzCalled > 0) { + t.Fatalf("expected authz=%v, saw %d calls", tc.expectAuthz, authzCalled) + } + }) + } +} + +type fakeStorage struct { + updated int + created int + rest.StandardStorage +} + +func (f *fakeStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) { + f.created++ + return nil, nil +} + +func (f *fakeStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) { + obj, err := objInfo.UpdatedObject(ctx, &rbac.Role{}) + if err != nil { + return obj, false, err + } + f.updated++ + return nil, false, nil +}