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