From d13e3510284cb00d8fbbb20f75a990ba82e7d98e Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 31 May 2016 16:50:33 -0700 Subject: [PATCH] add unit and integration tests for rbac authorizer --- pkg/apis/rbac/validation/rulevalidation.go | 81 ++- .../rbac/validation/rulevalidation_test.go | 66 --- pkg/apiserver/authz.go | 5 +- plugin/pkg/auth/authorizer/rbac/rbac.go | 4 +- plugin/pkg/auth/authorizer/rbac/rbac_test.go | 152 ++++-- test/integration/framework/etcd_utils.go | 7 + test/integration/rbac_test.go | 461 ++++++++++++++++++ 7 files changed, 669 insertions(+), 107 deletions(-) create mode 100644 test/integration/rbac_test.go diff --git a/pkg/apis/rbac/validation/rulevalidation.go b/pkg/apis/rbac/validation/rulevalidation.go index 04b15622dc4..3002f98842f 100644 --- a/pkg/apis/rbac/validation/rulevalidation.go +++ b/pkg/apis/rbac/validation/rulevalidation.go @@ -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 +} diff --git a/pkg/apis/rbac/validation/rulevalidation_test.go b/pkg/apis/rbac/validation/rulevalidation_test.go index ef2b2254052..747ae0487e9 100644 --- a/pkg/apis/rbac/validation/rulevalidation_test.go +++ b/pkg/apis/rbac/validation/rulevalidation_test.go @@ -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() diff --git a/pkg/apiserver/authz.go b/pkg/apiserver/authz.go index d12ae6b861c..2e7013df1b3 100644 --- a/pkg/apiserver/authz.go +++ b/pkg/apiserver/authz.go @@ -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) diff --git a/plugin/pkg/auth/authorizer/rbac/rbac.go b/plugin/pkg/auth/authorizer/rbac/rbac.go index 9fa1ec47ca6..afd415da7d7 100644 --- a/plugin/pkg/auth/authorizer/rbac/rbac.go +++ b/plugin/pkg/auth/authorizer/rbac/rbac.go @@ -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 } diff --git a/plugin/pkg/auth/authorizer/rbac/rbac_test.go b/plugin/pkg/auth/authorizer/rbac/rbac_test.go index 66ad85fb1a0..fc4f22de606 100644 --- a/plugin/pkg/auth/authorizer/rbac/rbac_test.go +++ b/plugin/pkg/auth/authorizer/rbac/rbac_test.go @@ -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) + } + } } } diff --git a/test/integration/framework/etcd_utils.go b/test/integration/framework/etcd_utils.go index 4cbd66b7e70..4ed2e8c4ab1 100644 --- a/test/integration/framework/etcd_utils.go +++ b/test/integration/framework/etcd_utils.go @@ -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) diff --git a/test/integration/rbac_test.go b/test/integration/rbac_test.go new file mode 100644 index 00000000000..4488ed0c718 --- /dev/null +++ b/test/integration/rbac_test.go @@ -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) + } + } + }() + } + } +}