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
|
||||||
pkg/registry/policy/poddisruptionbudget/storage
|
pkg/registry/policy/poddisruptionbudget/storage
|
||||||
pkg/registry/policy/rest
|
pkg/registry/policy/rest
|
||||||
pkg/registry/rbac
|
|
||||||
pkg/registry/rbac/clusterrole
|
pkg/registry/rbac/clusterrole
|
||||||
pkg/registry/rbac/clusterrole/policybased
|
pkg/registry/rbac/clusterrole/policybased
|
||||||
pkg/registry/rbac/clusterrolebinding
|
pkg/registry/rbac/clusterrolebinding
|
||||||
|
|
|
@ -54,7 +54,7 @@ var (
|
||||||
kubectl create role foo --verb=get,list,watch --resource=pods,pods/status`))
|
kubectl create role foo --verb=get,list,watch --resource=pods,pods/status`))
|
||||||
|
|
||||||
// Valid resource verb list for validation.
|
// 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
|
// Specialized verbs and GroupResources
|
||||||
specialVerbs = map[string][]schema.GroupResource{
|
specialVerbs = map[string][]schema.GroupResource{
|
||||||
|
@ -74,6 +74,16 @@ var (
|
||||||
Resource: "clusterroles",
|
Resource: "clusterroles",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"escalate": {
|
||||||
|
{
|
||||||
|
Group: "rbac.authorization.k8s.io",
|
||||||
|
Resource: "roles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Group: "rbac.authorization.k8s.io",
|
||||||
|
Resource: "clusterroles",
|
||||||
|
},
|
||||||
|
},
|
||||||
"impersonate": {
|
"impersonate": {
|
||||||
{
|
{
|
||||||
Group: "",
|
Group: "",
|
||||||
|
|
|
@ -18,6 +18,7 @@ go_library(
|
||||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_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/conversion:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime: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/apimachinery/pkg/util/runtime:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/authentication/user: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/authorization/authorizer:go_default_library",
|
||||||
|
|
|
@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"])
|
||||||
load(
|
load(
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
"go_library",
|
"go_library",
|
||||||
|
"go_test",
|
||||||
)
|
)
|
||||||
|
|
||||||
go_library(
|
go_library(
|
||||||
|
@ -16,6 +17,7 @@ go_library(
|
||||||
"//pkg/registry/rbac/validation:go_default_library",
|
"//pkg/registry/rbac/validation:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/api/errors: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/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",
|
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -32,3 +34,22 @@ filegroup(
|
||||||
srcs = [":package-srcs"],
|
srcs = [":package-srcs"],
|
||||||
tags = ["automanaged"],
|
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"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
kapihelper "k8s.io/kubernetes/pkg/apis/core/helper"
|
kapihelper "k8s.io/kubernetes/pkg/apis/core/helper"
|
||||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||||
|
@ -35,11 +36,13 @@ var groupResource = rbac.Resource("clusterroles")
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
rest.StandardStorage
|
rest.StandardStorage
|
||||||
|
|
||||||
|
authorizer authorizer.Authorizer
|
||||||
|
|
||||||
ruleResolver rbacregistryvalidation.AuthorizationRuleResolver
|
ruleResolver rbacregistryvalidation.AuthorizationRuleResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStorage(s rest.StandardStorage, ruleResolver rbacregistryvalidation.AuthorizationRuleResolver) *Storage {
|
func NewStorage(s rest.StandardStorage, authorizer authorizer.Authorizer, ruleResolver rbacregistryvalidation.AuthorizationRuleResolver) *Storage {
|
||||||
return &Storage{s, ruleResolver}
|
return &Storage{s, authorizer, ruleResolver}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Storage) NamespaceScoped() bool {
|
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) {
|
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)
|
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) {
|
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)
|
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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
@ -27,6 +28,7 @@ import (
|
||||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// EscalationAllowed checks if the user associated with the context is a superuser
|
||||||
func EscalationAllowed(ctx context.Context) bool {
|
func EscalationAllowed(ctx context.Context) bool {
|
||||||
u, ok := genericapirequest.UserFrom(ctx)
|
u, ok := genericapirequest.UserFrom(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -44,6 +46,56 @@ func EscalationAllowed(ctx context.Context) bool {
|
||||||
return false
|
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
|
// 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 {
|
func BindingAuthorized(ctx context.Context, roleRef rbac.RoleRef, bindingNamespace string, a authorizer.Authorizer) bool {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
|
|
|
@ -98,13 +98,13 @@ func (p RESTStorageProvider) storage(version schema.GroupVersion, apiResourceCon
|
||||||
)
|
)
|
||||||
|
|
||||||
// roles
|
// roles
|
||||||
storage["roles"] = rolepolicybased.NewStorage(rolesStorage, authorizationRuleResolver)
|
storage["roles"] = rolepolicybased.NewStorage(rolesStorage, p.Authorizer, authorizationRuleResolver)
|
||||||
|
|
||||||
// rolebindings
|
// rolebindings
|
||||||
storage["rolebindings"] = rolebindingpolicybased.NewStorage(roleBindingsStorage, p.Authorizer, authorizationRuleResolver)
|
storage["rolebindings"] = rolebindingpolicybased.NewStorage(roleBindingsStorage, p.Authorizer, authorizationRuleResolver)
|
||||||
|
|
||||||
// clusterroles
|
// clusterroles
|
||||||
storage["clusterroles"] = clusterrolepolicybased.NewStorage(clusterRolesStorage, authorizationRuleResolver)
|
storage["clusterroles"] = clusterrolepolicybased.NewStorage(clusterRolesStorage, p.Authorizer, authorizationRuleResolver)
|
||||||
|
|
||||||
// clusterrolebindings
|
// clusterrolebindings
|
||||||
storage["clusterrolebindings"] = clusterrolebindingpolicybased.NewStorage(clusterRoleBindingsStorage, p.Authorizer, authorizationRuleResolver)
|
storage["clusterrolebindings"] = clusterrolebindingpolicybased.NewStorage(clusterRoleBindingsStorage, p.Authorizer, authorizationRuleResolver)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"])
|
||||||
load(
|
load(
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
"go_library",
|
"go_library",
|
||||||
|
"go_test",
|
||||||
)
|
)
|
||||||
|
|
||||||
go_library(
|
go_library(
|
||||||
|
@ -16,6 +17,7 @@ go_library(
|
||||||
"//pkg/registry/rbac/validation:go_default_library",
|
"//pkg/registry/rbac/validation:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/api/errors: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/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",
|
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -32,3 +34,22 @@ filegroup(
|
||||||
srcs = [":package-srcs"],
|
srcs = [":package-srcs"],
|
||||||
tags = ["automanaged"],
|
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/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
kapihelper "k8s.io/kubernetes/pkg/apis/core/helper"
|
kapihelper "k8s.io/kubernetes/pkg/apis/core/helper"
|
||||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||||
|
@ -34,11 +35,13 @@ var groupResource = rbac.Resource("roles")
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
rest.StandardStorage
|
rest.StandardStorage
|
||||||
|
|
||||||
|
authorizer authorizer.Authorizer
|
||||||
|
|
||||||
ruleResolver rbacregistryvalidation.AuthorizationRuleResolver
|
ruleResolver rbacregistryvalidation.AuthorizationRuleResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStorage(s rest.StandardStorage, ruleResolver rbacregistryvalidation.AuthorizationRuleResolver) *Storage {
|
func NewStorage(s rest.StandardStorage, authorizer authorizer.Authorizer, ruleResolver rbacregistryvalidation.AuthorizationRuleResolver) *Storage {
|
||||||
return &Storage{s, ruleResolver}
|
return &Storage{s, authorizer, ruleResolver}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Storage) NamespaceScoped() bool {
|
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) {
|
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)
|
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) {
|
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)
|
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