...

Source file src/k8s.io/kubernetes/test/integration/auth/rbac_test.go

Documentation: k8s.io/kubernetes/test/integration/auth

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package auth
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/http/httputil"
    25  	gopath "path"
    26  	"reflect"
    27  	"strings"
    28  	"testing"
    29  
    30  	rbacapi "k8s.io/api/rbac/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/apimachinery/pkg/types"
    34  	"k8s.io/apimachinery/pkg/watch"
    35  	"k8s.io/apiserver/pkg/authentication/group"
    36  	"k8s.io/apiserver/pkg/authentication/request/bearertoken"
    37  	unionauthn "k8s.io/apiserver/pkg/authentication/request/union"
    38  	"k8s.io/apiserver/pkg/authentication/token/tokenfile"
    39  	"k8s.io/apiserver/pkg/authentication/user"
    40  	"k8s.io/apiserver/pkg/authorization/authorizer"
    41  	unionauthz "k8s.io/apiserver/pkg/authorization/union"
    42  	"k8s.io/apiserver/pkg/registry/generic"
    43  	clientset "k8s.io/client-go/kubernetes"
    44  	restclient "k8s.io/client-go/rest"
    45  	watchtools "k8s.io/client-go/tools/watch"
    46  	"k8s.io/client-go/transport"
    47  	"k8s.io/klog/v2"
    48  	"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
    49  	rbachelper "k8s.io/kubernetes/pkg/apis/rbac/v1"
    50  	"k8s.io/kubernetes/pkg/controlplane"
    51  	"k8s.io/kubernetes/pkg/registry/rbac/clusterrole"
    52  	clusterrolestore "k8s.io/kubernetes/pkg/registry/rbac/clusterrole/storage"
    53  	"k8s.io/kubernetes/pkg/registry/rbac/clusterrolebinding"
    54  	clusterrolebindingstore "k8s.io/kubernetes/pkg/registry/rbac/clusterrolebinding/storage"
    55  	"k8s.io/kubernetes/pkg/registry/rbac/role"
    56  	rolestore "k8s.io/kubernetes/pkg/registry/rbac/role/storage"
    57  	"k8s.io/kubernetes/pkg/registry/rbac/rolebinding"
    58  	rolebindingstore "k8s.io/kubernetes/pkg/registry/rbac/rolebinding/storage"
    59  	"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
    60  	"k8s.io/kubernetes/test/integration/framework"
    61  	"k8s.io/kubernetes/test/utils/ktesting"
    62  )
    63  
    64  func clientForToken(user string, rt http.RoundTripper) *http.Client {
    65  	return &http.Client{
    66  		Transport: transport.NewBearerAuthRoundTripper(
    67  			user,
    68  			transport.DebugWrappers(rt),
    69  		),
    70  	}
    71  }
    72  
    73  func clientsetForToken(user string, config *restclient.Config) (clientset.Interface, clientset.Interface) {
    74  	configCopy := *config
    75  	configCopy.BearerToken = user
    76  	return clientset.NewForConfigOrDie(&configCopy), clientset.NewForConfigOrDie(&configCopy)
    77  }
    78  
    79  type testRESTOptionsGetter struct {
    80  	config *controlplane.Config
    81  }
    82  
    83  func (getter *testRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
    84  	storageConfig, err := getter.config.ExtraConfig.StorageFactory.NewConfig(resource)
    85  	if err != nil {
    86  		return generic.RESTOptions{}, fmt.Errorf("failed to get storage: %v", err)
    87  	}
    88  	return generic.RESTOptions{StorageConfig: storageConfig, Decorator: generic.UndecoratedStorage, ResourcePrefix: resource.Resource}, nil
    89  }
    90  
    91  func newRBACAuthorizer(t *testing.T, config *controlplane.Config) (authorizer.Authorizer, func()) {
    92  	optsGetter := &testRESTOptionsGetter{config}
    93  	roleRest, err := rolestore.NewREST(optsGetter)
    94  	if err != nil {
    95  		t.Fatalf("unexpected error from REST storage: %v", err)
    96  	}
    97  	roleRegistry := role.AuthorizerAdapter{Registry: role.NewRegistry(roleRest)}
    98  	rolebindingRest, err := rolebindingstore.NewREST(optsGetter)
    99  	if err != nil {
   100  		t.Fatalf("unexpected error from REST storage: %v", err)
   101  	}
   102  	roleBindingRegistry := rolebinding.AuthorizerAdapter{Registry: rolebinding.NewRegistry(rolebindingRest)}
   103  	clusterroleRest, err := clusterrolestore.NewREST(optsGetter)
   104  	if err != nil {
   105  		t.Fatalf("unexpected error from REST storage: %v", err)
   106  	}
   107  	clusterRoleRegistry := clusterrole.AuthorizerAdapter{Registry: clusterrole.NewRegistry(clusterroleRest)}
   108  	clusterrolebindingRest, err := clusterrolebindingstore.NewREST(optsGetter)
   109  	if err != nil {
   110  		t.Fatalf("unexpected error from REST storage: %v", err)
   111  	}
   112  	clusterRoleBindingRegistry := clusterrolebinding.AuthorizerAdapter{Registry: clusterrolebinding.NewRegistry(clusterrolebindingRest)}
   113  
   114  	tearDownFn := func() {
   115  		roleRest.Destroy()
   116  		rolebindingRest.Destroy()
   117  		clusterroleRest.Destroy()
   118  		clusterrolebindingRest.Destroy()
   119  	}
   120  	return rbac.New(roleRegistry, roleBindingRegistry, clusterRoleRegistry, clusterRoleBindingRegistry), tearDownFn
   121  }
   122  
   123  // bootstrapRoles are a set of RBAC roles which will be populated before the test.
   124  type bootstrapRoles struct {
   125  	roles               []rbacapi.Role
   126  	roleBindings        []rbacapi.RoleBinding
   127  	clusterRoles        []rbacapi.ClusterRole
   128  	clusterRoleBindings []rbacapi.ClusterRoleBinding
   129  }
   130  
   131  // bootstrap uses the provided client to create the bootstrap roles and role bindings.
   132  //
   133  // client should be authenticated as the RBAC super user.
   134  func (b bootstrapRoles) bootstrap(client clientset.Interface) error {
   135  	for _, r := range b.clusterRoles {
   136  		_, err := client.RbacV1().ClusterRoles().Create(context.TODO(), &r, metav1.CreateOptions{})
   137  		if err != nil {
   138  			return fmt.Errorf("failed to make request: %v", err)
   139  		}
   140  	}
   141  	for _, r := range b.roles {
   142  		_, err := client.RbacV1().Roles(r.Namespace).Create(context.TODO(), &r, metav1.CreateOptions{})
   143  		if err != nil {
   144  			return fmt.Errorf("failed to make request: %v", err)
   145  		}
   146  	}
   147  	for _, r := range b.clusterRoleBindings {
   148  		_, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), &r, metav1.CreateOptions{})
   149  		if err != nil {
   150  			return fmt.Errorf("failed to make request: %v", err)
   151  		}
   152  	}
   153  	for _, r := range b.roleBindings {
   154  		_, err := client.RbacV1().RoleBindings(r.Namespace).Create(context.TODO(), &r, metav1.CreateOptions{})
   155  		if err != nil {
   156  			return fmt.Errorf("failed to make request: %v", err)
   157  		}
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  // request is a test case which can.
   164  type request struct {
   165  	// The bearer token sent as part of the request
   166  	token string
   167  
   168  	// Resource metadata
   169  	verb      string
   170  	apiGroup  string
   171  	resource  string
   172  	namespace string
   173  	name      string
   174  
   175  	// The actual resource.
   176  	body string
   177  
   178  	// The expected return status of this request.
   179  	expectedStatus int
   180  }
   181  
   182  func (r request) String() string {
   183  	return fmt.Sprintf("%s %s %s", r.token, r.verb, r.resource)
   184  }
   185  
   186  type statusCode int
   187  
   188  func (s statusCode) String() string {
   189  	return fmt.Sprintf("%d %s", int(s), http.StatusText(int(s)))
   190  }
   191  
   192  // Declare a set of raw objects to use.
   193  var (
   194  	writeJobsRoleBinding = `
   195  {
   196    "apiVersion": "rbac.authorization.k8s.io/v1",
   197    "kind": "RoleBinding",
   198    "metadata": {
   199      "name": "pi"%s
   200    },
   201    "roleRef": {
   202      "apiGroup": "rbac.authorization.k8s.io",
   203      "kind": "ClusterRole",
   204      "name": "write-jobs"
   205    },
   206    "subjects": [{
   207      "apiGroup": "rbac.authorization.k8s.io",
   208      "kind": "User",
   209      "name": "admin"
   210    }]
   211  }`
   212  
   213  	aJob = `
   214  {
   215    "apiVersion": "batch/v1",
   216    "kind": "Job",
   217    "metadata": {
   218      "name": "pi"%s
   219    },
   220    "spec": {
   221      "template": {
   222        "metadata": {
   223          "name": "a",
   224          "labels": {
   225            "name": "pijob"
   226          }
   227        },
   228        "spec": {
   229          "containers": [
   230            {
   231              "name": "pi",
   232              "image": "perl",
   233              "command": [
   234                "perl",
   235                "-Mbignum=bpi",
   236                "-wle",
   237                "print bpi(2000)"
   238              ]
   239            }
   240          ],
   241          "restartPolicy": "Never"
   242        }
   243      }
   244    }
   245  }
   246  `
   247  	aLimitRange = `
   248  {
   249    "apiVersion": "v1",
   250    "kind": "LimitRange",
   251    "metadata": {
   252      "name": "a"%s
   253    }
   254  }
   255  `
   256  	podNamespace = `
   257  {
   258    "apiVersion": "v1",
   259    "kind": "Namespace",
   260    "metadata": {
   261  	"name": "pod-namespace"%s
   262    }
   263  }
   264  `
   265  	jobNamespace = `
   266  {
   267    "apiVersion": "v1",
   268    "kind": "Namespace",
   269    "metadata": {
   270  	"name": "job-namespace"%s
   271    }
   272  }
   273  `
   274  	forbiddenNamespace = `
   275  {
   276    "apiVersion": "v1",
   277    "kind": "Namespace",
   278    "metadata": {
   279  	"name": "forbidden-namespace"%s
   280    }
   281  }
   282  `
   283  	limitRangeNamespace = `
   284  {
   285    "apiVersion": "v1",
   286    "kind": "Namespace",
   287    "metadata": {
   288  	"name": "limitrange-namespace"%s
   289    }
   290  }
   291  `
   292  )
   293  
   294  // Declare some PolicyRules beforehand.
   295  var (
   296  	ruleAllowAll  = rbachelper.NewRule("*").Groups("*").Resources("*").RuleOrDie()
   297  	ruleReadPods  = rbachelper.NewRule("list", "get", "watch").Groups("").Resources("pods").RuleOrDie()
   298  	ruleWriteJobs = rbachelper.NewRule("*").Groups("batch").Resources("*").RuleOrDie()
   299  )
   300  
   301  func TestRBAC(t *testing.T) {
   302  	superUser := "admin/system:masters"
   303  
   304  	tests := []struct {
   305  		bootstrapRoles bootstrapRoles
   306  
   307  		requests []request
   308  	}{
   309  		{
   310  			bootstrapRoles: bootstrapRoles{
   311  				clusterRoles: []rbacapi.ClusterRole{
   312  					{
   313  						ObjectMeta: metav1.ObjectMeta{Name: "allow-all"},
   314  						Rules:      []rbacapi.PolicyRule{ruleAllowAll},
   315  					},
   316  					{
   317  						ObjectMeta: metav1.ObjectMeta{Name: "read-pods"},
   318  						Rules:      []rbacapi.PolicyRule{ruleReadPods},
   319  					},
   320  				},
   321  				clusterRoleBindings: []rbacapi.ClusterRoleBinding{
   322  					{
   323  						ObjectMeta: metav1.ObjectMeta{Name: "read-pods"},
   324  						Subjects: []rbacapi.Subject{
   325  							{Kind: "User", Name: "pod-reader"},
   326  						},
   327  						RoleRef: rbacapi.RoleRef{Kind: "ClusterRole", Name: "read-pods"},
   328  					},
   329  				},
   330  			},
   331  			requests: []request{
   332  				// Create the namespace used later in the test
   333  				{superUser, "POST", "", "namespaces", "", "", podNamespace, http.StatusCreated},
   334  
   335  				{superUser, "GET", "", "pods", "", "", "", http.StatusOK},
   336  				{superUser, "GET", "", "pods", "pod-namespace", "a", "", http.StatusNotFound},
   337  				{superUser, "POST", "", "pods", "pod-namespace", "", aPod, http.StatusCreated},
   338  				{superUser, "GET", "", "pods", "pod-namespace", "a", "", http.StatusOK},
   339  
   340  				{"bob", "GET", "", "pods", "", "", "", http.StatusForbidden},
   341  				{"bob", "GET", "", "pods", "pod-namespace", "a", "", http.StatusForbidden},
   342  
   343  				{"pod-reader", "GET", "", "pods", "", "", "", http.StatusOK},
   344  				{"pod-reader", "POST", "", "pods", "pod-namespace", "", aPod, http.StatusForbidden},
   345  			},
   346  		},
   347  		{
   348  			bootstrapRoles: bootstrapRoles{
   349  				clusterRoles: []rbacapi.ClusterRole{
   350  					{
   351  						ObjectMeta: metav1.ObjectMeta{Name: "write-jobs"},
   352  						Rules:      []rbacapi.PolicyRule{ruleWriteJobs},
   353  					},
   354  					{
   355  						ObjectMeta: metav1.ObjectMeta{Name: "create-rolebindings"},
   356  						Rules: []rbacapi.PolicyRule{
   357  							rbachelper.NewRule("create").Groups("rbac.authorization.k8s.io").Resources("rolebindings").RuleOrDie(),
   358  						},
   359  					},
   360  					{
   361  						ObjectMeta: metav1.ObjectMeta{Name: "bind-any-clusterrole"},
   362  						Rules: []rbacapi.PolicyRule{
   363  							rbachelper.NewRule("bind").Groups("rbac.authorization.k8s.io").Resources("clusterroles").RuleOrDie(),
   364  						},
   365  					},
   366  				},
   367  				clusterRoleBindings: []rbacapi.ClusterRoleBinding{
   368  					{
   369  						ObjectMeta: metav1.ObjectMeta{Name: "write-jobs"},
   370  						Subjects:   []rbacapi.Subject{{Kind: "User", Name: "job-writer"}},
   371  						RoleRef:    rbacapi.RoleRef{Kind: "ClusterRole", Name: "write-jobs"},
   372  					},
   373  					{
   374  						ObjectMeta: metav1.ObjectMeta{Name: "create-rolebindings"},
   375  						Subjects: []rbacapi.Subject{
   376  							{Kind: "User", Name: "job-writer"},
   377  							{Kind: "User", Name: "nonescalating-rolebinding-writer"},
   378  							{Kind: "User", Name: "any-rolebinding-writer"},
   379  						},
   380  						RoleRef: rbacapi.RoleRef{Kind: "ClusterRole", Name: "create-rolebindings"},
   381  					},
   382  					{
   383  						ObjectMeta: metav1.ObjectMeta{Name: "bind-any-clusterrole"},
   384  						Subjects:   []rbacapi.Subject{{Kind: "User", Name: "any-rolebinding-writer"}},
   385  						RoleRef:    rbacapi.RoleRef{Kind: "ClusterRole", Name: "bind-any-clusterrole"},
   386  					},
   387  				},
   388  				roleBindings: []rbacapi.RoleBinding{
   389  					{
   390  						ObjectMeta: metav1.ObjectMeta{Name: "write-jobs", Namespace: "job-namespace"},
   391  						Subjects:   []rbacapi.Subject{{Kind: "User", Name: "job-writer-namespace"}},
   392  						RoleRef:    rbacapi.RoleRef{Kind: "ClusterRole", Name: "write-jobs"},
   393  					},
   394  					{
   395  						ObjectMeta: metav1.ObjectMeta{Name: "create-rolebindings", Namespace: "job-namespace"},
   396  						Subjects: []rbacapi.Subject{
   397  							{Kind: "User", Name: "job-writer-namespace"},
   398  							{Kind: "User", Name: "any-rolebinding-writer-namespace"},
   399  						},
   400  						RoleRef: rbacapi.RoleRef{Kind: "ClusterRole", Name: "create-rolebindings"},
   401  					},
   402  					{
   403  						ObjectMeta: metav1.ObjectMeta{Name: "bind-any-clusterrole", Namespace: "job-namespace"},
   404  						Subjects:   []rbacapi.Subject{{Kind: "User", Name: "any-rolebinding-writer-namespace"}},
   405  						RoleRef:    rbacapi.RoleRef{Kind: "ClusterRole", Name: "bind-any-clusterrole"},
   406  					},
   407  				},
   408  			},
   409  			requests: []request{
   410  				// Create the namespace used later in the test
   411  				{superUser, "POST", "", "namespaces", "", "", jobNamespace, http.StatusCreated},
   412  				{superUser, "POST", "", "namespaces", "", "", forbiddenNamespace, http.StatusCreated},
   413  
   414  				{"user-with-no-permissions", "POST", "batch", "jobs", "job-namespace", "", aJob, http.StatusForbidden},
   415  				{"user-with-no-permissions", "GET", "batch", "jobs", "job-namespace", "pi", "", http.StatusForbidden},
   416  
   417  				// job-writer-namespace cannot write to the "forbidden-namespace"
   418  				{"job-writer-namespace", "GET", "batch", "jobs", "forbidden-namespace", "", "", http.StatusForbidden},
   419  				{"job-writer-namespace", "GET", "batch", "jobs", "forbidden-namespace", "pi", "", http.StatusForbidden},
   420  				{"job-writer-namespace", "POST", "batch", "jobs", "forbidden-namespace", "", aJob, http.StatusForbidden},
   421  				{"job-writer-namespace", "GET", "batch", "jobs", "forbidden-namespace", "pi", "", http.StatusForbidden},
   422  
   423  				// job-writer can write to any namespace
   424  				{"job-writer", "GET", "batch", "jobs", "forbidden-namespace", "", "", http.StatusOK},
   425  				{"job-writer", "GET", "batch", "jobs", "forbidden-namespace", "pi", "", http.StatusNotFound},
   426  				{"job-writer", "POST", "batch", "jobs", "forbidden-namespace", "", aJob, http.StatusCreated},
   427  				{"job-writer", "GET", "batch", "jobs", "forbidden-namespace", "pi", "", http.StatusOK},
   428  
   429  				{"job-writer-namespace", "GET", "batch", "jobs", "job-namespace", "", "", http.StatusOK},
   430  				{"job-writer-namespace", "GET", "batch", "jobs", "job-namespace", "pi", "", http.StatusNotFound},
   431  				{"job-writer-namespace", "POST", "batch", "jobs", "job-namespace", "", aJob, http.StatusCreated},
   432  				{"job-writer-namespace", "GET", "batch", "jobs", "job-namespace", "pi", "", http.StatusOK},
   433  
   434  				// cannot bind role anywhere
   435  				{"user-with-no-permissions", "POST", "rbac.authorization.k8s.io", "rolebindings", "job-namespace", "", writeJobsRoleBinding, http.StatusForbidden},
   436  				// can only bind role in namespace where they have explicit bind permission
   437  				{"any-rolebinding-writer-namespace", "POST", "rbac.authorization.k8s.io", "rolebindings", "forbidden-namespace", "", writeJobsRoleBinding, http.StatusForbidden},
   438  				// can only bind role in namespace where they have covering permissions
   439  				{"job-writer-namespace", "POST", "rbac.authorization.k8s.io", "rolebindings", "forbidden-namespace", "", writeJobsRoleBinding, http.StatusForbidden},
   440  				{"job-writer-namespace", "POST", "rbac.authorization.k8s.io", "rolebindings", "job-namespace", "", writeJobsRoleBinding, http.StatusCreated},
   441  				{superUser, "DELETE", "rbac.authorization.k8s.io", "rolebindings", "job-namespace", "pi", "", http.StatusOK},
   442  				// can bind role in any namespace where they have covering permissions
   443  				{"job-writer", "POST", "rbac.authorization.k8s.io", "rolebindings", "forbidden-namespace", "", writeJobsRoleBinding, http.StatusCreated},
   444  				{superUser, "DELETE", "rbac.authorization.k8s.io", "rolebindings", "forbidden-namespace", "pi", "", http.StatusOK},
   445  				// cannot bind role because they don't have covering permissions
   446  				{"nonescalating-rolebinding-writer", "POST", "rbac.authorization.k8s.io", "rolebindings", "job-namespace", "", writeJobsRoleBinding, http.StatusForbidden},
   447  				// can bind role because they have explicit bind permission
   448  				{"any-rolebinding-writer", "POST", "rbac.authorization.k8s.io", "rolebindings", "job-namespace", "", writeJobsRoleBinding, http.StatusCreated},
   449  				{superUser, "DELETE", "rbac.authorization.k8s.io", "rolebindings", "job-namespace", "pi", "", http.StatusOK},
   450  				{"any-rolebinding-writer-namespace", "POST", "rbac.authorization.k8s.io", "rolebindings", "job-namespace", "", writeJobsRoleBinding, http.StatusCreated},
   451  				{superUser, "DELETE", "rbac.authorization.k8s.io", "rolebindings", "job-namespace", "pi", "", http.StatusOK},
   452  			},
   453  		},
   454  		{
   455  			bootstrapRoles: bootstrapRoles{
   456  				clusterRoles: []rbacapi.ClusterRole{
   457  					{
   458  						ObjectMeta: metav1.ObjectMeta{Name: "allow-all"},
   459  						Rules:      []rbacapi.PolicyRule{ruleAllowAll},
   460  					},
   461  					{
   462  						ObjectMeta: metav1.ObjectMeta{Name: "update-limitranges"},
   463  						Rules: []rbacapi.PolicyRule{
   464  							rbachelper.NewRule("update").Groups("").Resources("limitranges").RuleOrDie(),
   465  						},
   466  					},
   467  				},
   468  				clusterRoleBindings: []rbacapi.ClusterRoleBinding{
   469  					{
   470  						ObjectMeta: metav1.ObjectMeta{Name: "update-limitranges"},
   471  						Subjects: []rbacapi.Subject{
   472  							{Kind: "User", Name: "limitrange-updater"},
   473  						},
   474  						RoleRef: rbacapi.RoleRef{Kind: "ClusterRole", Name: "update-limitranges"},
   475  					},
   476  				},
   477  			},
   478  			requests: []request{
   479  				// Create the namespace used later in the test
   480  				{superUser, "POST", "", "namespaces", "", "", limitRangeNamespace, http.StatusCreated},
   481  
   482  				{"limitrange-updater", "PUT", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusForbidden},
   483  				{superUser, "PUT", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusCreated},
   484  				{superUser, "PUT", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusOK},
   485  				{"limitrange-updater", "PUT", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusOK},
   486  			},
   487  		},
   488  		{
   489  			bootstrapRoles: bootstrapRoles{
   490  				clusterRoles: []rbacapi.ClusterRole{
   491  					{
   492  						ObjectMeta: metav1.ObjectMeta{Name: "allow-all"},
   493  						Rules:      []rbacapi.PolicyRule{ruleAllowAll},
   494  					},
   495  					{
   496  						ObjectMeta: metav1.ObjectMeta{Name: "patch-limitranges"},
   497  						Rules: []rbacapi.PolicyRule{
   498  							rbachelper.NewRule("patch").Groups("").Resources("limitranges").RuleOrDie(),
   499  						},
   500  					},
   501  				},
   502  				clusterRoleBindings: []rbacapi.ClusterRoleBinding{
   503  					{
   504  						ObjectMeta: metav1.ObjectMeta{Name: "patch-limitranges"},
   505  						Subjects: []rbacapi.Subject{
   506  							{Kind: "User", Name: "limitrange-patcher"},
   507  						},
   508  						RoleRef: rbacapi.RoleRef{Kind: "ClusterRole", Name: "patch-limitranges"},
   509  					},
   510  				},
   511  			},
   512  			requests: []request{
   513  				// Create the namespace used later in the test
   514  				{superUser, "POST", "", "namespaces", "", "", limitRangeNamespace, http.StatusCreated},
   515  
   516  				{"limitrange-patcher", "PATCH", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusForbidden},
   517  				{superUser, "PATCH", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusCreated},
   518  				{superUser, "PATCH", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusOK},
   519  				{"limitrange-patcher", "PATCH", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusOK},
   520  			},
   521  		},
   522  	}
   523  
   524  	for i, tc := range tests {
   525  		t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
   526  			authenticator := group.NewAuthenticatedGroupAdder(bearertoken.New(tokenfile.New(map[string]*user.DefaultInfo{
   527  				superUser:                          {Name: "admin", Groups: []string{"system:masters"}},
   528  				"any-rolebinding-writer":           {Name: "any-rolebinding-writer"},
   529  				"any-rolebinding-writer-namespace": {Name: "any-rolebinding-writer-namespace"},
   530  				"bob":                              {Name: "bob"},
   531  				"job-writer":                       {Name: "job-writer"},
   532  				"job-writer-namespace":             {Name: "job-writer-namespace"},
   533  				"nonescalating-rolebinding-writer": {Name: "nonescalating-rolebinding-writer"},
   534  				"pod-reader":                       {Name: "pod-reader"},
   535  				"limitrange-updater":               {Name: "limitrange-updater"},
   536  				"limitrange-patcher":               {Name: "limitrange-patcher"},
   537  				"user-with-no-permissions":         {Name: "user-with-no-permissions"},
   538  			})))
   539  
   540  			tCtx := ktesting.Init(t)
   541  			var tearDownAuthorizerFn func()
   542  			defer func() {
   543  				if tearDownAuthorizerFn != nil {
   544  					tearDownAuthorizerFn()
   545  				}
   546  			}()
   547  
   548  			_, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{
   549  				ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
   550  					// Disable ServiceAccount admission plugin as we don't have serviceaccount controller running.
   551  					// Also disable namespace lifecycle to workaroung the test limitation that first creates
   552  					// roles/rolebindings and only then creates corresponding namespaces.
   553  					opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount", "NamespaceLifecycle"}
   554  					// Disable built-in authorizers
   555  					opts.Authorization.Modes = []string{"AlwaysDeny"}
   556  				},
   557  				ModifyServerConfig: func(config *controlplane.Config) {
   558  					// Append our custom test authenticator
   559  					config.GenericConfig.Authentication.Authenticator = unionauthn.New(config.GenericConfig.Authentication.Authenticator, authenticator)
   560  					// Append our custom test authorizer
   561  					var rbacAuthz authorizer.Authorizer
   562  					rbacAuthz, tearDownAuthorizerFn = newRBACAuthorizer(t, config)
   563  					config.GenericConfig.Authorization.Authorizer = unionauthz.New(config.GenericConfig.Authorization.Authorizer, rbacAuthz)
   564  				},
   565  			})
   566  			defer tearDownFn()
   567  
   568  			transport, err := restclient.TransportFor(kubeConfig)
   569  			if err != nil {
   570  				t.Fatal(err)
   571  			}
   572  
   573  			// Bootstrap the API Server with the test case's initial roles.
   574  			superuserClient, _ := clientsetForToken(superUser, kubeConfig)
   575  			if err := tc.bootstrapRoles.bootstrap(superuserClient); err != nil {
   576  				t.Errorf("case %d: failed to apply initial roles: %v", i, err)
   577  				return
   578  			}
   579  			previousResourceVersion := make(map[string]float64)
   580  
   581  			for j, r := range tc.requests {
   582  				// This is a URL-path, not a local path, so we use the "path"
   583  				// package (aliased as "gopath") instead of "path/filepath".
   584  				urlPath := "/"
   585  				if r.apiGroup == "" {
   586  					urlPath = gopath.Join(urlPath, "api/v1")
   587  				} else {
   588  					urlPath = gopath.Join(urlPath, "apis", r.apiGroup, "v1")
   589  				}
   590  				if r.namespace != "" {
   591  					urlPath = gopath.Join(urlPath, "namespaces", r.namespace)
   592  				}
   593  				if r.resource != "" {
   594  					urlPath = gopath.Join(urlPath, r.resource)
   595  				}
   596  				if r.name != "" {
   597  					urlPath = gopath.Join(urlPath, r.name)
   598  				}
   599  
   600  				var body io.Reader
   601  				if r.body != "" {
   602  					sub := ""
   603  					if r.verb == "PUT" {
   604  						// For update operations, insert previous resource version
   605  						if resVersion := previousResourceVersion[getPreviousResourceVersionKey(urlPath, "")]; resVersion != 0 {
   606  							sub += fmt.Sprintf(",\"resourceVersion\": \"%v\"", resVersion)
   607  						}
   608  					}
   609  					body = strings.NewReader(fmt.Sprintf(r.body, sub))
   610  				}
   611  
   612  				req, err := http.NewRequest(r.verb, kubeConfig.Host+urlPath, body)
   613  				if r.verb == "PATCH" {
   614  					// For patch operations, use the apply content type
   615  					req.Header.Add("Content-Type", string(types.ApplyPatchType))
   616  					q := req.URL.Query()
   617  					q.Add("fieldManager", "rbac_test")
   618  					req.URL.RawQuery = q.Encode()
   619  				}
   620  
   621  				if err != nil {
   622  					t.Fatalf("failed to create request: %v", err)
   623  				}
   624  
   625  				func() {
   626  					reqDump, err := httputil.DumpRequest(req, true)
   627  					if err != nil {
   628  						t.Fatalf("failed to dump request: %v", err)
   629  						return
   630  					}
   631  
   632  					resp, err := clientForToken(r.token, transport).Do(req)
   633  					if err != nil {
   634  						t.Errorf("case %d, req %d: failed to make request: %v", i, j, err)
   635  						return
   636  					}
   637  					defer resp.Body.Close()
   638  
   639  					respDump, err := httputil.DumpResponse(resp, true)
   640  					if err != nil {
   641  						t.Fatalf("failed to dump response: %v", err)
   642  						return
   643  					}
   644  
   645  					if resp.StatusCode != r.expectedStatus {
   646  						// When debugging is on, dump the entire request and response. Very helpful for
   647  						// debugging malformed test cases.
   648  						//
   649  						// To turn on debugging, use the '-args' flag.
   650  						//
   651  						//    go test -v -tags integration -run RBAC -args -v 10
   652  						//
   653  						klog.V(8).Infof("case %d, req %d: %s\n%s\n", i, j, reqDump, respDump)
   654  						t.Errorf("case %d, req %d: %s expected %q got %q", i, j, r, statusCode(r.expectedStatus), statusCode(resp.StatusCode))
   655  					}
   656  
   657  					b, _ := io.ReadAll(resp.Body)
   658  
   659  					if r.verb == "POST" && (resp.StatusCode/100) == 2 {
   660  						// For successful create operations, extract resourceVersion
   661  						id, currentResourceVersion, err := parseResourceVersion(b)
   662  						if err == nil {
   663  							key := getPreviousResourceVersionKey(urlPath, id)
   664  							previousResourceVersion[key] = currentResourceVersion
   665  						} else {
   666  							t.Logf("error in trying to extract resource version: %s", err)
   667  						}
   668  					}
   669  				}()
   670  			}
   671  		})
   672  	}
   673  }
   674  
   675  func TestBootstrapping(t *testing.T) {
   676  	tCtx := ktesting.Init(t)
   677  	clientset, _, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{
   678  		ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
   679  			opts.Authorization.Modes = []string{"RBAC"}
   680  		},
   681  	})
   682  	defer tearDownFn()
   683  
   684  	watcher, err := clientset.RbacV1().ClusterRoles().Watch(tCtx, metav1.ListOptions{ResourceVersion: "0"})
   685  	if err != nil {
   686  		t.Fatalf("unexpected error: %v", err)
   687  	}
   688  
   689  	_, err = watchtools.UntilWithoutRetry(tCtx, watcher, func(event watch.Event) (bool, error) {
   690  		if event.Type != watch.Added {
   691  			return false, nil
   692  		}
   693  		return true, nil
   694  	})
   695  	if err != nil {
   696  		t.Fatalf("unexpected error: %v", err)
   697  	}
   698  
   699  	clusterRoles, err := clientset.RbacV1().ClusterRoles().List(tCtx, metav1.ListOptions{})
   700  	if err != nil {
   701  		t.Fatalf("unexpected error: %v", err)
   702  	}
   703  	if len(clusterRoles.Items) == 0 {
   704  		t.Fatalf("missing cluster roles")
   705  	}
   706  
   707  	for _, clusterRole := range clusterRoles.Items {
   708  		if clusterRole.Name == "cluster-admin" {
   709  			return
   710  		}
   711  	}
   712  
   713  	t.Errorf("missing cluster-admin: %v", clusterRoles)
   714  
   715  	healthBytes, err := clientset.Discovery().RESTClient().Get().AbsPath("/healthz/poststarthook/rbac/bootstrap-roles").DoRaw(tCtx)
   716  	if err != nil {
   717  		t.Error(err)
   718  	}
   719  	t.Errorf("error bootstrapping roles: %s", string(healthBytes))
   720  }
   721  
   722  // TestDiscoveryUpgradeBootstrapping is primarily meant to test the behavior of
   723  // primePublicInfoClusterRoleBinding in storage_rbac.go during cluster upgrades.
   724  func TestDiscoveryUpgradeBootstrapping(t *testing.T) {
   725  	var tearDownFn func()
   726  	defer func() {
   727  		if tearDownFn != nil {
   728  			tearDownFn()
   729  		}
   730  	}()
   731  
   732  	etcdConfig := framework.SharedEtcd()
   733  
   734  	tCtx := ktesting.Init(t)
   735  	client, _, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{
   736  		ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
   737  			// Ensure we're using the same etcd across apiserver restarts.
   738  			opts.Etcd.StorageConfig = *etcdConfig
   739  			opts.Authorization.Modes = []string{"RBAC"}
   740  		},
   741  	})
   742  
   743  	// Modify the default RBAC discovery ClusterRoleBidnings to look more like the defaults that
   744  	// existed prior to v1.14, but with user modifications.
   745  	t.Logf("Modifying default `system:discovery` ClusterRoleBinding")
   746  	discRoleBinding, err := client.RbacV1().ClusterRoleBindings().Get(tCtx, "system:discovery", metav1.GetOptions{})
   747  	if err != nil {
   748  		t.Fatalf("Failed to get `system:discovery` ClusterRoleBinding: %v", err)
   749  	}
   750  	discRoleBinding.Annotations["rbac.authorization.kubernetes.io/autoupdate"] = "false"
   751  	discRoleBinding.Annotations["rbac-discovery-upgrade-test"] = "pass"
   752  	discRoleBinding.Subjects = []rbacapi.Subject{
   753  		{
   754  			Name:     "system:authenticated",
   755  			Kind:     "Group",
   756  			APIGroup: "rbac.authorization.k8s.io",
   757  		},
   758  	}
   759  	if discRoleBinding, err = client.RbacV1().ClusterRoleBindings().Update(tCtx, discRoleBinding, metav1.UpdateOptions{}); err != nil {
   760  		t.Fatalf("Failed to update `system:discovery` ClusterRoleBinding: %v", err)
   761  	}
   762  	t.Logf("Modifying default `system:basic-user` ClusterRoleBinding")
   763  	basicUserRoleBinding, err := client.RbacV1().ClusterRoleBindings().Get(tCtx, "system:basic-user", metav1.GetOptions{})
   764  	if err != nil {
   765  		t.Fatalf("Failed to get `system:basic-user` ClusterRoleBinding: %v", err)
   766  	}
   767  	basicUserRoleBinding.Annotations["rbac.authorization.kubernetes.io/autoupdate"] = "false"
   768  	basicUserRoleBinding.Annotations["rbac-discovery-upgrade-test"] = "pass"
   769  	if basicUserRoleBinding, err = client.RbacV1().ClusterRoleBindings().Update(tCtx, basicUserRoleBinding, metav1.UpdateOptions{}); err != nil {
   770  		t.Fatalf("Failed to update `system:basic-user` ClusterRoleBinding: %v", err)
   771  	}
   772  	t.Logf("Deleting default `system:public-info-viewer` ClusterRoleBinding")
   773  	if err = client.RbacV1().ClusterRoleBindings().Delete(tCtx, "system:public-info-viewer", metav1.DeleteOptions{}); err != nil {
   774  		t.Fatalf("Failed to delete `system:public-info-viewer` ClusterRoleBinding: %v", err)
   775  	}
   776  
   777  	// Stop the first API server.
   778  	tearDownFn()
   779  	tearDownFn = nil
   780  
   781  	// Check that upgraded API servers inherit `system:public-info-viewer` settings from
   782  	// `system:discovery`, and respect auto-reconciliation annotations.
   783  	client, _, tearDownFn = framework.StartTestServer(tCtx, t, framework.TestServerSetup{
   784  		ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
   785  			// Ensure we're using the same etcd across apiserver restarts.
   786  			opts.Etcd.StorageConfig = *etcdConfig
   787  			opts.Authorization.Modes = []string{"RBAC"}
   788  		},
   789  	})
   790  
   791  	newDiscRoleBinding, err := client.RbacV1().ClusterRoleBindings().Get(tCtx, "system:discovery", metav1.GetOptions{})
   792  	if err != nil {
   793  		t.Fatalf("Failed to get `system:discovery` ClusterRoleBinding: %v", err)
   794  	}
   795  	if !reflect.DeepEqual(newDiscRoleBinding, discRoleBinding) {
   796  		t.Errorf("`system:discovery` should have been unmodified. Wanted: %v, got %v", discRoleBinding, newDiscRoleBinding)
   797  	}
   798  	newBasicUserRoleBinding, err := client.RbacV1().ClusterRoleBindings().Get(tCtx, "system:basic-user", metav1.GetOptions{})
   799  	if err != nil {
   800  		t.Fatalf("Failed to get `system:basic-user` ClusterRoleBinding: %v", err)
   801  	}
   802  	if !reflect.DeepEqual(newBasicUserRoleBinding, basicUserRoleBinding) {
   803  		t.Errorf("`system:basic-user` should have been unmodified. Wanted: %v, got %v", basicUserRoleBinding, newBasicUserRoleBinding)
   804  	}
   805  	publicInfoViewerRoleBinding, err := client.RbacV1().ClusterRoleBindings().Get(tCtx, "system:public-info-viewer", metav1.GetOptions{})
   806  	if err != nil {
   807  		t.Fatalf("Failed to get `system:public-info-viewer` ClusterRoleBinding: %v", err)
   808  	}
   809  	if publicInfoViewerRoleBinding.Annotations["rbac.authorization.kubernetes.io/autoupdate"] != "false" {
   810  		t.Errorf("publicInfoViewerRoleBinding.Annotations[\"rbac.authorization.kubernetes.io/autoupdate\"] should be %v, got %v", publicInfoViewerRoleBinding.Annotations["rbac.authorization.kubernetes.io/autoupdate"], "false")
   811  	}
   812  	if publicInfoViewerRoleBinding.Annotations["rbac-discovery-upgrade-test"] != "pass" {
   813  		t.Errorf("publicInfoViewerRoleBinding.Annotations[\"rbac-discovery-upgrade-test\"] should be %v, got %v", publicInfoViewerRoleBinding.Annotations["rbac-discovery-upgrade-test"], "pass")
   814  	}
   815  	if !reflect.DeepEqual(publicInfoViewerRoleBinding.Subjects, newDiscRoleBinding.Subjects) {
   816  		t.Errorf("`system:public-info-viewer` should have inherited Subjects from `system:discovery` Wanted: %v, got %v", newDiscRoleBinding.Subjects, publicInfoViewerRoleBinding.Subjects)
   817  	}
   818  }
   819  

View as plain text