mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	Allow non-RBAC authorizers to participate in role/clusterrole escalation checks
This commit is contained in:
		@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										195
									
								
								pkg/registry/rbac/clusterrole/policybased/storage_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								pkg/registry/rbac/clusterrole/policybased/storage_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										197
									
								
								pkg/registry/rbac/role/policybased/storage_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								pkg/registry/rbac/role/policybased/storage_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user