k8s.io/kubernetes@v1.29.3/test/integration/auth/auth_test.go (about) 1 /* 2 Copyright 2014 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 // This file tests authentication and (soon) authorization of HTTP requests to an API server object. 20 // It does not use the client in pkg/client/... because authentication and authorization needs 21 // to work for any client of the HTTP interface. 22 23 import ( 24 "bytes" 25 "context" 26 "crypto/ed25519" 27 "crypto/rand" 28 "crypto/x509" 29 "crypto/x509/pkix" 30 "encoding/json" 31 "encoding/pem" 32 "fmt" 33 "io" 34 "net/http" 35 "net/http/httptest" 36 "net/url" 37 "os" 38 "path/filepath" 39 "strconv" 40 "strings" 41 "testing" 42 "time" 43 44 utiltesting "k8s.io/client-go/util/testing" 45 46 "github.com/google/go-cmp/cmp" 47 48 authenticationv1beta1 "k8s.io/api/authentication/v1beta1" 49 certificatesv1 "k8s.io/api/certificates/v1" 50 rbacv1 "k8s.io/api/rbac/v1" 51 "k8s.io/apimachinery/pkg/api/errors" 52 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 53 utilnet "k8s.io/apimachinery/pkg/util/net" 54 "k8s.io/apimachinery/pkg/util/wait" 55 "k8s.io/apiserver/pkg/authentication/authenticator" 56 "k8s.io/apiserver/pkg/authentication/group" 57 "k8s.io/apiserver/pkg/authentication/request/bearertoken" 58 "k8s.io/apiserver/pkg/authentication/serviceaccount" 59 "k8s.io/apiserver/pkg/authentication/token/cache" 60 "k8s.io/apiserver/pkg/authorization/authorizer" 61 unionauthz "k8s.io/apiserver/pkg/authorization/union" 62 webhookutil "k8s.io/apiserver/pkg/util/webhook" 63 "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" 64 clientset "k8s.io/client-go/kubernetes" 65 "k8s.io/client-go/rest" 66 v1 "k8s.io/client-go/tools/clientcmd/api/v1" 67 resttransport "k8s.io/client-go/transport" 68 "k8s.io/kubernetes/cmd/kube-apiserver/app/options" 69 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 70 "k8s.io/kubernetes/pkg/apis/autoscaling" 71 api "k8s.io/kubernetes/pkg/apis/core" 72 "k8s.io/kubernetes/pkg/apis/extensions" 73 "k8s.io/kubernetes/pkg/controlplane" 74 "k8s.io/kubernetes/test/integration" 75 "k8s.io/kubernetes/test/integration/authutil" 76 "k8s.io/kubernetes/test/integration/framework" 77 "k8s.io/kubernetes/test/utils/ktesting" 78 ) 79 80 const ( 81 AliceToken string = "abc123" // username: alice. Present in token file. 82 BobToken string = "xyz987" // username: bob. Present in token file. 83 UnknownToken string = "qwerty" // Not present in token file. 84 ) 85 86 func getTestWebhookTokenAuth(serverURL string, customDial utilnet.DialFunc) (authenticator.Request, error) { 87 kubecfgFile, err := os.CreateTemp("", "webhook-kubecfg") 88 if err != nil { 89 return nil, err 90 } 91 defer utiltesting.CloseAndRemove(&testing.T{}, kubecfgFile) 92 config := v1.Config{ 93 Clusters: []v1.NamedCluster{ 94 { 95 Cluster: v1.Cluster{Server: serverURL}, 96 }, 97 }, 98 } 99 if err := json.NewEncoder(kubecfgFile).Encode(config); err != nil { 100 return nil, err 101 } 102 103 retryBackoff := wait.Backoff{ 104 Duration: 500 * time.Millisecond, 105 Factor: 1.5, 106 Jitter: 0.2, 107 Steps: 5, 108 } 109 110 clientConfig, err := webhookutil.LoadKubeconfig(kubecfgFile.Name(), customDial) 111 if err != nil { 112 return nil, err 113 } 114 115 webhookTokenAuth, err := webhook.New(clientConfig, "v1beta1", nil, retryBackoff) 116 if err != nil { 117 return nil, err 118 } 119 return bearertoken.New(cache.New(webhookTokenAuth, false, 2*time.Minute, 2*time.Minute)), nil 120 } 121 122 func getTestWebhookTokenAuthCustomDialer(serverURL string) (authenticator.Request, error) { 123 customDial := http.DefaultTransport.(*http.Transport).DialContext 124 125 return getTestWebhookTokenAuth(serverURL, customDial) 126 } 127 128 func path(resource, namespace, name string) string { 129 return pathWithPrefix("", resource, namespace, name) 130 } 131 132 func pathWithPrefix(prefix, resource, namespace, name string) string { 133 path := "/api/v1" 134 if prefix != "" { 135 path = path + "/" + prefix 136 } 137 if namespace != "" { 138 path = path + "/namespaces/" + namespace 139 } 140 // Resource names are lower case. 141 resource = strings.ToLower(resource) 142 if resource != "" { 143 path = path + "/" + resource 144 } 145 if name != "" { 146 path = path + "/" + name 147 } 148 return path 149 } 150 151 func pathWithSubResource(resource, namespace, name, subresource string) string { 152 path := pathWithPrefix("", resource, namespace, name) 153 if subresource != "" { 154 path = path + "/" + subresource 155 } 156 return path 157 } 158 159 func timeoutPath(resource, namespace, name string) string { 160 return addTimeoutFlag(path(resource, namespace, name)) 161 } 162 163 // Bodies for requests used in subsequent tests. 164 var aPod = ` 165 { 166 "kind": "Pod", 167 "apiVersion": "v1", 168 "metadata": { 169 "name": "a", 170 "creationTimestamp": null%s 171 }, 172 "spec": { 173 "containers": [ 174 { 175 "name": "foo", 176 "image": "bar/foo" 177 } 178 ] 179 } 180 } 181 ` 182 var aRC = ` 183 { 184 "kind": "ReplicationController", 185 "apiVersion": "v1", 186 "metadata": { 187 "name": "a", 188 "labels": { 189 "name": "a" 190 }%s 191 }, 192 "spec": { 193 "replicas": 2, 194 "selector": { 195 "name": "a" 196 }, 197 "template": { 198 "metadata": { 199 "labels": { 200 "name": "a" 201 } 202 }, 203 "spec": { 204 "containers": [ 205 { 206 "name": "foo", 207 "image": "bar/foo" 208 } 209 ] 210 } 211 } 212 } 213 } 214 ` 215 var aService = ` 216 { 217 "kind": "Service", 218 "apiVersion": "v1", 219 "metadata": { 220 "name": "a", 221 "labels": { 222 "name": "a" 223 }%s 224 }, 225 "spec": { 226 "ports": [ 227 { 228 "protocol": "TCP", 229 "port": 8000 230 } 231 ], 232 "selector": { 233 "name": "a" 234 }, 235 "clusterIP": "10.0.0.100" 236 } 237 } 238 ` 239 var aNode = ` 240 { 241 "kind": "Node", 242 "apiVersion": "v1", 243 "metadata": { 244 "name": "a"%s 245 }, 246 "spec": { 247 "externalID": "external" 248 } 249 } 250 ` 251 252 func aEvent(namespace string) string { 253 return ` 254 { 255 "kind": "Event", 256 "apiVersion": "v1", 257 "metadata": { 258 "name": "a"%s 259 }, 260 "involvedObject": { 261 "kind": "Pod", 262 "namespace": "` + namespace + `", 263 "name": "a", 264 "apiVersion": "v1" 265 } 266 } 267 ` 268 } 269 270 var aBinding = ` 271 { 272 "kind": "Binding", 273 "apiVersion": "v1", 274 "metadata": { 275 "name": "a"%s 276 }, 277 "target": { 278 "name": "10.10.10.10" 279 } 280 } 281 ` 282 283 var emptyEndpoints = ` 284 { 285 "kind": "Endpoints", 286 "apiVersion": "v1", 287 "metadata": { 288 "name": "a"%s 289 } 290 } 291 ` 292 293 var aEndpoints = ` 294 { 295 "kind": "Endpoints", 296 "apiVersion": "v1", 297 "metadata": { 298 "name": "a"%s 299 }, 300 "subsets": [ 301 { 302 "addresses": [ 303 { 304 "ip": "10.10.1.1" 305 } 306 ], 307 "ports": [ 308 { 309 "port": 1909, 310 "protocol": "TCP" 311 } 312 ] 313 } 314 ] 315 } 316 ` 317 318 var deleteNow = ` 319 { 320 "kind": "DeleteOptions", 321 "apiVersion": "v1", 322 "gracePeriodSeconds": 0%s 323 } 324 ` 325 326 // To ensure that a POST completes before a dependent GET, set a timeout. 327 func addTimeoutFlag(URLString string) string { 328 u, _ := url.Parse(URLString) 329 values := u.Query() 330 values.Set("timeout", "60s") 331 u.RawQuery = values.Encode() 332 return u.String() 333 } 334 335 type testRequest struct { 336 verb string 337 URL string 338 body string 339 statusCodes map[int]bool // allowed status codes. 340 } 341 342 func getTestRequests(namespace string) []testRequest { 343 requests := []testRequest{ 344 // Normal methods on pods 345 {"GET", path("pods", "", ""), "", integration.Code200}, 346 {"GET", path("pods", namespace, ""), "", integration.Code200}, 347 {"POST", timeoutPath("pods", namespace, ""), aPod, integration.Code201}, 348 {"PUT", timeoutPath("pods", namespace, "a"), aPod, integration.Code200}, 349 {"GET", path("pods", namespace, "a"), "", integration.Code200}, 350 // GET and POST for /exec should return Bad Request (400) since the pod has not been assigned a node yet. 351 {"GET", path("pods", namespace, "a") + "/exec", "", integration.Code400}, 352 {"POST", path("pods", namespace, "a") + "/exec", "", integration.Code400}, 353 // PUT for /exec should return Method Not Allowed (405). 354 {"PUT", path("pods", namespace, "a") + "/exec", "", integration.Code405}, 355 // GET and POST for /portforward should return Bad Request (400) since the pod has not been assigned a node yet. 356 {"GET", path("pods", namespace, "a") + "/portforward", "", integration.Code400}, 357 {"POST", path("pods", namespace, "a") + "/portforward", "", integration.Code400}, 358 // PUT for /portforward should return Method Not Allowed (405). 359 {"PUT", path("pods", namespace, "a") + "/portforward", "", integration.Code405}, 360 {"PATCH", path("pods", namespace, "a"), "{%v}", integration.Code200}, 361 {"DELETE", timeoutPath("pods", namespace, "a"), deleteNow, integration.Code200}, 362 363 // Non-standard methods (not expected to work, 364 // but expected to pass/fail authorization prior to 365 // failing validation. 366 {"OPTIONS", path("pods", namespace, ""), "", integration.Code405}, 367 {"OPTIONS", path("pods", namespace, "a"), "", integration.Code405}, 368 {"HEAD", path("pods", namespace, ""), "", integration.Code405}, 369 {"HEAD", path("pods", namespace, "a"), "", integration.Code405}, 370 {"TRACE", path("pods", namespace, ""), "", integration.Code405}, 371 {"TRACE", path("pods", namespace, "a"), "", integration.Code405}, 372 {"NOSUCHVERB", path("pods", namespace, ""), "", integration.Code405}, 373 374 // Normal methods on services 375 {"GET", path("services", "", ""), "", integration.Code200}, 376 {"GET", path("services", namespace, ""), "", integration.Code200}, 377 {"POST", timeoutPath("services", namespace, ""), aService, integration.Code201}, 378 // Create an endpoint for the service (this is done automatically by endpoint controller 379 // whenever a service is created, but this test does not run that controller) 380 {"POST", timeoutPath("endpoints", namespace, ""), emptyEndpoints, integration.Code201}, 381 // Should return service unavailable when endpoint.subset is empty. 382 {"GET", pathWithSubResource("services", namespace, "a", "proxy") + "/", "", integration.Code503}, 383 {"PUT", timeoutPath("services", namespace, "a"), aService, integration.Code200}, 384 {"GET", path("services", namespace, "a"), "", integration.Code200}, 385 {"DELETE", timeoutPath("endpoints", namespace, "a"), "", integration.Code200}, 386 {"DELETE", timeoutPath("services", namespace, "a"), "", integration.Code200}, 387 388 // Normal methods on replicationControllers 389 {"GET", path("replicationControllers", "", ""), "", integration.Code200}, 390 {"GET", path("replicationControllers", namespace, ""), "", integration.Code200}, 391 {"POST", timeoutPath("replicationControllers", namespace, ""), aRC, integration.Code201}, 392 {"PUT", timeoutPath("replicationControllers", namespace, "a"), aRC, integration.Code200}, 393 {"GET", path("replicationControllers", namespace, "a"), "", integration.Code200}, 394 {"DELETE", timeoutPath("replicationControllers", namespace, "a"), "", integration.Code200}, 395 396 // Normal methods on endpoints 397 {"GET", path("endpoints", "", ""), "", integration.Code200}, 398 {"GET", path("endpoints", namespace, ""), "", integration.Code200}, 399 {"POST", timeoutPath("endpoints", namespace, ""), aEndpoints, integration.Code201}, 400 {"PUT", timeoutPath("endpoints", namespace, "a"), aEndpoints, integration.Code200}, 401 {"GET", path("endpoints", namespace, "a"), "", integration.Code200}, 402 {"DELETE", timeoutPath("endpoints", namespace, "a"), "", integration.Code200}, 403 404 // Normal methods on nodes 405 {"GET", path("nodes", "", ""), "", integration.Code200}, 406 {"POST", timeoutPath("nodes", "", ""), aNode, integration.Code201}, 407 {"PUT", timeoutPath("nodes", "", "a"), aNode, integration.Code200}, 408 {"GET", path("nodes", "", "a"), "", integration.Code200}, 409 {"DELETE", timeoutPath("nodes", "", "a"), "", integration.Code200}, 410 411 // Normal methods on events 412 {"GET", path("events", "", ""), "", integration.Code200}, 413 {"GET", path("events", namespace, ""), "", integration.Code200}, 414 {"POST", timeoutPath("events", namespace, ""), aEvent(namespace), integration.Code201}, 415 {"PUT", timeoutPath("events", namespace, "a"), aEvent(namespace), integration.Code200}, 416 {"GET", path("events", namespace, "a"), "", integration.Code200}, 417 {"DELETE", timeoutPath("events", namespace, "a"), "", integration.Code200}, 418 419 // Normal methods on bindings 420 {"GET", path("bindings", namespace, ""), "", integration.Code405}, 421 {"POST", timeoutPath("pods", namespace, ""), aPod, integration.Code201}, // Need a pod to bind or you get a 404 422 {"POST", timeoutPath("bindings", namespace, ""), aBinding, integration.Code201}, 423 {"PUT", timeoutPath("bindings", namespace, "a"), aBinding, integration.Code404}, 424 {"GET", path("bindings", namespace, "a"), "", integration.Code404}, // No bindings instances 425 {"DELETE", timeoutPath("bindings", namespace, "a"), "", integration.Code404}, 426 427 // Non-existent object type. 428 {"GET", path("foo", "", ""), "", integration.Code404}, 429 {"POST", path("foo", namespace, ""), `{"foo": "foo"}`, integration.Code404}, 430 {"PUT", path("foo", namespace, "a"), `{"foo": "foo"}`, integration.Code404}, 431 {"GET", path("foo", namespace, "a"), "", integration.Code404}, 432 {"DELETE", timeoutPath("foo", namespace, ""), "", integration.Code404}, 433 434 // Special verbs on nodes 435 {"GET", pathWithSubResource("nodes", namespace, "a", "proxy"), "", integration.Code404}, 436 {"GET", pathWithPrefix("redirect", "nodes", namespace, "a"), "", integration.Code404}, 437 // TODO: test .../watch/..., which doesn't end before the test timeout. 438 // TODO: figure out how to create a node so that it can successfully proxy/redirect. 439 440 // Non-object endpoints 441 {"GET", "/", "", integration.Code200}, 442 {"GET", "/api", "", integration.Code200}, 443 {"GET", "/healthz", "", integration.Code200}, 444 {"GET", "/version", "", integration.Code200}, 445 {"GET", "/invalidURL", "", integration.Code404}, 446 } 447 return requests 448 } 449 450 // The TestAuthMode* tests a large number of URLs and checks that they 451 // are FORBIDDEN or not, depending on the mode. They do not attempt to do 452 // detailed verification of behaviour beyond authorization. They are not 453 // fuzz tests. 454 // 455 // TODO(etune): write a fuzz test of the REST API. 456 func TestAuthModeAlwaysAllow(t *testing.T) { 457 _, ctx := ktesting.NewTestContext(t) 458 ctx, cancel := context.WithCancel(ctx) 459 defer cancel() 460 461 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 462 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 463 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 464 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 465 opts.Authorization.Modes = []string{"AlwaysAllow"} 466 }, 467 }) 468 defer tearDownFn() 469 470 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-always-allow", t) 471 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 472 473 transport, err := rest.TransportFor(kubeConfig) 474 if err != nil { 475 t.Fatal(err) 476 } 477 previousResourceVersion := make(map[string]float64) 478 479 for _, r := range getTestRequests(ns.Name) { 480 var bodyStr string 481 if r.body != "" { 482 sub := "" 483 if r.verb == "PUT" { 484 // For update operations, insert previous resource version 485 if resVersion := previousResourceVersion[getPreviousResourceVersionKey(r.URL, "")]; resVersion != 0 { 486 sub += fmt.Sprintf(",\r\n\"resourceVersion\": \"%v\"", resVersion) 487 } 488 sub += fmt.Sprintf(",\r\n\"namespace\": %q", ns.Name) 489 } 490 bodyStr = fmt.Sprintf(r.body, sub) 491 } 492 r.body = bodyStr 493 bodyBytes := bytes.NewReader([]byte(bodyStr)) 494 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 495 if err != nil { 496 t.Logf("case %v", r) 497 t.Fatalf("unexpected error: %v", err) 498 } 499 if r.verb == "PATCH" { 500 req.Header.Set("Content-Type", "application/merge-patch+json") 501 } 502 func() { 503 resp, err := transport.RoundTrip(req) 504 if err != nil { 505 t.Logf("case %v", r) 506 t.Fatalf("unexpected error: %v", err) 507 } 508 defer resp.Body.Close() 509 b, _ := io.ReadAll(resp.Body) 510 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 511 t.Logf("case %v", r) 512 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 513 t.Errorf("Body: %v", string(b)) 514 } else { 515 if r.verb == "POST" { 516 // For successful create operations, extract resourceVersion 517 id, currentResourceVersion, err := parseResourceVersion(b) 518 if err == nil { 519 key := getPreviousResourceVersionKey(r.URL, id) 520 previousResourceVersion[key] = currentResourceVersion 521 } else { 522 t.Logf("error in trying to extract resource version: %s", err) 523 } 524 } 525 } 526 }() 527 } 528 } 529 530 func parseResourceVersion(response []byte) (string, float64, error) { 531 var resultBodyMap map[string]interface{} 532 err := json.Unmarshal(response, &resultBodyMap) 533 if err != nil { 534 return "", 0, fmt.Errorf("unexpected error unmarshaling resultBody: %v", err) 535 } 536 metadata, ok := resultBodyMap["metadata"].(map[string]interface{}) 537 if !ok { 538 return "", 0, fmt.Errorf("unexpected error, metadata not found in JSON response: %v", string(response)) 539 } 540 id, ok := metadata["name"].(string) 541 if !ok { 542 return "", 0, fmt.Errorf("unexpected error, id not found in JSON response: %v", string(response)) 543 } 544 resourceVersionString, ok := metadata["resourceVersion"].(string) 545 if !ok { 546 return "", 0, fmt.Errorf("unexpected error, resourceVersion not found in JSON response: %v", string(response)) 547 } 548 resourceVersion, err := strconv.ParseFloat(resourceVersionString, 64) 549 if err != nil { 550 return "", 0, fmt.Errorf("unexpected error, could not parse resourceVersion as float64, err: %s. JSON response: %v", err, string(response)) 551 } 552 return id, resourceVersion, nil 553 } 554 555 func getPreviousResourceVersionKey(url, id string) string { 556 baseURL := strings.Split(url, "?")[0] 557 key := baseURL 558 if id != "" { 559 key = fmt.Sprintf("%s/%v", baseURL, id) 560 } 561 return key 562 } 563 564 func TestAuthModeAlwaysDeny(t *testing.T) { 565 _, ctx := ktesting.NewTestContext(t) 566 ctx, cancel := context.WithCancel(ctx) 567 defer cancel() 568 569 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 570 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 571 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 572 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 573 opts.Authorization.Modes = []string{"AlwaysDeny"} 574 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 575 }, 576 }) 577 defer tearDownFn() 578 579 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-always-deny", t) 580 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 581 582 transport, err := rest.TransportFor(kubeConfig) 583 if err != nil { 584 t.Fatal(err) 585 } 586 transport = resttransport.NewBearerAuthRoundTripper(AliceToken, transport) 587 588 for _, r := range getTestRequests(ns.Name) { 589 bodyBytes := bytes.NewReader([]byte(r.body)) 590 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 591 if err != nil { 592 t.Logf("case %v", r) 593 t.Fatalf("unexpected error: %v", err) 594 } 595 func() { 596 resp, err := transport.RoundTrip(req) 597 if err != nil { 598 t.Logf("case %v", r) 599 t.Fatalf("unexpected error: %v", err) 600 } 601 defer resp.Body.Close() 602 if resp.StatusCode != http.StatusForbidden { 603 t.Logf("case %v", r) 604 t.Errorf("Expected status Forbidden but got status %v", resp.Status) 605 } 606 }() 607 } 608 } 609 610 // TestAliceNotForbiddenOrUnauthorized tests a user who is known to 611 // the authentication system and authorized to do any actions. 612 func TestAliceNotForbiddenOrUnauthorized(t *testing.T) { 613 _, ctx := ktesting.NewTestContext(t) 614 ctx, cancel := context.WithCancel(ctx) 615 defer cancel() 616 617 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 618 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 619 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 620 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 621 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 622 opts.Authorization.Modes = []string{"ABAC"} 623 opts.Authorization.PolicyFile = "testdata/allowalice.jsonl" 624 }, 625 }) 626 defer tearDownFn() 627 628 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-alice-not-forbidden", t) 629 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 630 631 previousResourceVersion := make(map[string]float64) 632 transport, err := rest.TransportFor(kubeConfig) 633 if err != nil { 634 t.Fatal(err) 635 } 636 637 for _, r := range getTestRequests(ns.Name) { 638 token := AliceToken 639 var bodyStr string 640 if r.body != "" { 641 sub := "" 642 if r.verb == "PUT" { 643 // For update operations, insert previous resource version 644 if resVersion := previousResourceVersion[getPreviousResourceVersionKey(r.URL, "")]; resVersion != 0 { 645 sub += fmt.Sprintf(",\r\n\"resourceVersion\": \"%v\"", resVersion) 646 } 647 sub += fmt.Sprintf(",\r\n\"namespace\": %q", ns.Name) 648 } 649 bodyStr = fmt.Sprintf(r.body, sub) 650 } 651 r.body = bodyStr 652 bodyBytes := bytes.NewReader([]byte(bodyStr)) 653 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 654 if err != nil { 655 t.Fatalf("unexpected error: %v", err) 656 } 657 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 658 if r.verb == "PATCH" { 659 req.Header.Set("Content-Type", "application/merge-patch+json") 660 } 661 662 func() { 663 resp, err := transport.RoundTrip(req) 664 if err != nil { 665 t.Logf("case %v", r) 666 t.Fatalf("unexpected error: %v", err) 667 } 668 defer resp.Body.Close() 669 b, _ := io.ReadAll(resp.Body) 670 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 671 t.Logf("case %v", r) 672 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 673 t.Errorf("Body: %v", string(b)) 674 } else { 675 if r.verb == "POST" { 676 // For successful create operations, extract resourceVersion 677 id, currentResourceVersion, err := parseResourceVersion(b) 678 if err == nil { 679 key := getPreviousResourceVersionKey(r.URL, id) 680 previousResourceVersion[key] = currentResourceVersion 681 } 682 } 683 } 684 685 }() 686 } 687 } 688 689 // TestBobIsForbidden tests that a user who is known to 690 // the authentication system but not authorized to do any actions 691 // should receive "Forbidden". 692 func TestBobIsForbidden(t *testing.T) { 693 _, ctx := ktesting.NewTestContext(t) 694 ctx, cancel := context.WithCancel(ctx) 695 defer cancel() 696 697 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 698 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 699 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 700 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 701 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 702 opts.Authorization.Modes = []string{"ABAC"} 703 opts.Authorization.PolicyFile = "testdata/allowalice.jsonl" 704 }, 705 }) 706 defer tearDownFn() 707 708 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-bob-forbidden", t) 709 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 710 711 transport, err := rest.TransportFor(kubeConfig) 712 if err != nil { 713 t.Fatal(err) 714 } 715 716 for _, r := range getTestRequests(ns.Name) { 717 token := BobToken 718 bodyBytes := bytes.NewReader([]byte(r.body)) 719 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 720 if err != nil { 721 t.Fatalf("unexpected error: %v", err) 722 } 723 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 724 725 func() { 726 resp, err := transport.RoundTrip(req) 727 if err != nil { 728 t.Logf("case %v", r) 729 t.Fatalf("unexpected error: %v", err) 730 } 731 defer resp.Body.Close() 732 // Expect all of bob's actions to return Forbidden 733 if resp.StatusCode != http.StatusForbidden { 734 t.Logf("case %v", r) 735 t.Errorf("Expected not status Forbidden, but got %s", resp.Status) 736 } 737 }() 738 } 739 } 740 741 // TestUnknownUserIsUnauthorized tests that a user who is unknown 742 // to the authentication system get status code "Unauthorized". 743 // An authorization module is installed in this scenario for integration 744 // test purposes, but requests aren't expected to reach it. 745 func TestUnknownUserIsUnauthorized(t *testing.T) { 746 _, ctx := ktesting.NewTestContext(t) 747 ctx, cancel := context.WithCancel(ctx) 748 defer cancel() 749 750 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 751 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 752 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 753 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 754 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 755 opts.Authorization.Modes = []string{"ABAC"} 756 opts.Authorization.PolicyFile = "testdata/allowalice.jsonl" 757 }, 758 }) 759 defer tearDownFn() 760 761 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-unknown-unauthorized", t) 762 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 763 764 transport, err := rest.TransportFor(kubeConfig) 765 if err != nil { 766 t.Fatal(err) 767 } 768 769 for _, r := range getTestRequests(ns.Name) { 770 token := UnknownToken 771 bodyBytes := bytes.NewReader([]byte(r.body)) 772 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 773 if err != nil { 774 t.Fatalf("unexpected error: %v", err) 775 } 776 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 777 func() { 778 resp, err := transport.RoundTrip(req) 779 if err != nil { 780 t.Logf("case %v", r) 781 t.Fatalf("unexpected error: %v", err) 782 } 783 defer resp.Body.Close() 784 // Expect all of unauthenticated user's request to be "Unauthorized" 785 if resp.StatusCode != http.StatusUnauthorized { 786 t.Logf("case %v", r) 787 t.Errorf("Expected status %v, but got %v", http.StatusUnauthorized, resp.StatusCode) 788 b, _ := io.ReadAll(resp.Body) 789 t.Errorf("Body: %v", string(b)) 790 } 791 }() 792 } 793 } 794 795 type impersonateAuthorizer struct{} 796 797 // alice can't act as anyone and bob can't do anything but act-as someone 798 func (impersonateAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { 799 // alice can impersonate service accounts and do other actions 800 if a.GetUser() != nil && a.GetUser().GetName() == "alice" && a.GetVerb() == "impersonate" && a.GetResource() == "serviceaccounts" { 801 return authorizer.DecisionAllow, "", nil 802 } 803 if a.GetUser() != nil && a.GetUser().GetName() == "alice" && a.GetVerb() != "impersonate" { 804 return authorizer.DecisionAllow, "", nil 805 } 806 // bob can impersonate anyone, but that's it 807 if a.GetUser() != nil && a.GetUser().GetName() == "bob" && a.GetVerb() == "impersonate" { 808 return authorizer.DecisionAllow, "", nil 809 } 810 if a.GetUser() != nil && a.GetUser().GetName() == "bob" && a.GetVerb() != "impersonate" { 811 return authorizer.DecisionDeny, "", nil 812 } 813 // service accounts can do everything 814 if a.GetUser() != nil && strings.HasPrefix(a.GetUser().GetName(), serviceaccount.ServiceAccountUsernamePrefix) { 815 return authorizer.DecisionAllow, "", nil 816 } 817 818 return authorizer.DecisionNoOpinion, "I can't allow that. Go ask alice.", nil 819 } 820 821 func TestImpersonateIsForbidden(t *testing.T) { 822 _, ctx := ktesting.NewTestContext(t) 823 ctx, cancel := context.WithCancel(ctx) 824 defer cancel() 825 826 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 827 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 828 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 829 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 830 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 831 }, 832 ModifyServerConfig: func(config *controlplane.Config) { 833 // Prepend an impersonation authorizer with specific opinions about alice and bob 834 config.GenericConfig.Authorization.Authorizer = unionauthz.New(impersonateAuthorizer{}, config.GenericConfig.Authorization.Authorizer) 835 }, 836 }) 837 defer tearDownFn() 838 839 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-impersonate-forbidden", t) 840 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 841 842 transport, err := rest.TransportFor(kubeConfig) 843 if err != nil { 844 t.Fatal(err) 845 } 846 847 // bob can't perform actions himself 848 for _, r := range getTestRequests(ns.Name) { 849 token := BobToken 850 bodyBytes := bytes.NewReader([]byte(r.body)) 851 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 852 if err != nil { 853 t.Fatalf("unexpected error: %v", err) 854 } 855 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 856 857 func() { 858 resp, err := transport.RoundTrip(req) 859 if err != nil { 860 t.Logf("case %v", r) 861 t.Fatalf("unexpected error: %v", err) 862 } 863 defer resp.Body.Close() 864 // Expect all of bob's actions to return Forbidden 865 if resp.StatusCode != http.StatusForbidden { 866 t.Logf("case %v", r) 867 t.Errorf("Expected status Forbidden, but got %s", resp.Status) 868 } 869 }() 870 } 871 872 // bob can impersonate alice to do other things 873 for _, r := range getTestRequests(ns.Name) { 874 token := BobToken 875 bodyBytes := bytes.NewReader([]byte(r.body)) 876 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 877 if err != nil { 878 t.Fatalf("unexpected error: %v", err) 879 } 880 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 881 req.Header.Set("Impersonate-User", "alice") 882 func() { 883 resp, err := transport.RoundTrip(req) 884 if err != nil { 885 t.Logf("case %v", r) 886 t.Fatalf("unexpected error: %v", err) 887 } 888 defer resp.Body.Close() 889 // Expect all the requests to be allowed, don't care what they actually do 890 if resp.StatusCode == http.StatusForbidden { 891 t.Logf("case %v", r) 892 t.Errorf("Expected status not %v, but got %v", http.StatusForbidden, resp.StatusCode) 893 } 894 }() 895 } 896 897 // alice can't impersonate bob 898 for _, r := range getTestRequests(ns.Name) { 899 token := AliceToken 900 bodyBytes := bytes.NewReader([]byte(r.body)) 901 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 902 if err != nil { 903 t.Fatalf("unexpected error: %v", err) 904 } 905 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 906 req.Header.Set("Impersonate-User", "bob") 907 908 func() { 909 resp, err := transport.RoundTrip(req) 910 if err != nil { 911 t.Logf("case %v", r) 912 t.Fatalf("unexpected error: %v", err) 913 } 914 defer resp.Body.Close() 915 // Expect all of bob's actions to return Forbidden 916 if resp.StatusCode != http.StatusForbidden { 917 t.Logf("case %v", r) 918 t.Errorf("Expected not status Forbidden, but got %s", resp.Status) 919 } 920 }() 921 } 922 923 // bob can impersonate a service account 924 for _, r := range getTestRequests(ns.Name) { 925 token := BobToken 926 bodyBytes := bytes.NewReader([]byte(r.body)) 927 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 928 if err != nil { 929 t.Fatalf("unexpected error: %v", err) 930 } 931 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 932 req.Header.Set("Impersonate-User", serviceaccount.MakeUsername("default", "default")) 933 func() { 934 resp, err := transport.RoundTrip(req) 935 if err != nil { 936 t.Logf("case %v", r) 937 t.Fatalf("unexpected error: %v", err) 938 } 939 defer resp.Body.Close() 940 // Expect all the requests to be allowed, don't care what they actually do 941 if resp.StatusCode == http.StatusForbidden { 942 t.Logf("case %v", r) 943 t.Errorf("Expected status not %v, but got %v", http.StatusForbidden, resp.StatusCode) 944 } 945 }() 946 } 947 948 } 949 950 func TestImpersonateWithUID(t *testing.T) { 951 server := kubeapiservertesting.StartTestServerOrDie( 952 t, 953 nil, 954 []string{ 955 "--authorization-mode=RBAC", 956 "--anonymous-auth", 957 }, 958 framework.SharedEtcd(), 959 ) 960 t.Cleanup(server.TearDownFn) 961 962 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) 963 t.Cleanup(cancel) 964 965 t.Run("impersonation with uid header", func(t *testing.T) { 966 adminClient := clientset.NewForConfigOrDie(server.ClientConfig) 967 968 authutil.GrantUserAuthorization(t, ctx, adminClient, "alice", 969 rbacv1.PolicyRule{ 970 Verbs: []string{"create"}, 971 APIGroups: []string{"certificates.k8s.io"}, 972 Resources: []string{"certificatesigningrequests"}, 973 }, 974 ) 975 976 req := csrPEM(t) 977 978 clientConfig := rest.CopyConfig(server.ClientConfig) 979 clientConfig.Impersonate = rest.ImpersonationConfig{ 980 UserName: "alice", 981 UID: "1234", 982 } 983 984 client := clientset.NewForConfigOrDie(clientConfig) 985 createdCsr, err := client.CertificatesV1().CertificateSigningRequests().Create( 986 ctx, 987 &certificatesv1.CertificateSigningRequest{ 988 Spec: certificatesv1.CertificateSigningRequestSpec{ 989 SignerName: "kubernetes.io/kube-apiserver-client", 990 Request: req, 991 Usages: []certificatesv1.KeyUsage{"client auth"}, 992 }, 993 ObjectMeta: metav1.ObjectMeta{ 994 Name: "impersonated-csr", 995 }, 996 }, 997 metav1.CreateOptions{}, 998 ) 999 if err != nil { 1000 t.Fatalf("Unexpected error creating Certificate Signing Request: %v", err) 1001 } 1002 1003 // require that all the original fields and the impersonated user's info 1004 // is in the returned spec. 1005 expectedCsrSpec := certificatesv1.CertificateSigningRequestSpec{ 1006 Groups: []string{"system:authenticated"}, 1007 SignerName: "kubernetes.io/kube-apiserver-client", 1008 Request: req, 1009 Usages: []certificatesv1.KeyUsage{"client auth"}, 1010 Username: "alice", 1011 UID: "1234", 1012 } 1013 actualCsrSpec := createdCsr.Spec 1014 1015 if diff := cmp.Diff(expectedCsrSpec, actualCsrSpec); diff != "" { 1016 t.Fatalf("CSR spec was different than expected, -got, +want:\n %s", diff) 1017 } 1018 }) 1019 1020 t.Run("impersonation with only UID fails", func(t *testing.T) { 1021 clientConfig := rest.CopyConfig(server.ClientConfig) 1022 clientConfig.Impersonate = rest.ImpersonationConfig{ 1023 UID: "1234", 1024 } 1025 1026 client := clientset.NewForConfigOrDie(clientConfig) 1027 _, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 1028 1029 if !errors.IsInternalError(err) { 1030 t.Fatalf("expected internal error, got %T %v", err, err) 1031 } 1032 if diff := cmp.Diff( 1033 `an error on the server ("Internal Server Error: \"/api/v1/nodes\": `+ 1034 `requested [{UID 1234 authentication.k8s.io/v1 }] without impersonating a user") `+ 1035 `has prevented the request from succeeding (get nodes)`, 1036 err.Error(), 1037 ); diff != "" { 1038 t.Fatalf("internal error different than expected, -got, +want:\n %s", diff) 1039 } 1040 }) 1041 1042 t.Run("impersonating UID without authorization fails", func(t *testing.T) { 1043 adminClient := clientset.NewForConfigOrDie(server.ClientConfig) 1044 1045 authutil.GrantUserAuthorization(t, ctx, adminClient, "system:anonymous", 1046 rbacv1.PolicyRule{ 1047 Verbs: []string{"impersonate"}, 1048 APIGroups: []string{""}, 1049 Resources: []string{"users"}, 1050 ResourceNames: []string{"some-user-anonymous-can-impersonate"}, 1051 }, 1052 ) 1053 1054 clientConfig := rest.AnonymousClientConfig(server.ClientConfig) 1055 clientConfig.Impersonate = rest.ImpersonationConfig{ 1056 UserName: "some-user-anonymous-can-impersonate", 1057 UID: "1234", 1058 } 1059 1060 client := clientset.NewForConfigOrDie(clientConfig) 1061 _, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 1062 1063 if !errors.IsForbidden(err) { 1064 t.Fatalf("expected forbidden error, got %T %v", err, err) 1065 } 1066 if diff := cmp.Diff( 1067 `uids.authentication.k8s.io "1234" is forbidden: `+ 1068 `User "system:anonymous" cannot impersonate resource "uids" in API group "authentication.k8s.io" at the cluster scope`, 1069 err.Error(), 1070 ); diff != "" { 1071 t.Fatalf("forbidden error different than expected, -got, +want:\n %s", diff) 1072 } 1073 }) 1074 } 1075 1076 func csrPEM(t *testing.T) []byte { 1077 t.Helper() 1078 1079 _, privateKey, err := ed25519.GenerateKey(rand.Reader) 1080 if err != nil { 1081 t.Fatalf("Unexpected error generating ed25519 key: %v", err) 1082 } 1083 1084 csrDER, err := x509.CreateCertificateRequest( 1085 rand.Reader, 1086 &x509.CertificateRequest{ 1087 Subject: pkix.Name{ 1088 Organization: []string{}, 1089 }, 1090 }, 1091 privateKey) 1092 if err != nil { 1093 t.Fatalf("Unexpected error creating x509 certificate request: %v", err) 1094 } 1095 1096 csrPemBlock := &pem.Block{ 1097 Type: "CERTIFICATE REQUEST", 1098 Bytes: csrDER, 1099 } 1100 1101 req := pem.EncodeToMemory(csrPemBlock) 1102 if req == nil { 1103 t.Fatalf("Failed to encode PEM to memory.") 1104 } 1105 return req 1106 } 1107 1108 func newABACFileWithContents(t *testing.T, contents string) string { 1109 dir := t.TempDir() 1110 file := filepath.Join(dir, "auth_test") 1111 if err := os.WriteFile(file, []byte(contents), 0700); err != nil { 1112 t.Fatalf("unexpected error writing policyfile: %v", err) 1113 } 1114 return file 1115 } 1116 1117 type trackingAuthorizer struct { 1118 requestAttributes []authorizer.Attributes 1119 } 1120 1121 func (a *trackingAuthorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) { 1122 a.requestAttributes = append(a.requestAttributes, attributes) 1123 return authorizer.DecisionAllow, "", nil 1124 } 1125 1126 // TestAuthorizationAttributeDetermination tests that authorization attributes are built correctly 1127 func TestAuthorizationAttributeDetermination(t *testing.T) { 1128 _, ctx := ktesting.NewTestContext(t) 1129 ctx, cancel := context.WithCancel(ctx) 1130 defer cancel() 1131 1132 trackingAuthorizer := &trackingAuthorizer{} 1133 1134 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 1135 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1136 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1137 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1138 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 1139 }, 1140 ModifyServerConfig: func(config *controlplane.Config) { 1141 config.GenericConfig.Authorization.Authorizer = unionauthz.New(config.GenericConfig.Authorization.Authorizer, trackingAuthorizer) 1142 }, 1143 }) 1144 defer tearDownFn() 1145 1146 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-attribute-determination", t) 1147 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1148 1149 transport, err := rest.TransportFor(kubeConfig) 1150 if err != nil { 1151 t.Fatal(err) 1152 } 1153 1154 requests := map[string]struct { 1155 verb string 1156 URL string 1157 expectedAttributes authorizer.Attributes 1158 }{ 1159 "prefix/version/resource": {"GET", "/api/v1/pods", authorizer.AttributesRecord{APIGroup: api.GroupName, Resource: "pods"}}, 1160 "prefix/group/version/resource": {"GET", "/apis/extensions/v1/pods", authorizer.AttributesRecord{APIGroup: extensions.GroupName, Resource: "pods"}}, 1161 "prefix/group/version/resource2": {"GET", "/apis/autoscaling/v1/horizontalpodautoscalers", authorizer.AttributesRecord{APIGroup: autoscaling.GroupName, Resource: "horizontalpodautoscalers"}}, 1162 } 1163 1164 currentAuthorizationAttributesIndex := 0 1165 1166 for testName, r := range requests { 1167 token := BobToken 1168 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, nil) 1169 if err != nil { 1170 t.Logf("case %v", testName) 1171 t.Fatalf("unexpected error: %v", err) 1172 } 1173 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1174 func() { 1175 resp, err := transport.RoundTrip(req) 1176 if err != nil { 1177 t.Logf("case %v", r) 1178 t.Fatalf("unexpected error: %v", err) 1179 } 1180 defer resp.Body.Close() 1181 1182 found := false 1183 for i := currentAuthorizationAttributesIndex; i < len(trackingAuthorizer.requestAttributes); i++ { 1184 if trackingAuthorizer.requestAttributes[i].GetAPIGroup() == r.expectedAttributes.GetAPIGroup() && 1185 trackingAuthorizer.requestAttributes[i].GetResource() == r.expectedAttributes.GetResource() { 1186 found = true 1187 break 1188 } 1189 1190 t.Logf("%#v did not match %#v", r.expectedAttributes, trackingAuthorizer.requestAttributes[i].(*authorizer.AttributesRecord)) 1191 } 1192 if !found { 1193 t.Errorf("did not find %#v in %#v", r.expectedAttributes, trackingAuthorizer.requestAttributes[currentAuthorizationAttributesIndex:]) 1194 } 1195 1196 currentAuthorizationAttributesIndex = len(trackingAuthorizer.requestAttributes) 1197 }() 1198 } 1199 } 1200 1201 // TestNamespaceAuthorization tests that authorization can be controlled 1202 // by namespace. 1203 func TestNamespaceAuthorization(t *testing.T) { 1204 _, ctx := ktesting.NewTestContext(t) 1205 ctx, cancel := context.WithCancel(ctx) 1206 defer cancel() 1207 1208 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 1209 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1210 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1211 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1212 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 1213 opts.Authorization.PolicyFile = newABACFileWithContents(t, `{"namespace": "auth-namespace"}`) 1214 opts.Authorization.Modes = []string{"ABAC"} 1215 }, 1216 }) 1217 defer tearDownFn() 1218 1219 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-namespace", t) 1220 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1221 1222 previousResourceVersion := make(map[string]float64) 1223 transport, err := rest.TransportFor(kubeConfig) 1224 if err != nil { 1225 t.Fatal(err) 1226 } 1227 1228 requests := []struct { 1229 verb string 1230 URL string 1231 namespace string 1232 body string 1233 statusCodes map[int]bool // allowed status codes. 1234 }{ 1235 1236 {"POST", timeoutPath("pods", ns.Name, ""), "foo", aPod, integration.Code201}, 1237 {"GET", path("pods", ns.Name, ""), "foo", "", integration.Code200}, 1238 {"GET", path("pods", ns.Name, "a"), "foo", "", integration.Code200}, 1239 {"DELETE", timeoutPath("pods", ns.Name, "a"), "foo", "", integration.Code200}, 1240 1241 {"POST", timeoutPath("pods", "foo", ""), "bar", aPod, integration.Code403}, 1242 {"GET", path("pods", "foo", ""), "bar", "", integration.Code403}, 1243 {"GET", path("pods", "foo", "a"), "bar", "", integration.Code403}, 1244 {"DELETE", timeoutPath("pods", "foo", "a"), "bar", "", integration.Code403}, 1245 1246 {"POST", timeoutPath("pods", metav1.NamespaceDefault, ""), "", aPod, integration.Code403}, 1247 {"GET", path("pods", "", ""), "", "", integration.Code403}, 1248 {"GET", path("pods", metav1.NamespaceDefault, "a"), "", "", integration.Code403}, 1249 {"DELETE", timeoutPath("pods", metav1.NamespaceDefault, "a"), "", "", integration.Code403}, 1250 } 1251 1252 for _, r := range requests { 1253 token := BobToken 1254 var bodyStr string 1255 if r.body != "" { 1256 sub := "" 1257 if r.verb == "PUT" && r.body != "" { 1258 // For update operations, insert previous resource version 1259 if resVersion := previousResourceVersion[getPreviousResourceVersionKey(r.URL, "")]; resVersion != 0 { 1260 sub += fmt.Sprintf(",\r\n\"resourceVersion\": \"%v\"", resVersion) 1261 } 1262 namespace := r.namespace 1263 // FIXME: Is that correct? 1264 if len(namespace) == 0 { 1265 namespace = "default" 1266 } 1267 sub += fmt.Sprintf(",\r\n\"namespace\": %q", namespace) 1268 } 1269 bodyStr = fmt.Sprintf(r.body, sub) 1270 } 1271 r.body = bodyStr 1272 bodyBytes := bytes.NewReader([]byte(bodyStr)) 1273 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1274 if err != nil { 1275 t.Logf("case %v", r) 1276 t.Fatalf("unexpected error: %v", err) 1277 } 1278 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1279 func() { 1280 resp, err := transport.RoundTrip(req) 1281 if err != nil { 1282 t.Logf("case %v", r) 1283 t.Fatalf("unexpected error: %v", err) 1284 } 1285 defer resp.Body.Close() 1286 b, _ := io.ReadAll(resp.Body) 1287 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 1288 t.Logf("case %v", r) 1289 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 1290 t.Errorf("Body: %v", string(b)) 1291 } else { 1292 if r.verb == "POST" { 1293 // For successful create operations, extract resourceVersion 1294 id, currentResourceVersion, err := parseResourceVersion(b) 1295 if err == nil { 1296 key := getPreviousResourceVersionKey(r.URL, id) 1297 previousResourceVersion[key] = currentResourceVersion 1298 } 1299 } 1300 } 1301 1302 }() 1303 } 1304 } 1305 1306 // TestKindAuthorization tests that authorization can be controlled 1307 // by namespace. 1308 func TestKindAuthorization(t *testing.T) { 1309 _, ctx := ktesting.NewTestContext(t) 1310 ctx, cancel := context.WithCancel(ctx) 1311 defer cancel() 1312 1313 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 1314 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1315 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1316 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1317 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 1318 opts.Authorization.PolicyFile = newABACFileWithContents(t, `{"resource": "services"}`) 1319 opts.Authorization.Modes = []string{"ABAC"} 1320 }, 1321 }) 1322 defer tearDownFn() 1323 1324 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-kind", t) 1325 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1326 1327 previousResourceVersion := make(map[string]float64) 1328 transport, err := rest.TransportFor(kubeConfig) 1329 if err != nil { 1330 t.Fatal(err) 1331 } 1332 1333 requests := []testRequest{ 1334 {"POST", timeoutPath("services", ns.Name, ""), aService, integration.Code201}, 1335 {"GET", path("services", ns.Name, ""), "", integration.Code200}, 1336 {"GET", path("services", ns.Name, "a"), "", integration.Code200}, 1337 {"DELETE", timeoutPath("services", ns.Name, "a"), "", integration.Code200}, 1338 1339 {"POST", timeoutPath("pods", ns.Name, ""), aPod, integration.Code403}, 1340 {"GET", path("pods", "", ""), "", integration.Code403}, 1341 {"GET", path("pods", ns.Name, "a"), "", integration.Code403}, 1342 {"DELETE", timeoutPath("pods", ns.Name, "a"), "", integration.Code403}, 1343 } 1344 1345 for _, r := range requests { 1346 token := BobToken 1347 var bodyStr string 1348 if r.body != "" { 1349 bodyStr = fmt.Sprintf(r.body, "") 1350 if r.verb == "PUT" && r.body != "" { 1351 // For update operations, insert previous resource version 1352 if resVersion := previousResourceVersion[getPreviousResourceVersionKey(r.URL, "")]; resVersion != 0 { 1353 resourceVersionJSON := fmt.Sprintf(",\r\n\"resourceVersion\": \"%v\"", resVersion) 1354 bodyStr = fmt.Sprintf(r.body, resourceVersionJSON) 1355 } 1356 } 1357 } 1358 r.body = bodyStr 1359 bodyBytes := bytes.NewReader([]byte(bodyStr)) 1360 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1361 if err != nil { 1362 t.Logf("case %v", r) 1363 t.Fatalf("unexpected error: %v", err) 1364 } 1365 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1366 func() { 1367 resp, err := transport.RoundTrip(req) 1368 if err != nil { 1369 t.Logf("case %v", r) 1370 t.Fatalf("unexpected error: %v", err) 1371 } 1372 defer resp.Body.Close() 1373 b, _ := io.ReadAll(resp.Body) 1374 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 1375 t.Logf("case %v", r) 1376 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 1377 t.Errorf("Body: %v", string(b)) 1378 } else { 1379 if r.verb == "POST" { 1380 // For successful create operations, extract resourceVersion 1381 id, currentResourceVersion, err := parseResourceVersion(b) 1382 if err == nil { 1383 key := getPreviousResourceVersionKey(r.URL, id) 1384 previousResourceVersion[key] = currentResourceVersion 1385 } 1386 } 1387 } 1388 1389 }() 1390 } 1391 } 1392 1393 // TestReadOnlyAuthorization tests that authorization can be controlled 1394 // by namespace. 1395 func TestReadOnlyAuthorization(t *testing.T) { 1396 _, ctx := ktesting.NewTestContext(t) 1397 ctx, cancel := context.WithCancel(ctx) 1398 defer cancel() 1399 1400 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 1401 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1402 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1403 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1404 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 1405 opts.Authorization.PolicyFile = newABACFileWithContents(t, `{"readonly": true}`) 1406 opts.Authorization.Modes = []string{"ABAC"} 1407 }, 1408 }) 1409 defer tearDownFn() 1410 1411 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-read-only", t) 1412 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1413 1414 transport, err := rest.TransportFor(kubeConfig) 1415 if err != nil { 1416 t.Fatal(err) 1417 } 1418 1419 requests := []testRequest{ 1420 {"POST", path("pods", ns.Name, ""), aPod, integration.Code403}, 1421 {"GET", path("pods", ns.Name, ""), "", integration.Code200}, 1422 {"GET", path("pods", metav1.NamespaceDefault, "a"), "", integration.Code404}, 1423 } 1424 1425 for _, r := range requests { 1426 token := BobToken 1427 bodyBytes := bytes.NewReader([]byte(r.body)) 1428 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1429 if err != nil { 1430 t.Fatalf("unexpected error: %v", err) 1431 } 1432 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1433 func() { 1434 resp, err := transport.RoundTrip(req) 1435 if err != nil { 1436 t.Logf("case %v", r) 1437 t.Fatalf("unexpected error: %v", err) 1438 } 1439 defer resp.Body.Close() 1440 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 1441 t.Logf("case %v", r) 1442 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 1443 b, _ := io.ReadAll(resp.Body) 1444 t.Errorf("Body: %v", string(b)) 1445 } 1446 }() 1447 } 1448 } 1449 1450 // TestWebhookTokenAuthenticator tests that a control plane can use the webhook token 1451 // authenticator to call out to a remote web server for authentication 1452 // decisions. 1453 func TestWebhookTokenAuthenticator(t *testing.T) { 1454 testWebhookTokenAuthenticator(false, t) 1455 } 1456 1457 // TestWebhookTokenAuthenticatorCustomDial is the same as TestWebhookTokenAuthenticator, but uses a 1458 // custom dialer 1459 func TestWebhookTokenAuthenticatorCustomDial(t *testing.T) { 1460 testWebhookTokenAuthenticator(true, t) 1461 } 1462 1463 func testWebhookTokenAuthenticator(customDialer bool, t *testing.T) { 1464 _, ctx := ktesting.NewTestContext(t) 1465 ctx, cancel := context.WithCancel(ctx) 1466 defer cancel() 1467 1468 authServer := newTestWebhookTokenAuthServer() 1469 defer authServer.Close() 1470 var authenticator authenticator.Request 1471 var err error 1472 1473 if customDialer == false { 1474 authenticator, err = getTestWebhookTokenAuth(authServer.URL, nil) 1475 } else { 1476 authenticator, err = getTestWebhookTokenAuthCustomDialer(authServer.URL) 1477 } 1478 1479 if err != nil { 1480 t.Fatalf("error starting webhook token authenticator server: %v", err) 1481 } 1482 1483 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 1484 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1485 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1486 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1487 opts.Authorization.Modes = []string{"ABAC"} 1488 opts.Authorization.PolicyFile = "testdata/allowalice.jsonl" 1489 }, 1490 ModifyServerConfig: func(config *controlplane.Config) { 1491 config.GenericConfig.Authentication.Authenticator = group.NewAuthenticatedGroupAdder(authenticator) 1492 // Disable checking API audiences that is set by testserver by default. 1493 config.GenericConfig.Authentication.APIAudiences = nil 1494 }, 1495 }) 1496 defer tearDownFn() 1497 1498 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-webhook-token", t) 1499 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1500 1501 transport, err := rest.TransportFor(kubeConfig) 1502 if err != nil { 1503 t.Fatal(err) 1504 } 1505 1506 for _, r := range getTestRequests(ns.Name) { 1507 // Expect Bob's requests to all fail. 1508 token := BobToken 1509 bodyBytes := bytes.NewReader([]byte(r.body)) 1510 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1511 if err != nil { 1512 t.Fatalf("unexpected error: %v", err) 1513 } 1514 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1515 1516 func() { 1517 resp, err := transport.RoundTrip(req) 1518 if err != nil { 1519 t.Logf("case %v", r) 1520 t.Fatalf("unexpected error: %v", err) 1521 } 1522 defer resp.Body.Close() 1523 // Expect all of Bob's actions to return Forbidden 1524 if resp.StatusCode != http.StatusForbidden { 1525 t.Logf("case %v", r) 1526 t.Errorf("Expected http.Forbidden, but got %s", resp.Status) 1527 } 1528 }() 1529 // Expect Alice's requests to succeed. 1530 token = AliceToken 1531 bodyBytes = bytes.NewReader([]byte(r.body)) 1532 req, err = http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1533 if err != nil { 1534 t.Fatalf("unexpected error: %v", err) 1535 } 1536 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1537 1538 func() { 1539 resp, err := transport.RoundTrip(req) 1540 if err != nil { 1541 t.Logf("case %v", r) 1542 t.Fatalf("unexpected error: %v", err) 1543 } 1544 defer resp.Body.Close() 1545 // Expect all of Alice's actions to at least get past authn/authz. 1546 if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { 1547 t.Logf("case %v", r) 1548 t.Errorf("Expected something other than Unauthorized/Forbidden, but got %s", resp.Status) 1549 } 1550 }() 1551 } 1552 } 1553 1554 // newTestWebhookTokenAuthServer creates an http token authentication server 1555 // that knows about both Alice and Bob. 1556 func newTestWebhookTokenAuthServer() *httptest.Server { 1557 serveHTTP := func(w http.ResponseWriter, r *http.Request) { 1558 var review authenticationv1beta1.TokenReview 1559 if err := json.NewDecoder(r.Body).Decode(&review); err != nil { 1560 http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest) 1561 return 1562 } 1563 type userInfo struct { 1564 Username string `json:"username"` 1565 UID string `json:"uid"` 1566 Groups []string `json:"groups"` 1567 } 1568 type status struct { 1569 Authenticated bool `json:"authenticated"` 1570 User userInfo `json:"user"` 1571 } 1572 var username, uid string 1573 authenticated := false 1574 if review.Spec.Token == AliceToken { 1575 authenticated, username, uid = true, "alice", "1" 1576 } else if review.Spec.Token == BobToken { 1577 authenticated, username, uid = true, "bob", "2" 1578 } 1579 1580 resp := struct { 1581 APIVersion string `json:"apiVersion"` 1582 Status status `json:"status"` 1583 }{ 1584 APIVersion: authenticationv1beta1.SchemeGroupVersion.String(), 1585 Status: status{ 1586 authenticated, 1587 userInfo{ 1588 Username: username, 1589 UID: uid, 1590 }, 1591 }, 1592 } 1593 w.Header().Set("Content-Type", "application/json") 1594 json.NewEncoder(w).Encode(resp) 1595 } 1596 1597 server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP)) 1598 server.Start() 1599 return server 1600 }