mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-20 18:31:15 +00:00
Merge pull request #26753 from ericchiang/rbac-authorizer-tests
Automatic merge from submit-queue add unit and integration tests for rbac authorizer This PR adds lots of tests for the RBAC authorizer. The plan over the next couple days is to add a lot more test cases. Updates #23396 cc @erictune
This commit is contained in:
commit
6fbf99b11a
@ -17,11 +17,12 @@ limitations under the License.
|
||||
package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/errors"
|
||||
apierrors "k8s.io/kubernetes/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
utilerrors "k8s.io/kubernetes/pkg/util/errors"
|
||||
@ -66,7 +67,7 @@ func ConfirmNoEscalation(ctx api.Context, ruleResolver AuthorizationRuleResolver
|
||||
ownerRightsCover, missingRights := Covers(ownerRules, rules)
|
||||
if !ownerRightsCover {
|
||||
user, _ := api.UserFrom(ctx)
|
||||
return errors.NewUnauthorized(fmt.Sprintf("attempt to grant extra privileges: %v user=%v ownerrules=%v ruleResolutionErrors=%v", missingRights, user, ownerRules, ruleResolutionErrors))
|
||||
return apierrors.NewUnauthorized(fmt.Sprintf("attempt to grant extra privileges: %v user=%v ownerrules=%v ruleResolutionErrors=%v", missingRights, user, ownerRules, ruleResolutionErrors))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -206,3 +207,79 @@ func appliesToUser(user user.Info, subject rbac.Subject) (bool, error) {
|
||||
return false, fmt.Errorf("unknown subject kind: %s", subject.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestRuleResolver returns a rule resolver from lists of role objects.
|
||||
func NewTestRuleResolver(roles []rbac.Role, roleBindings []rbac.RoleBinding, clusterRoles []rbac.ClusterRole, clusterRoleBindings []rbac.ClusterRoleBinding) AuthorizationRuleResolver {
|
||||
r := staticRoles{
|
||||
roles: roles,
|
||||
roleBindings: roleBindings,
|
||||
clusterRoles: clusterRoles,
|
||||
clusterRoleBindings: clusterRoleBindings,
|
||||
}
|
||||
return newMockRuleResolver(&r)
|
||||
}
|
||||
|
||||
func newMockRuleResolver(r *staticRoles) AuthorizationRuleResolver {
|
||||
return NewDefaultRuleResolver(r, r, r, r)
|
||||
}
|
||||
|
||||
type staticRoles struct {
|
||||
roles []rbac.Role
|
||||
roleBindings []rbac.RoleBinding
|
||||
clusterRoles []rbac.ClusterRole
|
||||
clusterRoleBindings []rbac.ClusterRoleBinding
|
||||
}
|
||||
|
||||
func (r *staticRoles) GetRole(ctx api.Context, id string) (*rbac.Role, error) {
|
||||
namespace, ok := api.NamespaceFrom(ctx)
|
||||
if !ok || namespace == "" {
|
||||
return nil, errors.New("must provide namespace when getting role")
|
||||
}
|
||||
for _, role := range r.roles {
|
||||
if role.Namespace == namespace && role.Name == id {
|
||||
return &role, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
|
||||
func (r *staticRoles) GetClusterRole(ctx api.Context, id string) (*rbac.ClusterRole, error) {
|
||||
namespace, ok := api.NamespaceFrom(ctx)
|
||||
if ok && namespace != "" {
|
||||
return nil, errors.New("cannot provide namespace when getting cluster role")
|
||||
}
|
||||
for _, clusterRole := range r.clusterRoles {
|
||||
if clusterRole.Namespace == namespace && clusterRole.Name == id {
|
||||
return &clusterRole, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
|
||||
func (r *staticRoles) ListRoleBindings(ctx api.Context, options *api.ListOptions) (*rbac.RoleBindingList, error) {
|
||||
namespace, ok := api.NamespaceFrom(ctx)
|
||||
if !ok || namespace == "" {
|
||||
return nil, errors.New("must provide namespace when listing role bindings")
|
||||
}
|
||||
|
||||
roleBindingList := new(rbac.RoleBindingList)
|
||||
for _, roleBinding := range r.roleBindings {
|
||||
if roleBinding.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
// TODO(ericchiang): need to implement label selectors?
|
||||
roleBindingList.Items = append(roleBindingList.Items, roleBinding)
|
||||
}
|
||||
return roleBindingList, nil
|
||||
}
|
||||
|
||||
func (r *staticRoles) ListClusterRoleBindings(ctx api.Context, options *api.ListOptions) (*rbac.ClusterRoleBindingList, error) {
|
||||
namespace, ok := api.NamespaceFrom(ctx)
|
||||
if ok && namespace != "" {
|
||||
return nil, errors.New("cannot list cluster role bindings from within a namespace")
|
||||
}
|
||||
clusterRoleBindings := new(rbac.ClusterRoleBindingList)
|
||||
clusterRoleBindings.Items = make([]rbac.ClusterRoleBinding, len(r.clusterRoleBindings))
|
||||
copy(clusterRoleBindings.Items, r.clusterRoleBindings)
|
||||
return clusterRoleBindings, nil
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||
package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"reflect"
|
||||
@ -30,71 +29,6 @@ import (
|
||||
"k8s.io/kubernetes/pkg/util/diff"
|
||||
)
|
||||
|
||||
func newMockRuleResolver(r *staticRoles) AuthorizationRuleResolver {
|
||||
return NewDefaultRuleResolver(r, r, r, r)
|
||||
}
|
||||
|
||||
type staticRoles struct {
|
||||
roles []rbac.Role
|
||||
roleBindings []rbac.RoleBinding
|
||||
clusterRoles []rbac.ClusterRole
|
||||
clusterRoleBindings []rbac.ClusterRoleBinding
|
||||
}
|
||||
|
||||
func (r *staticRoles) GetRole(ctx api.Context, id string) (*rbac.Role, error) {
|
||||
namespace, ok := api.NamespaceFrom(ctx)
|
||||
if !ok || namespace == "" {
|
||||
return nil, errors.New("must provide namespace when getting role")
|
||||
}
|
||||
for _, role := range r.roles {
|
||||
if role.Namespace == namespace && role.Name == id {
|
||||
return &role, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
|
||||
func (r *staticRoles) GetClusterRole(ctx api.Context, id string) (*rbac.ClusterRole, error) {
|
||||
namespace, ok := api.NamespaceFrom(ctx)
|
||||
if ok && namespace != "" {
|
||||
return nil, errors.New("cannot provide namespace when getting cluster role")
|
||||
}
|
||||
for _, clusterRole := range r.clusterRoles {
|
||||
if clusterRole.Namespace == namespace && clusterRole.Name == id {
|
||||
return &clusterRole, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
|
||||
func (r *staticRoles) ListRoleBindings(ctx api.Context, options *api.ListOptions) (*rbac.RoleBindingList, error) {
|
||||
namespace, ok := api.NamespaceFrom(ctx)
|
||||
if !ok || namespace == "" {
|
||||
return nil, errors.New("must provide namespace when listing role bindings")
|
||||
}
|
||||
|
||||
roleBindingList := new(rbac.RoleBindingList)
|
||||
for _, roleBinding := range r.roleBindings {
|
||||
if roleBinding.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
// TODO(ericchiang): need to implement label selectors?
|
||||
roleBindingList.Items = append(roleBindingList.Items, roleBinding)
|
||||
}
|
||||
return roleBindingList, nil
|
||||
}
|
||||
|
||||
func (r *staticRoles) ListClusterRoleBindings(ctx api.Context, options *api.ListOptions) (*rbac.ClusterRoleBindingList, error) {
|
||||
namespace, ok := api.NamespaceFrom(ctx)
|
||||
if ok && namespace != "" {
|
||||
return nil, errors.New("cannot list cluster role bindings from within a namespace")
|
||||
}
|
||||
clusterRoleBindings := new(rbac.ClusterRoleBindingList)
|
||||
clusterRoleBindings.Items = make([]rbac.ClusterRoleBinding, len(r.clusterRoleBindings))
|
||||
copy(clusterRoleBindings.Items, r.clusterRoleBindings)
|
||||
return clusterRoleBindings, nil
|
||||
}
|
||||
|
||||
// compute a hash of a policy rule so we can sort in a deterministic order
|
||||
func hashOf(p rbac.PolicyRule) string {
|
||||
hash := fnv.New32()
|
||||
|
@ -143,16 +143,13 @@ func NewAuthorizerFromAuthorizationConfig(authorizationModes []string, config Au
|
||||
}
|
||||
authorizers = append(authorizers, webhookAuthorizer)
|
||||
case ModeRBAC:
|
||||
rbacAuthorizer, err := rbac.New(
|
||||
rbacAuthorizer := rbac.New(
|
||||
config.RBACRoleRegistry,
|
||||
config.RBACRoleBindingRegistry,
|
||||
config.RBACClusterRoleRegistry,
|
||||
config.RBACClusterRoleBindingRegistry,
|
||||
config.RBACSuperUser,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorizers = append(authorizers, rbacAuthorizer)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown authorization mode %s specified", authorizationMode)
|
||||
|
@ -65,7 +65,7 @@ func (r *RBACAuthorizer) Authorize(attr authorizer.Attributes) error {
|
||||
return validation.ConfirmNoEscalation(ctx, r.authorizationRuleResolver, []rbac.PolicyRule{requestedRule})
|
||||
}
|
||||
|
||||
func New(roleRegistry role.Registry, roleBindingRegistry rolebinding.Registry, clusterRoleRegistry clusterrole.Registry, clusterRoleBindingRegistry clusterrolebinding.Registry, superUser string) (*RBACAuthorizer, error) {
|
||||
func New(roleRegistry role.Registry, roleBindingRegistry rolebinding.Registry, clusterRoleRegistry clusterrole.Registry, clusterRoleBindingRegistry clusterrolebinding.Registry, superUser string) *RBACAuthorizer {
|
||||
authorizer := &RBACAuthorizer{
|
||||
superUser: superUser,
|
||||
authorizationRuleResolver: validation.NewDefaultRuleResolver(
|
||||
@ -75,5 +75,5 @@ func New(roleRegistry role.Registry, roleBindingRegistry rolebinding.Registry, c
|
||||
clusterRoleBindingRegistry,
|
||||
),
|
||||
}
|
||||
return authorizer, nil
|
||||
return authorizer
|
||||
}
|
||||
|
@ -17,43 +17,129 @@ limitations under the License.
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||
"k8s.io/kubernetes/pkg/registry/clusterrole"
|
||||
clusterroleetcd "k8s.io/kubernetes/pkg/registry/clusterrole/etcd"
|
||||
"k8s.io/kubernetes/pkg/registry/clusterrolebinding"
|
||||
clusterrolebindingetcd "k8s.io/kubernetes/pkg/registry/clusterrolebinding/etcd"
|
||||
"k8s.io/kubernetes/pkg/registry/generic"
|
||||
"k8s.io/kubernetes/pkg/registry/role"
|
||||
roleetcd "k8s.io/kubernetes/pkg/registry/role/etcd"
|
||||
"k8s.io/kubernetes/pkg/registry/rolebinding"
|
||||
rolebindingetcd "k8s.io/kubernetes/pkg/registry/rolebinding/etcd"
|
||||
"k8s.io/kubernetes/pkg/storage/etcd"
|
||||
"k8s.io/kubernetes/pkg/storage/etcd/etcdtest"
|
||||
etcdtesting "k8s.io/kubernetes/pkg/storage/etcd/testing"
|
||||
"k8s.io/kubernetes/pkg/apis/rbac/validation"
|
||||
"k8s.io/kubernetes/pkg/auth/authorizer"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
// NOTE(ericchiang): Can't get this strategy to do reads. Get cryptic "client: etcd cluster is unavailable or misconfigured"
|
||||
// Writes work fine, so use to test storing initial data.
|
||||
server := etcdtesting.NewEtcdTestClientServer(t)
|
||||
defer server.Terminate(t)
|
||||
|
||||
codec := testapi.Groups[rbac.GroupName].StorageCodec()
|
||||
getRESTOptions := func(resource string) generic.RESTOptions {
|
||||
cacheSize := etcdtest.DeserializationCacheSize
|
||||
storage := etcd.NewEtcdStorage(server.Client, codec, resource, false, cacheSize)
|
||||
return generic.RESTOptions{Storage: storage, Decorator: generic.UndecoratedStorage}
|
||||
}
|
||||
|
||||
roleRegistry := role.NewRegistry(roleetcd.NewREST(getRESTOptions("roles")))
|
||||
roleBindingRegistry := rolebinding.NewRegistry(rolebindingetcd.NewREST(getRESTOptions("rolebindings")))
|
||||
clusterRoleRegistry := clusterrole.NewRegistry(clusterroleetcd.NewREST(getRESTOptions("clusterroles")))
|
||||
clusterRoleBindingRegistry := clusterrolebinding.NewRegistry(clusterrolebindingetcd.NewREST(getRESTOptions("clusterrolebindings")))
|
||||
_, err := New(roleRegistry, roleBindingRegistry, clusterRoleRegistry, clusterRoleBindingRegistry, "")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create authorizer: %v", err)
|
||||
func newRule(verbs, apiGroups, resources string) rbac.PolicyRule {
|
||||
return rbac.PolicyRule{
|
||||
Verbs: strings.Split(verbs, ","),
|
||||
APIGroups: strings.Split(apiGroups, ","),
|
||||
Resources: strings.Split(resources, ","),
|
||||
}
|
||||
}
|
||||
|
||||
func newRole(name, namespace string, rules ...rbac.PolicyRule) rbac.Role {
|
||||
return rbac.Role{ObjectMeta: api.ObjectMeta{Namespace: namespace, Name: name}, Rules: rules}
|
||||
}
|
||||
|
||||
func newClusterRole(name string, rules ...rbac.PolicyRule) rbac.ClusterRole {
|
||||
return rbac.ClusterRole{ObjectMeta: api.ObjectMeta{Name: name}, Rules: rules}
|
||||
}
|
||||
|
||||
const (
|
||||
bindToRole uint16 = 0x0
|
||||
bindToClusterRole uint16 = 0x1
|
||||
)
|
||||
|
||||
func newRoleBinding(namespace, roleName string, bindType uint16, subjects ...string) rbac.RoleBinding {
|
||||
r := rbac.RoleBinding{ObjectMeta: api.ObjectMeta{Namespace: namespace}}
|
||||
|
||||
switch bindType {
|
||||
case bindToRole:
|
||||
r.RoleRef = api.ObjectReference{Kind: "Role", Namespace: namespace, Name: roleName}
|
||||
case bindToClusterRole:
|
||||
r.RoleRef = api.ObjectReference{Kind: "ClusterRole", Name: roleName}
|
||||
}
|
||||
|
||||
r.Subjects = make([]rbac.Subject, len(subjects))
|
||||
for i, subject := range subjects {
|
||||
split := strings.SplitN(subject, ":", 2)
|
||||
r.Subjects[i].Kind, r.Subjects[i].Name = split[0], split[1]
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type defaultAttributes struct {
|
||||
user string
|
||||
groups string
|
||||
verb string
|
||||
resource string
|
||||
namespace string
|
||||
apiGroup string
|
||||
}
|
||||
|
||||
func (d *defaultAttributes) String() string {
|
||||
return fmt.Sprintf("user=(%s), groups=(%s), verb=(%s), resource=(%s), namespace=(%s), apiGroup=(%s)",
|
||||
d.user, strings.Split(d.groups, ","), d.verb, d.resource, d.namespace, d.apiGroup)
|
||||
}
|
||||
|
||||
func (d *defaultAttributes) GetUserName() string { return d.user }
|
||||
func (d *defaultAttributes) GetGroups() []string { return strings.Split(d.groups, ",") }
|
||||
func (d *defaultAttributes) GetVerb() string { return d.verb }
|
||||
func (d *defaultAttributes) IsReadOnly() bool { return d.verb == "get" || d.verb == "watch" }
|
||||
func (d *defaultAttributes) GetNamespace() string { return d.namespace }
|
||||
func (d *defaultAttributes) GetResource() string { return d.resource }
|
||||
func (d *defaultAttributes) GetSubresource() string { return "" }
|
||||
func (d *defaultAttributes) GetName() string { return "" }
|
||||
func (d *defaultAttributes) GetAPIGroup() string { return d.apiGroup }
|
||||
func (d *defaultAttributes) GetAPIVersion() string { return "" }
|
||||
func (d *defaultAttributes) IsResourceRequest() bool { return true }
|
||||
func (d *defaultAttributes) GetPath() string { return "" }
|
||||
|
||||
func TestAuthorizer(t *testing.T) {
|
||||
tests := []struct {
|
||||
roles []rbac.Role
|
||||
roleBindings []rbac.RoleBinding
|
||||
clusterRoles []rbac.ClusterRole
|
||||
clusterRoleBindings []rbac.ClusterRoleBinding
|
||||
|
||||
superUser string
|
||||
|
||||
shouldPass []authorizer.Attributes
|
||||
shouldFail []authorizer.Attributes
|
||||
}{
|
||||
{
|
||||
clusterRoles: []rbac.ClusterRole{
|
||||
newClusterRole("admin", newRule("*", "*", "*")),
|
||||
},
|
||||
roleBindings: []rbac.RoleBinding{
|
||||
newRoleBinding("ns1", "admin", bindToClusterRole, "User:admin", "Group:admins"),
|
||||
},
|
||||
shouldPass: []authorizer.Attributes{
|
||||
&defaultAttributes{"admin", "", "get", "Pods", "ns1", ""},
|
||||
&defaultAttributes{"admin", "", "watch", "Pods", "ns1", ""},
|
||||
&defaultAttributes{"admin", "group1", "watch", "Foobar", "ns1", ""},
|
||||
&defaultAttributes{"joe", "admins", "watch", "Foobar", "ns1", ""},
|
||||
&defaultAttributes{"joe", "group1,admins", "watch", "Foobar", "ns1", ""},
|
||||
},
|
||||
shouldFail: []authorizer.Attributes{
|
||||
&defaultAttributes{"admin", "", "GET", "Pods", "ns2", ""},
|
||||
&defaultAttributes{"admin", "", "GET", "Nodes", "", ""},
|
||||
&defaultAttributes{"admin", "admins", "GET", "Pods", "ns2", ""},
|
||||
&defaultAttributes{"admin", "admins", "GET", "Nodes", "", ""},
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
ruleResolver := validation.NewTestRuleResolver(tt.roles, tt.roleBindings, tt.clusterRoles, tt.clusterRoleBindings)
|
||||
a := RBACAuthorizer{tt.superUser, ruleResolver}
|
||||
for _, attr := range tt.shouldPass {
|
||||
if err := a.Authorize(attr); err != nil {
|
||||
t.Errorf("case %d: incorrectly restricted %s: %T %v", i, attr, err, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, attr := range tt.shouldFail {
|
||||
if err := a.Authorize(attr); err == nil {
|
||||
t.Errorf("case %d: incorrectly passed %s", i, attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +76,13 @@ func NewExtensionsEtcdStorage(client etcd.Client) storage.Interface {
|
||||
return etcdstorage.NewEtcdStorage(client, testapi.Extensions.Codec(), etcdtest.PathPrefix(), false, etcdtest.DeserializationCacheSize)
|
||||
}
|
||||
|
||||
func NewRbacEtcdStorage(client etcd.Client) storage.Interface {
|
||||
if client == nil {
|
||||
client = NewEtcdClient()
|
||||
}
|
||||
return etcdstorage.NewEtcdStorage(client, testapi.Rbac.Codec(), etcdtest.PathPrefix(), false, etcdtest.DeserializationCacheSize)
|
||||
}
|
||||
|
||||
func RequireEtcd() {
|
||||
if _, err := etcd.NewKeysAPI(NewEtcdClient()).Get(context.TODO(), "/", nil); err != nil {
|
||||
glog.Fatalf("unable to connect to etcd for testing: %v", err)
|
||||
|
461
test/integration/rbac_test.go
Normal file
461
test/integration/rbac_test.go
Normal file
@ -0,0 +1,461 @@
|
||||
// +build integration,!no-etcd
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
rbacapi "k8s.io/kubernetes/pkg/apis/rbac"
|
||||
"k8s.io/kubernetes/pkg/apis/rbac/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/auth/authenticator"
|
||||
"k8s.io/kubernetes/pkg/auth/authenticator/bearertoken"
|
||||
"k8s.io/kubernetes/pkg/auth/authorizer"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
"k8s.io/kubernetes/pkg/client/transport"
|
||||
"k8s.io/kubernetes/pkg/master"
|
||||
"k8s.io/kubernetes/pkg/registry/clusterrole"
|
||||
clusterroleetcd "k8s.io/kubernetes/pkg/registry/clusterrole/etcd"
|
||||
"k8s.io/kubernetes/pkg/registry/clusterrolebinding"
|
||||
clusterrolebindingetcd "k8s.io/kubernetes/pkg/registry/clusterrolebinding/etcd"
|
||||
"k8s.io/kubernetes/pkg/registry/generic"
|
||||
"k8s.io/kubernetes/pkg/registry/role"
|
||||
roleetcd "k8s.io/kubernetes/pkg/registry/role/etcd"
|
||||
"k8s.io/kubernetes/pkg/registry/rolebinding"
|
||||
rolebindingetcd "k8s.io/kubernetes/pkg/registry/rolebinding/etcd"
|
||||
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
func newFakeAuthenticator() authenticator.Request {
|
||||
return bearertoken.New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) {
|
||||
if token == "" {
|
||||
return nil, false, errors.New("no bearer token found")
|
||||
}
|
||||
// Set the bearer token as the user name.
|
||||
return &user.DefaultInfo{Name: token, UID: token}, true, nil
|
||||
}))
|
||||
}
|
||||
|
||||
func clientForUser(user string) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: transport.NewBearerAuthRoundTripper(
|
||||
user,
|
||||
transport.DebugWrappers(http.DefaultTransport),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func newRBACAuthorizer(t *testing.T, superUser string, config *master.Config) authorizer.Authorizer {
|
||||
newRESTOptions := func(resource string) generic.RESTOptions {
|
||||
storageInterface, err := config.StorageFactory.New(rbacapi.Resource(resource))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get storage: %v", err)
|
||||
}
|
||||
return generic.RESTOptions{Storage: storageInterface, Decorator: generic.UndecoratedStorage}
|
||||
}
|
||||
|
||||
roleRegistry := role.NewRegistry(roleetcd.NewREST(newRESTOptions("roles")))
|
||||
roleBindingRegistry := rolebinding.NewRegistry(rolebindingetcd.NewREST(newRESTOptions("rolebindings")))
|
||||
clusterRoleRegistry := clusterrole.NewRegistry(clusterroleetcd.NewREST(newRESTOptions("clusterroles")))
|
||||
clusterRoleBindingRegistry := clusterrolebinding.NewRegistry(clusterrolebindingetcd.NewREST(newRESTOptions("clusterrolebindings")))
|
||||
return rbac.New(roleRegistry, roleBindingRegistry, clusterRoleRegistry, clusterRoleBindingRegistry, superUser)
|
||||
}
|
||||
|
||||
// bootstrapRoles are a set of RBAC roles which will be populated before the test.
|
||||
type bootstrapRoles struct {
|
||||
roles []v1alpha1.Role
|
||||
roleBindings []v1alpha1.RoleBinding
|
||||
clusterRoles []v1alpha1.ClusterRole
|
||||
clusterRoleBindings []v1alpha1.ClusterRoleBinding
|
||||
}
|
||||
|
||||
// bootstrap uses the provided client to create the bootstrap roles and role bindings.
|
||||
//
|
||||
// client should be authenticated as the RBAC super user.
|
||||
func (b bootstrapRoles) bootstrap(client *http.Client, serverURL string) error {
|
||||
newReq := func(resource, name, namespace string, v interface{}) *http.Request {
|
||||
body, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
path := testapi.Rbac.ResourcePath(resource, namespace, name)
|
||||
req, err := http.NewRequest("PUT", serverURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.ContentLength = int64(len(body))
|
||||
return req
|
||||
}
|
||||
|
||||
apiVersion := v1alpha1.SchemeGroupVersion.String()
|
||||
|
||||
var requests []*http.Request
|
||||
for _, r := range b.clusterRoles {
|
||||
r.TypeMeta = unversioned.TypeMeta{Kind: "ClusterRole", APIVersion: apiVersion}
|
||||
requests = append(requests, newReq("clusterroles", r.Name, r.Namespace, r))
|
||||
}
|
||||
for _, r := range b.roles {
|
||||
r.TypeMeta = unversioned.TypeMeta{Kind: "Role", APIVersion: apiVersion}
|
||||
requests = append(requests, newReq("roles", r.Name, r.Namespace, r))
|
||||
}
|
||||
for _, r := range b.clusterRoleBindings {
|
||||
r.TypeMeta = unversioned.TypeMeta{Kind: "ClusterRoleBinding", APIVersion: apiVersion}
|
||||
requests = append(requests, newReq("clusterrolebindings", r.Name, r.Namespace, r))
|
||||
}
|
||||
for _, r := range b.roleBindings {
|
||||
r.TypeMeta = unversioned.TypeMeta{Kind: "RoleBinding", APIVersion: apiVersion}
|
||||
requests = append(requests, newReq("rolebindings", r.Name, r.Namespace, r))
|
||||
}
|
||||
|
||||
for _, req := range requests {
|
||||
err := func() error {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read body: %v", err)
|
||||
}
|
||||
return fmt.Errorf("POST %s: expected %d got %s\n%s", req.URL, resp.Status, body)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// request is a test case which can.
|
||||
type request struct {
|
||||
// The username attempting to send the request.
|
||||
user string
|
||||
|
||||
// Resource metadata
|
||||
verb string
|
||||
apiGroup string
|
||||
resource string
|
||||
namespace string
|
||||
name string
|
||||
|
||||
// The actual resource.
|
||||
body string
|
||||
|
||||
// The expected return status of this request.
|
||||
expectedStatus int
|
||||
}
|
||||
|
||||
func (r request) String() string {
|
||||
return fmt.Sprintf("%s %s %s", r.user, r.verb, r.resource)
|
||||
}
|
||||
|
||||
type statusCode int
|
||||
|
||||
func (s statusCode) String() string {
|
||||
return fmt.Sprintf("%d %s", int(s), http.StatusText(int(s)))
|
||||
}
|
||||
|
||||
// Declare a set of raw objects to use.
|
||||
var (
|
||||
aJob = `
|
||||
{
|
||||
"apiVersion": "batch/v1",
|
||||
"kind": "Job",
|
||||
"metadata": {
|
||||
"name": "pi"%s
|
||||
},
|
||||
"spec": {
|
||||
"template": {
|
||||
"metadata": {
|
||||
"name": "a",
|
||||
"labels": {
|
||||
"name": "pijob"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "pi",
|
||||
"image": "perl",
|
||||
"command": [
|
||||
"perl",
|
||||
"-Mbignum=bpi",
|
||||
"-wle",
|
||||
"print bpi(2000)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"restartPolicy": "Never"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
jobNamespace = `
|
||||
{
|
||||
"apiVersion": "` + testapi.Default.GroupVersion().String() + `",
|
||||
"kind": "Namespace",
|
||||
"metadata": {
|
||||
"name": "job-namespace"%s
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
// Declare some PolicyRules beforehand.
|
||||
var (
|
||||
ruleAllowAll = v1alpha1.PolicyRule{
|
||||
Verbs: []string{"*"},
|
||||
APIGroups: []string{"*"},
|
||||
Resources: []string{"*"},
|
||||
}
|
||||
|
||||
ruleReadPods = v1alpha1.PolicyRule{
|
||||
Verbs: []string{"list", "get", "watch"},
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"pods"},
|
||||
}
|
||||
|
||||
ruleWriteJobs = v1alpha1.PolicyRule{
|
||||
Verbs: []string{"*"},
|
||||
APIGroups: []string{"batch"},
|
||||
Resources: []string{"*"},
|
||||
}
|
||||
)
|
||||
|
||||
func TestRBAC(t *testing.T) {
|
||||
superUser := "admin"
|
||||
|
||||
tests := []struct {
|
||||
bootstrapRoles bootstrapRoles
|
||||
|
||||
requests []request
|
||||
}{
|
||||
{
|
||||
bootstrapRoles: bootstrapRoles{
|
||||
clusterRoles: []v1alpha1.ClusterRole{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{Name: "allow-all"},
|
||||
Rules: []v1alpha1.PolicyRule{ruleAllowAll},
|
||||
},
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{Name: "read-pods"},
|
||||
Rules: []v1alpha1.PolicyRule{ruleReadPods},
|
||||
},
|
||||
},
|
||||
clusterRoleBindings: []v1alpha1.ClusterRoleBinding{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{Name: "read-pods"},
|
||||
Subjects: []v1alpha1.Subject{
|
||||
{Kind: "User", Name: "pod-reader"},
|
||||
},
|
||||
RoleRef: v1.ObjectReference{Kind: "ClusterRole", Name: "read-pods"},
|
||||
},
|
||||
},
|
||||
},
|
||||
requests: []request{
|
||||
{superUser, "GET", "", "pods", "", "", "", http.StatusOK},
|
||||
{superUser, "GET", "", "pods", api.NamespaceDefault, "a", "", http.StatusNotFound},
|
||||
{superUser, "POST", "", "pods", api.NamespaceDefault, "", aPod, http.StatusCreated},
|
||||
{superUser, "GET", "", "pods", api.NamespaceDefault, "a", "", http.StatusOK},
|
||||
|
||||
{"bob", "GET", "", "pods", "", "", "", http.StatusForbidden},
|
||||
{"bob", "GET", "", "pods", api.NamespaceDefault, "a", "", http.StatusForbidden},
|
||||
|
||||
{"pod-reader", "GET", "", "pods", "", "", "", http.StatusOK},
|
||||
{"pod-reader", "POST", "", "pods", api.NamespaceDefault, "", aPod, http.StatusForbidden},
|
||||
},
|
||||
},
|
||||
{
|
||||
bootstrapRoles: bootstrapRoles{
|
||||
clusterRoles: []v1alpha1.ClusterRole{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{Name: "write-jobs"},
|
||||
Rules: []v1alpha1.PolicyRule{ruleWriteJobs},
|
||||
},
|
||||
},
|
||||
clusterRoleBindings: []v1alpha1.ClusterRoleBinding{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{Name: "write-jobs"},
|
||||
Subjects: []v1alpha1.Subject{{Kind: "User", Name: "job-writer"}},
|
||||
RoleRef: v1.ObjectReference{Kind: "ClusterRole", Name: "write-jobs"},
|
||||
},
|
||||
},
|
||||
roleBindings: []v1alpha1.RoleBinding{
|
||||
{
|
||||
ObjectMeta: v1.ObjectMeta{Name: "write-jobs", Namespace: "job-namespace"},
|
||||
Subjects: []v1alpha1.Subject{{Kind: "User", Name: "job-writer-namespace"}},
|
||||
RoleRef: v1.ObjectReference{Kind: "ClusterRole", Name: "write-jobs"},
|
||||
},
|
||||
},
|
||||
},
|
||||
requests: []request{
|
||||
// Create the namespace used later in the test
|
||||
{superUser, "POST", "", "namespaces", "", "", jobNamespace, http.StatusCreated},
|
||||
|
||||
{"user-with-no-permissions", "POST", "batch", "jobs", "job-namespace", "", aJob, http.StatusForbidden},
|
||||
{"user-with-no-permissions", "GET", "batch", "jobs", "job-namespace", "pi", "", http.StatusForbidden},
|
||||
|
||||
// job-writer-namespace cannot write to the "default" namespace
|
||||
{"job-writer-namespace", "GET", "batch", "jobs", "default", "", "", http.StatusForbidden},
|
||||
{"job-writer-namespace", "GET", "batch", "jobs", "default", "pi", "", http.StatusForbidden},
|
||||
{"job-writer-namespace", "POST", "batch", "jobs", "default", "", aJob, http.StatusForbidden},
|
||||
{"job-writer-namespace", "GET", "batch", "jobs", "default", "pi", "", http.StatusForbidden},
|
||||
|
||||
// job-writer can write to any namespace
|
||||
{"job-writer", "GET", "batch", "jobs", "default", "", "", http.StatusOK},
|
||||
{"job-writer", "GET", "batch", "jobs", "default", "pi", "", http.StatusNotFound},
|
||||
{"job-writer", "POST", "batch", "jobs", "default", "", aJob, http.StatusCreated},
|
||||
{"job-writer", "GET", "batch", "jobs", "default", "pi", "", http.StatusOK},
|
||||
|
||||
{"job-writer-namespace", "GET", "batch", "jobs", "job-namespace", "", "", http.StatusOK},
|
||||
{"job-writer-namespace", "GET", "batch", "jobs", "job-namespace", "pi", "", http.StatusNotFound},
|
||||
{"job-writer-namespace", "POST", "batch", "jobs", "job-namespace", "", aJob, http.StatusCreated},
|
||||
{"job-writer-namespace", "GET", "batch", "jobs", "job-namespace", "pi", "", http.StatusOK},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
framework.DeleteAllEtcdKeys()
|
||||
|
||||
var m *master.Master
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
m.Handler.ServeHTTP(w, r)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
// Create an API Server.
|
||||
masterConfig := framework.NewIntegrationTestMasterConfig()
|
||||
masterConfig.Authorizer = newRBACAuthorizer(t, superUser, masterConfig)
|
||||
masterConfig.Authenticator = newFakeAuthenticator()
|
||||
masterConfig.AuthorizerRBACSuperUser = superUser
|
||||
m, err := master.New(masterConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("case %d: error bringing up master: %v", i, err)
|
||||
}
|
||||
|
||||
// Bootstrap the API Server with the test case's initial roles.
|
||||
if err := tc.bootstrapRoles.bootstrap(clientForUser(superUser), s.URL); err != nil {
|
||||
t.Errorf("case %d: failed to apply initial roles: %v", i, err)
|
||||
continue
|
||||
}
|
||||
previousResourceVersion := make(map[string]float64)
|
||||
|
||||
for j, r := range tc.requests {
|
||||
testGroup, ok := testapi.Groups[r.apiGroup]
|
||||
if !ok {
|
||||
t.Errorf("case %d %d: unknown api group %q, %s", i, j, r.apiGroup, r)
|
||||
continue
|
||||
}
|
||||
path := testGroup.ResourcePath(r.resource, r.namespace, r.name)
|
||||
|
||||
var body io.Reader
|
||||
if r.body != "" {
|
||||
sub := ""
|
||||
if r.verb == "PUT" {
|
||||
// For update operations, insert previous resource version
|
||||
if resVersion := previousResourceVersion[getPreviousResourceVersionKey(path, "")]; resVersion != 0 {
|
||||
sub += fmt.Sprintf(",\"resourceVersion\": \"%v\"", resVersion)
|
||||
}
|
||||
}
|
||||
// For any creation requests, add the namespace to the object meta.
|
||||
if r.verb == "POST" || r.verb == "PUT" {
|
||||
if r.namespace != "" {
|
||||
sub += fmt.Sprintf(",\"namespace\": %q", r.namespace)
|
||||
}
|
||||
}
|
||||
body = strings.NewReader(fmt.Sprintf(r.body, sub))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(r.verb, s.URL+path, body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
func() {
|
||||
reqDump, err := httputil.DumpRequest(req, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dump request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := clientForUser(r.user).Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("case %d, req %d: failed to make request: %v", i, j, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dump response: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != r.expectedStatus {
|
||||
// When debugging is on, dump the entire request and response. Very helpful for
|
||||
// debugging malformed test cases.
|
||||
//
|
||||
// To turn on debugging, use the '-args' flag.
|
||||
//
|
||||
// go test -v -tags integration -run RBAC -args -v 10
|
||||
//
|
||||
glog.V(8).Infof("case %d, req %d: %s\n%s\n", i, j, reqDump, respDump)
|
||||
t.Errorf("case %d, req %d: %s expected %q got %q", i, j, r, statusCode(r.expectedStatus), statusCode(resp.StatusCode))
|
||||
}
|
||||
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if r.verb == "POST" && (resp.StatusCode/100) == 2 {
|
||||
// For successful create operations, extract resourceVersion
|
||||
id, currentResourceVersion, err := parseResourceVersion(b)
|
||||
if err == nil {
|
||||
key := getPreviousResourceVersionKey(path, id)
|
||||
previousResourceVersion[key] = currentResourceVersion
|
||||
} else {
|
||||
t.Logf("error in trying to extract resource version: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user