mirror of https://github.com/k3s-io/k3s
Allow non-RBAC authorizers to participate in role/clusterrole escalation checks
parent
f54593b740
commit
1034efd439
|
@ -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
|
||||
|
|
|
@ -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: "",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue