k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/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 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.ControlPlane.Extra.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.ControlPlane.Generic.Authentication.Authenticator = unionauthn.New(config.ControlPlane.Generic.Authentication.Authenticator, authenticator) 560 // Append our custom test authorizer 561 var rbacAuthz authorizer.Authorizer 562 rbacAuthz, tearDownAuthorizerFn = newRBACAuthorizer(t, config) 563 config.ControlPlane.Generic.Authorization.Authorizer = unionauthz.New(config.ControlPlane.Generic.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 }