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:
k8s-merge-robot 2016-06-19 19:19:08 -07:00 committed by GitHub
commit 6fbf99b11a
7 changed files with 669 additions and 107 deletions

View File

@ -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
}

View File

@ -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()

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}
}
}
}

View File

@ -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)

View 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)
}
}
}()
}
}
}