k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/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 tCtx := ktesting.Init(t) 458 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 459 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 460 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 461 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 462 opts.Authorization.Modes = []string{"AlwaysAllow"} 463 }, 464 }) 465 defer tearDownFn() 466 467 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-always-allow", t) 468 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 469 470 transport, err := rest.TransportFor(kubeConfig) 471 if err != nil { 472 t.Fatal(err) 473 } 474 previousResourceVersion := make(map[string]float64) 475 476 for _, r := range getTestRequests(ns.Name) { 477 var bodyStr string 478 if r.body != "" { 479 sub := "" 480 if r.verb == "PUT" { 481 // For update operations, insert previous resource version 482 if resVersion := previousResourceVersion[getPreviousResourceVersionKey(r.URL, "")]; resVersion != 0 { 483 sub += fmt.Sprintf(",\r\n\"resourceVersion\": \"%v\"", resVersion) 484 } 485 sub += fmt.Sprintf(",\r\n\"namespace\": %q", ns.Name) 486 } 487 bodyStr = fmt.Sprintf(r.body, sub) 488 } 489 r.body = bodyStr 490 bodyBytes := bytes.NewReader([]byte(bodyStr)) 491 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 492 if err != nil { 493 t.Logf("case %v", r) 494 t.Fatalf("unexpected error: %v", err) 495 } 496 if r.verb == "PATCH" { 497 req.Header.Set("Content-Type", "application/merge-patch+json") 498 } 499 func() { 500 resp, err := transport.RoundTrip(req) 501 if err != nil { 502 t.Logf("case %v", r) 503 t.Fatalf("unexpected error: %v", err) 504 } 505 defer resp.Body.Close() 506 b, _ := io.ReadAll(resp.Body) 507 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 508 t.Logf("case %v", r) 509 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 510 t.Errorf("Body: %v", string(b)) 511 } else { 512 if r.verb == "POST" { 513 // For successful create operations, extract resourceVersion 514 id, currentResourceVersion, err := parseResourceVersion(b) 515 if err == nil { 516 key := getPreviousResourceVersionKey(r.URL, id) 517 previousResourceVersion[key] = currentResourceVersion 518 } else { 519 t.Logf("error in trying to extract resource version: %s", err) 520 } 521 } 522 } 523 }() 524 } 525 } 526 527 func parseResourceVersion(response []byte) (string, float64, error) { 528 var resultBodyMap map[string]interface{} 529 err := json.Unmarshal(response, &resultBodyMap) 530 if err != nil { 531 return "", 0, fmt.Errorf("unexpected error unmarshaling resultBody: %v", err) 532 } 533 metadata, ok := resultBodyMap["metadata"].(map[string]interface{}) 534 if !ok { 535 return "", 0, fmt.Errorf("unexpected error, metadata not found in JSON response: %v", string(response)) 536 } 537 id, ok := metadata["name"].(string) 538 if !ok { 539 return "", 0, fmt.Errorf("unexpected error, id not found in JSON response: %v", string(response)) 540 } 541 resourceVersionString, ok := metadata["resourceVersion"].(string) 542 if !ok { 543 return "", 0, fmt.Errorf("unexpected error, resourceVersion not found in JSON response: %v", string(response)) 544 } 545 resourceVersion, err := strconv.ParseFloat(resourceVersionString, 64) 546 if err != nil { 547 return "", 0, fmt.Errorf("unexpected error, could not parse resourceVersion as float64, err: %s. JSON response: %v", err, string(response)) 548 } 549 return id, resourceVersion, nil 550 } 551 552 func getPreviousResourceVersionKey(url, id string) string { 553 baseURL := strings.Split(url, "?")[0] 554 key := baseURL 555 if id != "" { 556 key = fmt.Sprintf("%s/%v", baseURL, id) 557 } 558 return key 559 } 560 561 func TestAuthModeAlwaysDeny(t *testing.T) { 562 tCtx := ktesting.Init(t) 563 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 564 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 565 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 566 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 567 opts.Authorization.Modes = []string{"AlwaysDeny"} 568 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 569 }, 570 }) 571 defer tearDownFn() 572 573 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-always-deny", t) 574 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 575 576 transport, err := rest.TransportFor(kubeConfig) 577 if err != nil { 578 t.Fatal(err) 579 } 580 transport = resttransport.NewBearerAuthRoundTripper(AliceToken, transport) 581 582 for _, r := range getTestRequests(ns.Name) { 583 bodyBytes := bytes.NewReader([]byte(r.body)) 584 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 585 if err != nil { 586 t.Logf("case %v", r) 587 t.Fatalf("unexpected error: %v", err) 588 } 589 func() { 590 resp, err := transport.RoundTrip(req) 591 if err != nil { 592 t.Logf("case %v", r) 593 t.Fatalf("unexpected error: %v", err) 594 } 595 defer resp.Body.Close() 596 if resp.StatusCode != http.StatusForbidden { 597 t.Logf("case %v", r) 598 t.Errorf("Expected status Forbidden but got status %v", resp.Status) 599 } 600 }() 601 } 602 } 603 604 // TestAliceNotForbiddenOrUnauthorized tests a user who is known to 605 // the authentication system and authorized to do any actions. 606 func TestAliceNotForbiddenOrUnauthorized(t *testing.T) { 607 tCtx := ktesting.Init(t) 608 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 609 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 610 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 611 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 612 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 613 opts.Authorization.Modes = []string{"ABAC"} 614 opts.Authorization.PolicyFile = "testdata/allowalice.jsonl" 615 }, 616 }) 617 defer tearDownFn() 618 619 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-alice-not-forbidden", t) 620 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 621 622 previousResourceVersion := make(map[string]float64) 623 transport, err := rest.TransportFor(kubeConfig) 624 if err != nil { 625 t.Fatal(err) 626 } 627 628 for _, r := range getTestRequests(ns.Name) { 629 token := AliceToken 630 var bodyStr string 631 if r.body != "" { 632 sub := "" 633 if r.verb == "PUT" { 634 // For update operations, insert previous resource version 635 if resVersion := previousResourceVersion[getPreviousResourceVersionKey(r.URL, "")]; resVersion != 0 { 636 sub += fmt.Sprintf(",\r\n\"resourceVersion\": \"%v\"", resVersion) 637 } 638 sub += fmt.Sprintf(",\r\n\"namespace\": %q", ns.Name) 639 } 640 bodyStr = fmt.Sprintf(r.body, sub) 641 } 642 r.body = bodyStr 643 bodyBytes := bytes.NewReader([]byte(bodyStr)) 644 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 645 if err != nil { 646 t.Fatalf("unexpected error: %v", err) 647 } 648 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 649 if r.verb == "PATCH" { 650 req.Header.Set("Content-Type", "application/merge-patch+json") 651 } 652 653 func() { 654 resp, err := transport.RoundTrip(req) 655 if err != nil { 656 t.Logf("case %v", r) 657 t.Fatalf("unexpected error: %v", err) 658 } 659 defer resp.Body.Close() 660 b, _ := io.ReadAll(resp.Body) 661 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 662 t.Logf("case %v", r) 663 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 664 t.Errorf("Body: %v", string(b)) 665 } else { 666 if r.verb == "POST" { 667 // For successful create operations, extract resourceVersion 668 id, currentResourceVersion, err := parseResourceVersion(b) 669 if err == nil { 670 key := getPreviousResourceVersionKey(r.URL, id) 671 previousResourceVersion[key] = currentResourceVersion 672 } 673 } 674 } 675 676 }() 677 } 678 } 679 680 // TestBobIsForbidden tests that a user who is known to 681 // the authentication system but not authorized to do any actions 682 // should receive "Forbidden". 683 func TestBobIsForbidden(t *testing.T) { 684 tCtx := ktesting.Init(t) 685 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 686 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 687 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 688 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 689 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 690 opts.Authorization.Modes = []string{"ABAC"} 691 opts.Authorization.PolicyFile = "testdata/allowalice.jsonl" 692 }, 693 }) 694 defer tearDownFn() 695 696 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-bob-forbidden", t) 697 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 698 699 transport, err := rest.TransportFor(kubeConfig) 700 if err != nil { 701 t.Fatal(err) 702 } 703 704 for _, r := range getTestRequests(ns.Name) { 705 token := BobToken 706 bodyBytes := bytes.NewReader([]byte(r.body)) 707 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 708 if err != nil { 709 t.Fatalf("unexpected error: %v", err) 710 } 711 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 712 713 func() { 714 resp, err := transport.RoundTrip(req) 715 if err != nil { 716 t.Logf("case %v", r) 717 t.Fatalf("unexpected error: %v", err) 718 } 719 defer resp.Body.Close() 720 // Expect all of bob's actions to return Forbidden 721 if resp.StatusCode != http.StatusForbidden { 722 t.Logf("case %v", r) 723 t.Errorf("Expected not status Forbidden, but got %s", resp.Status) 724 } 725 }() 726 } 727 } 728 729 // TestUnknownUserIsUnauthorized tests that a user who is unknown 730 // to the authentication system get status code "Unauthorized". 731 // An authorization module is installed in this scenario for integration 732 // test purposes, but requests aren't expected to reach it. 733 func TestUnknownUserIsUnauthorized(t *testing.T) { 734 tCtx := ktesting.Init(t) 735 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 736 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 737 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 738 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 739 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 740 opts.Authorization.Modes = []string{"ABAC"} 741 opts.Authorization.PolicyFile = "testdata/allowalice.jsonl" 742 }, 743 }) 744 defer tearDownFn() 745 746 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-unknown-unauthorized", t) 747 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 748 749 transport, err := rest.TransportFor(kubeConfig) 750 if err != nil { 751 t.Fatal(err) 752 } 753 754 for _, r := range getTestRequests(ns.Name) { 755 token := UnknownToken 756 bodyBytes := bytes.NewReader([]byte(r.body)) 757 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 758 if err != nil { 759 t.Fatalf("unexpected error: %v", err) 760 } 761 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 762 func() { 763 resp, err := transport.RoundTrip(req) 764 if err != nil { 765 t.Logf("case %v", r) 766 t.Fatalf("unexpected error: %v", err) 767 } 768 defer resp.Body.Close() 769 // Expect all of unauthenticated user's request to be "Unauthorized" 770 if resp.StatusCode != http.StatusUnauthorized { 771 t.Logf("case %v", r) 772 t.Errorf("Expected status %v, but got %v", http.StatusUnauthorized, resp.StatusCode) 773 b, _ := io.ReadAll(resp.Body) 774 t.Errorf("Body: %v", string(b)) 775 } 776 }() 777 } 778 } 779 780 type impersonateAuthorizer struct{} 781 782 // alice can't act as anyone and bob can't do anything but act-as someone 783 func (impersonateAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { 784 // alice can impersonate service accounts and do other actions 785 if a.GetUser() != nil && a.GetUser().GetName() == "alice" && a.GetVerb() == "impersonate" && a.GetResource() == "serviceaccounts" { 786 return authorizer.DecisionAllow, "", nil 787 } 788 if a.GetUser() != nil && a.GetUser().GetName() == "alice" && a.GetVerb() != "impersonate" { 789 return authorizer.DecisionAllow, "", nil 790 } 791 // bob can impersonate anyone, but that's it 792 if a.GetUser() != nil && a.GetUser().GetName() == "bob" && a.GetVerb() == "impersonate" { 793 return authorizer.DecisionAllow, "", nil 794 } 795 if a.GetUser() != nil && a.GetUser().GetName() == "bob" && a.GetVerb() != "impersonate" { 796 return authorizer.DecisionDeny, "", nil 797 } 798 // service accounts can do everything 799 if a.GetUser() != nil && strings.HasPrefix(a.GetUser().GetName(), serviceaccount.ServiceAccountUsernamePrefix) { 800 return authorizer.DecisionAllow, "", nil 801 } 802 803 return authorizer.DecisionNoOpinion, "I can't allow that. Go ask alice.", nil 804 } 805 806 func TestImpersonateIsForbidden(t *testing.T) { 807 tCtx := ktesting.Init(t) 808 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 809 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 810 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 811 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 812 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 813 }, 814 ModifyServerConfig: func(config *controlplane.Config) { 815 // Prepend an impersonation authorizer with specific opinions about alice and bob 816 config.ControlPlane.Generic.Authorization.Authorizer = unionauthz.New(impersonateAuthorizer{}, config.ControlPlane.Generic.Authorization.Authorizer) 817 }, 818 }) 819 defer tearDownFn() 820 821 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-impersonate-forbidden", t) 822 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 823 824 transport, err := rest.TransportFor(kubeConfig) 825 if err != nil { 826 t.Fatal(err) 827 } 828 829 // bob can't perform actions himself 830 for _, r := range getTestRequests(ns.Name) { 831 token := BobToken 832 bodyBytes := bytes.NewReader([]byte(r.body)) 833 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 834 if err != nil { 835 t.Fatalf("unexpected error: %v", err) 836 } 837 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 838 839 func() { 840 resp, err := transport.RoundTrip(req) 841 if err != nil { 842 t.Logf("case %v", r) 843 t.Fatalf("unexpected error: %v", err) 844 } 845 defer resp.Body.Close() 846 // Expect all of bob's actions to return Forbidden 847 if resp.StatusCode != http.StatusForbidden { 848 t.Logf("case %v", r) 849 t.Errorf("Expected status Forbidden, but got %s", resp.Status) 850 } 851 }() 852 } 853 854 // bob can impersonate alice to do other things 855 for _, r := range getTestRequests(ns.Name) { 856 token := BobToken 857 bodyBytes := bytes.NewReader([]byte(r.body)) 858 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 859 if err != nil { 860 t.Fatalf("unexpected error: %v", err) 861 } 862 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 863 req.Header.Set("Impersonate-User", "alice") 864 func() { 865 resp, err := transport.RoundTrip(req) 866 if err != nil { 867 t.Logf("case %v", r) 868 t.Fatalf("unexpected error: %v", err) 869 } 870 defer resp.Body.Close() 871 // Expect all the requests to be allowed, don't care what they actually do 872 if resp.StatusCode == http.StatusForbidden { 873 t.Logf("case %v", r) 874 t.Errorf("Expected status not %v, but got %v", http.StatusForbidden, resp.StatusCode) 875 } 876 }() 877 } 878 879 // alice can't impersonate bob 880 for _, r := range getTestRequests(ns.Name) { 881 token := AliceToken 882 bodyBytes := bytes.NewReader([]byte(r.body)) 883 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 884 if err != nil { 885 t.Fatalf("unexpected error: %v", err) 886 } 887 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 888 req.Header.Set("Impersonate-User", "bob") 889 890 func() { 891 resp, err := transport.RoundTrip(req) 892 if err != nil { 893 t.Logf("case %v", r) 894 t.Fatalf("unexpected error: %v", err) 895 } 896 defer resp.Body.Close() 897 // Expect all of bob's actions to return Forbidden 898 if resp.StatusCode != http.StatusForbidden { 899 t.Logf("case %v", r) 900 t.Errorf("Expected not status Forbidden, but got %s", resp.Status) 901 } 902 }() 903 } 904 905 // bob can impersonate a service account 906 for _, r := range getTestRequests(ns.Name) { 907 token := BobToken 908 bodyBytes := bytes.NewReader([]byte(r.body)) 909 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 910 if err != nil { 911 t.Fatalf("unexpected error: %v", err) 912 } 913 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 914 req.Header.Set("Impersonate-User", serviceaccount.MakeUsername("default", "default")) 915 func() { 916 resp, err := transport.RoundTrip(req) 917 if err != nil { 918 t.Logf("case %v", r) 919 t.Fatalf("unexpected error: %v", err) 920 } 921 defer resp.Body.Close() 922 // Expect all the requests to be allowed, don't care what they actually do 923 if resp.StatusCode == http.StatusForbidden { 924 t.Logf("case %v", r) 925 t.Errorf("Expected status not %v, but got %v", http.StatusForbidden, resp.StatusCode) 926 } 927 }() 928 } 929 930 } 931 932 func TestImpersonateWithUID(t *testing.T) { 933 server := kubeapiservertesting.StartTestServerOrDie( 934 t, 935 nil, 936 []string{ 937 "--authorization-mode=RBAC", 938 "--anonymous-auth", 939 }, 940 framework.SharedEtcd(), 941 ) 942 t.Cleanup(server.TearDownFn) 943 944 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) 945 t.Cleanup(cancel) 946 947 t.Run("impersonation with uid header", func(t *testing.T) { 948 adminClient := clientset.NewForConfigOrDie(server.ClientConfig) 949 950 authutil.GrantUserAuthorization(t, ctx, adminClient, "alice", 951 rbacv1.PolicyRule{ 952 Verbs: []string{"create"}, 953 APIGroups: []string{"certificates.k8s.io"}, 954 Resources: []string{"certificatesigningrequests"}, 955 }, 956 ) 957 958 req := csrPEM(t) 959 960 clientConfig := rest.CopyConfig(server.ClientConfig) 961 clientConfig.Impersonate = rest.ImpersonationConfig{ 962 UserName: "alice", 963 UID: "1234", 964 } 965 966 client := clientset.NewForConfigOrDie(clientConfig) 967 createdCsr, err := client.CertificatesV1().CertificateSigningRequests().Create( 968 ctx, 969 &certificatesv1.CertificateSigningRequest{ 970 Spec: certificatesv1.CertificateSigningRequestSpec{ 971 SignerName: "kubernetes.io/kube-apiserver-client", 972 Request: req, 973 Usages: []certificatesv1.KeyUsage{"client auth"}, 974 }, 975 ObjectMeta: metav1.ObjectMeta{ 976 Name: "impersonated-csr", 977 }, 978 }, 979 metav1.CreateOptions{}, 980 ) 981 if err != nil { 982 t.Fatalf("Unexpected error creating Certificate Signing Request: %v", err) 983 } 984 985 // require that all the original fields and the impersonated user's info 986 // is in the returned spec. 987 expectedCsrSpec := certificatesv1.CertificateSigningRequestSpec{ 988 Groups: []string{"system:authenticated"}, 989 SignerName: "kubernetes.io/kube-apiserver-client", 990 Request: req, 991 Usages: []certificatesv1.KeyUsage{"client auth"}, 992 Username: "alice", 993 UID: "1234", 994 } 995 actualCsrSpec := createdCsr.Spec 996 997 if diff := cmp.Diff(expectedCsrSpec, actualCsrSpec); diff != "" { 998 t.Fatalf("CSR spec was different than expected, -got, +want:\n %s", diff) 999 } 1000 }) 1001 1002 t.Run("impersonation with only UID fails", func(t *testing.T) { 1003 clientConfig := rest.CopyConfig(server.ClientConfig) 1004 clientConfig.Impersonate = rest.ImpersonationConfig{ 1005 UID: "1234", 1006 } 1007 1008 client := clientset.NewForConfigOrDie(clientConfig) 1009 _, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 1010 1011 if !errors.IsInternalError(err) { 1012 t.Fatalf("expected internal error, got %T %v", err, err) 1013 } 1014 if diff := cmp.Diff( 1015 `an error on the server ("Internal Server Error: \"/api/v1/nodes\": `+ 1016 `requested [{UID 1234 authentication.k8s.io/v1 }] without impersonating a user") `+ 1017 `has prevented the request from succeeding (get nodes)`, 1018 err.Error(), 1019 ); diff != "" { 1020 t.Fatalf("internal error different than expected, -got, +want:\n %s", diff) 1021 } 1022 }) 1023 1024 t.Run("impersonating UID without authorization fails", func(t *testing.T) { 1025 adminClient := clientset.NewForConfigOrDie(server.ClientConfig) 1026 1027 authutil.GrantUserAuthorization(t, ctx, adminClient, "system:anonymous", 1028 rbacv1.PolicyRule{ 1029 Verbs: []string{"impersonate"}, 1030 APIGroups: []string{""}, 1031 Resources: []string{"users"}, 1032 ResourceNames: []string{"some-user-anonymous-can-impersonate"}, 1033 }, 1034 ) 1035 1036 clientConfig := rest.AnonymousClientConfig(server.ClientConfig) 1037 clientConfig.Impersonate = rest.ImpersonationConfig{ 1038 UserName: "some-user-anonymous-can-impersonate", 1039 UID: "1234", 1040 } 1041 1042 client := clientset.NewForConfigOrDie(clientConfig) 1043 _, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 1044 1045 if !errors.IsForbidden(err) { 1046 t.Fatalf("expected forbidden error, got %T %v", err, err) 1047 } 1048 if diff := cmp.Diff( 1049 `uids.authentication.k8s.io "1234" is forbidden: `+ 1050 `User "system:anonymous" cannot impersonate resource "uids" in API group "authentication.k8s.io" at the cluster scope`, 1051 err.Error(), 1052 ); diff != "" { 1053 t.Fatalf("forbidden error different than expected, -got, +want:\n %s", diff) 1054 } 1055 }) 1056 } 1057 1058 func csrPEM(t *testing.T) []byte { 1059 t.Helper() 1060 1061 _, privateKey, err := ed25519.GenerateKey(rand.Reader) 1062 if err != nil { 1063 t.Fatalf("Unexpected error generating ed25519 key: %v", err) 1064 } 1065 1066 csrDER, err := x509.CreateCertificateRequest( 1067 rand.Reader, 1068 &x509.CertificateRequest{ 1069 Subject: pkix.Name{ 1070 Organization: []string{}, 1071 }, 1072 }, 1073 privateKey) 1074 if err != nil { 1075 t.Fatalf("Unexpected error creating x509 certificate request: %v", err) 1076 } 1077 1078 csrPemBlock := &pem.Block{ 1079 Type: "CERTIFICATE REQUEST", 1080 Bytes: csrDER, 1081 } 1082 1083 req := pem.EncodeToMemory(csrPemBlock) 1084 if req == nil { 1085 t.Fatalf("Failed to encode PEM to memory.") 1086 } 1087 return req 1088 } 1089 1090 func newABACFileWithContents(t *testing.T, contents string) string { 1091 dir := t.TempDir() 1092 file := filepath.Join(dir, "auth_test") 1093 if err := os.WriteFile(file, []byte(contents), 0700); err != nil { 1094 t.Fatalf("unexpected error writing policyfile: %v", err) 1095 } 1096 return file 1097 } 1098 1099 type trackingAuthorizer struct { 1100 requestAttributes []authorizer.Attributes 1101 } 1102 1103 func (a *trackingAuthorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) { 1104 a.requestAttributes = append(a.requestAttributes, attributes) 1105 return authorizer.DecisionAllow, "", nil 1106 } 1107 1108 // TestAuthorizationAttributeDetermination tests that authorization attributes are built correctly 1109 func TestAuthorizationAttributeDetermination(t *testing.T) { 1110 tCtx := ktesting.Init(t) 1111 1112 trackingAuthorizer := &trackingAuthorizer{} 1113 1114 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 1115 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1116 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1117 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1118 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 1119 }, 1120 ModifyServerConfig: func(config *controlplane.Config) { 1121 config.ControlPlane.Generic.Authorization.Authorizer = unionauthz.New(config.ControlPlane.Generic.Authorization.Authorizer, trackingAuthorizer) 1122 }, 1123 }) 1124 defer tearDownFn() 1125 1126 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-attribute-determination", t) 1127 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1128 1129 transport, err := rest.TransportFor(kubeConfig) 1130 if err != nil { 1131 t.Fatal(err) 1132 } 1133 1134 requests := map[string]struct { 1135 verb string 1136 URL string 1137 expectedAttributes authorizer.Attributes 1138 }{ 1139 "prefix/version/resource": {"GET", "/api/v1/pods", authorizer.AttributesRecord{APIGroup: api.GroupName, Resource: "pods"}}, 1140 "prefix/group/version/resource": {"GET", "/apis/extensions/v1/pods", authorizer.AttributesRecord{APIGroup: extensions.GroupName, Resource: "pods"}}, 1141 "prefix/group/version/resource2": {"GET", "/apis/autoscaling/v1/horizontalpodautoscalers", authorizer.AttributesRecord{APIGroup: autoscaling.GroupName, Resource: "horizontalpodautoscalers"}}, 1142 } 1143 1144 currentAuthorizationAttributesIndex := 0 1145 1146 for testName, r := range requests { 1147 token := BobToken 1148 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, nil) 1149 if err != nil { 1150 t.Logf("case %v", testName) 1151 t.Fatalf("unexpected error: %v", err) 1152 } 1153 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1154 func() { 1155 resp, err := transport.RoundTrip(req) 1156 if err != nil { 1157 t.Logf("case %v", r) 1158 t.Fatalf("unexpected error: %v", err) 1159 } 1160 defer resp.Body.Close() 1161 1162 found := false 1163 for i := currentAuthorizationAttributesIndex; i < len(trackingAuthorizer.requestAttributes); i++ { 1164 if trackingAuthorizer.requestAttributes[i].GetAPIGroup() == r.expectedAttributes.GetAPIGroup() && 1165 trackingAuthorizer.requestAttributes[i].GetResource() == r.expectedAttributes.GetResource() { 1166 found = true 1167 break 1168 } 1169 1170 t.Logf("%#v did not match %#v", r.expectedAttributes, trackingAuthorizer.requestAttributes[i].(*authorizer.AttributesRecord)) 1171 } 1172 if !found { 1173 t.Errorf("did not find %#v in %#v", r.expectedAttributes, trackingAuthorizer.requestAttributes[currentAuthorizationAttributesIndex:]) 1174 } 1175 1176 currentAuthorizationAttributesIndex = len(trackingAuthorizer.requestAttributes) 1177 }() 1178 } 1179 } 1180 1181 // TestNamespaceAuthorization tests that authorization can be controlled 1182 // by namespace. 1183 func TestNamespaceAuthorization(t *testing.T) { 1184 tCtx := ktesting.Init(t) 1185 1186 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 1187 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1188 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1189 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1190 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 1191 opts.Authorization.PolicyFile = newABACFileWithContents(t, `{"namespace": "auth-namespace"}`) 1192 opts.Authorization.Modes = []string{"ABAC"} 1193 }, 1194 }) 1195 defer tearDownFn() 1196 1197 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-namespace", t) 1198 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1199 1200 previousResourceVersion := make(map[string]float64) 1201 transport, err := rest.TransportFor(kubeConfig) 1202 if err != nil { 1203 t.Fatal(err) 1204 } 1205 1206 requests := []struct { 1207 verb string 1208 URL string 1209 namespace string 1210 body string 1211 statusCodes map[int]bool // allowed status codes. 1212 }{ 1213 1214 {"POST", timeoutPath("pods", ns.Name, ""), "foo", aPod, integration.Code201}, 1215 {"GET", path("pods", ns.Name, ""), "foo", "", integration.Code200}, 1216 {"GET", path("pods", ns.Name, "a"), "foo", "", integration.Code200}, 1217 {"DELETE", timeoutPath("pods", ns.Name, "a"), "foo", "", integration.Code200}, 1218 1219 {"POST", timeoutPath("pods", "foo", ""), "bar", aPod, integration.Code403}, 1220 {"GET", path("pods", "foo", ""), "bar", "", integration.Code403}, 1221 {"GET", path("pods", "foo", "a"), "bar", "", integration.Code403}, 1222 {"DELETE", timeoutPath("pods", "foo", "a"), "bar", "", integration.Code403}, 1223 1224 {"POST", timeoutPath("pods", metav1.NamespaceDefault, ""), "", aPod, integration.Code403}, 1225 {"GET", path("pods", "", ""), "", "", integration.Code403}, 1226 {"GET", path("pods", metav1.NamespaceDefault, "a"), "", "", integration.Code403}, 1227 {"DELETE", timeoutPath("pods", metav1.NamespaceDefault, "a"), "", "", integration.Code403}, 1228 } 1229 1230 for _, r := range requests { 1231 token := BobToken 1232 var bodyStr string 1233 if r.body != "" { 1234 sub := "" 1235 if r.verb == "PUT" && r.body != "" { 1236 // For update operations, insert previous resource version 1237 if resVersion := previousResourceVersion[getPreviousResourceVersionKey(r.URL, "")]; resVersion != 0 { 1238 sub += fmt.Sprintf(",\r\n\"resourceVersion\": \"%v\"", resVersion) 1239 } 1240 namespace := r.namespace 1241 // FIXME: Is that correct? 1242 if len(namespace) == 0 { 1243 namespace = "default" 1244 } 1245 sub += fmt.Sprintf(",\r\n\"namespace\": %q", namespace) 1246 } 1247 bodyStr = fmt.Sprintf(r.body, sub) 1248 } 1249 r.body = bodyStr 1250 bodyBytes := bytes.NewReader([]byte(bodyStr)) 1251 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1252 if err != nil { 1253 t.Logf("case %v", r) 1254 t.Fatalf("unexpected error: %v", err) 1255 } 1256 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1257 func() { 1258 resp, err := transport.RoundTrip(req) 1259 if err != nil { 1260 t.Logf("case %v", r) 1261 t.Fatalf("unexpected error: %v", err) 1262 } 1263 defer resp.Body.Close() 1264 b, _ := io.ReadAll(resp.Body) 1265 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 1266 t.Logf("case %v", r) 1267 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 1268 t.Errorf("Body: %v", string(b)) 1269 } else { 1270 if r.verb == "POST" { 1271 // For successful create operations, extract resourceVersion 1272 id, currentResourceVersion, err := parseResourceVersion(b) 1273 if err == nil { 1274 key := getPreviousResourceVersionKey(r.URL, id) 1275 previousResourceVersion[key] = currentResourceVersion 1276 } 1277 } 1278 } 1279 1280 }() 1281 } 1282 } 1283 1284 // TestKindAuthorization tests that authorization can be controlled 1285 // by namespace. 1286 func TestKindAuthorization(t *testing.T) { 1287 tCtx := ktesting.Init(t) 1288 1289 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 1290 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1291 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1292 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1293 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 1294 opts.Authorization.PolicyFile = newABACFileWithContents(t, `{"resource": "services"}`) 1295 opts.Authorization.Modes = []string{"ABAC"} 1296 }, 1297 }) 1298 defer tearDownFn() 1299 1300 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-kind", t) 1301 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1302 1303 previousResourceVersion := make(map[string]float64) 1304 transport, err := rest.TransportFor(kubeConfig) 1305 if err != nil { 1306 t.Fatal(err) 1307 } 1308 1309 requests := []testRequest{ 1310 {"POST", timeoutPath("services", ns.Name, ""), aService, integration.Code201}, 1311 {"GET", path("services", ns.Name, ""), "", integration.Code200}, 1312 {"GET", path("services", ns.Name, "a"), "", integration.Code200}, 1313 {"DELETE", timeoutPath("services", ns.Name, "a"), "", integration.Code200}, 1314 1315 {"POST", timeoutPath("pods", ns.Name, ""), aPod, integration.Code403}, 1316 {"GET", path("pods", "", ""), "", integration.Code403}, 1317 {"GET", path("pods", ns.Name, "a"), "", integration.Code403}, 1318 {"DELETE", timeoutPath("pods", ns.Name, "a"), "", integration.Code403}, 1319 } 1320 1321 for _, r := range requests { 1322 token := BobToken 1323 var bodyStr string 1324 if r.body != "" { 1325 bodyStr = fmt.Sprintf(r.body, "") 1326 if r.verb == "PUT" && r.body != "" { 1327 // For update operations, insert previous resource version 1328 if resVersion := previousResourceVersion[getPreviousResourceVersionKey(r.URL, "")]; resVersion != 0 { 1329 resourceVersionJSON := fmt.Sprintf(",\r\n\"resourceVersion\": \"%v\"", resVersion) 1330 bodyStr = fmt.Sprintf(r.body, resourceVersionJSON) 1331 } 1332 } 1333 } 1334 r.body = bodyStr 1335 bodyBytes := bytes.NewReader([]byte(bodyStr)) 1336 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1337 if err != nil { 1338 t.Logf("case %v", r) 1339 t.Fatalf("unexpected error: %v", err) 1340 } 1341 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1342 func() { 1343 resp, err := transport.RoundTrip(req) 1344 if err != nil { 1345 t.Logf("case %v", r) 1346 t.Fatalf("unexpected error: %v", err) 1347 } 1348 defer resp.Body.Close() 1349 b, _ := io.ReadAll(resp.Body) 1350 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 1351 t.Logf("case %v", r) 1352 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 1353 t.Errorf("Body: %v", string(b)) 1354 } else { 1355 if r.verb == "POST" { 1356 // For successful create operations, extract resourceVersion 1357 id, currentResourceVersion, err := parseResourceVersion(b) 1358 if err == nil { 1359 key := getPreviousResourceVersionKey(r.URL, id) 1360 previousResourceVersion[key] = currentResourceVersion 1361 } 1362 } 1363 } 1364 1365 }() 1366 } 1367 } 1368 1369 // TestReadOnlyAuthorization tests that authorization can be controlled 1370 // by namespace. 1371 func TestReadOnlyAuthorization(t *testing.T) { 1372 tCtx := ktesting.Init(t) 1373 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 1374 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1375 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1376 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1377 opts.Authentication.TokenFile.TokenFile = "testdata/tokens.csv" 1378 opts.Authorization.PolicyFile = newABACFileWithContents(t, `{"readonly": true}`) 1379 opts.Authorization.Modes = []string{"ABAC"} 1380 }, 1381 }) 1382 defer tearDownFn() 1383 1384 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-read-only", t) 1385 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1386 1387 transport, err := rest.TransportFor(kubeConfig) 1388 if err != nil { 1389 t.Fatal(err) 1390 } 1391 1392 requests := []testRequest{ 1393 {"POST", path("pods", ns.Name, ""), aPod, integration.Code403}, 1394 {"GET", path("pods", ns.Name, ""), "", integration.Code200}, 1395 {"GET", path("pods", metav1.NamespaceDefault, "a"), "", integration.Code404}, 1396 } 1397 1398 for _, r := range requests { 1399 token := BobToken 1400 bodyBytes := bytes.NewReader([]byte(r.body)) 1401 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1402 if err != nil { 1403 t.Fatalf("unexpected error: %v", err) 1404 } 1405 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1406 func() { 1407 resp, err := transport.RoundTrip(req) 1408 if err != nil { 1409 t.Logf("case %v", r) 1410 t.Fatalf("unexpected error: %v", err) 1411 } 1412 defer resp.Body.Close() 1413 if _, ok := r.statusCodes[resp.StatusCode]; !ok { 1414 t.Logf("case %v", r) 1415 t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) 1416 b, _ := io.ReadAll(resp.Body) 1417 t.Errorf("Body: %v", string(b)) 1418 } 1419 }() 1420 } 1421 } 1422 1423 // TestWebhookTokenAuthenticator tests that a control plane can use the webhook token 1424 // authenticator to call out to a remote web server for authentication 1425 // decisions. 1426 func TestWebhookTokenAuthenticator(t *testing.T) { 1427 testWebhookTokenAuthenticator(false, t) 1428 } 1429 1430 // TestWebhookTokenAuthenticatorCustomDial is the same as TestWebhookTokenAuthenticator, but uses a 1431 // custom dialer 1432 func TestWebhookTokenAuthenticatorCustomDial(t *testing.T) { 1433 testWebhookTokenAuthenticator(true, t) 1434 } 1435 1436 func testWebhookTokenAuthenticator(customDialer bool, t *testing.T) { 1437 tCtx := ktesting.Init(t) 1438 authServer := newTestWebhookTokenAuthServer() 1439 defer authServer.Close() 1440 var authenticator authenticator.Request 1441 var err error 1442 1443 if customDialer == false { 1444 authenticator, err = getTestWebhookTokenAuth(authServer.URL, nil) 1445 } else { 1446 authenticator, err = getTestWebhookTokenAuthCustomDialer(authServer.URL) 1447 } 1448 1449 if err != nil { 1450 t.Fatalf("error starting webhook token authenticator server: %v", err) 1451 } 1452 1453 kubeClient, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 1454 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 1455 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 1456 opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"} 1457 opts.Authorization.Modes = []string{"ABAC"} 1458 opts.Authorization.PolicyFile = "testdata/allowalice.jsonl" 1459 }, 1460 ModifyServerConfig: func(config *controlplane.Config) { 1461 config.ControlPlane.Generic.Authentication.Authenticator = group.NewAuthenticatedGroupAdder(authenticator) 1462 // Disable checking API audiences that is set by testserver by default. 1463 config.ControlPlane.Generic.Authentication.APIAudiences = nil 1464 }, 1465 }) 1466 defer tearDownFn() 1467 1468 ns := framework.CreateNamespaceOrDie(kubeClient, "auth-webhook-token", t) 1469 defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) 1470 1471 transport, err := rest.TransportFor(kubeConfig) 1472 if err != nil { 1473 t.Fatal(err) 1474 } 1475 1476 for _, r := range getTestRequests(ns.Name) { 1477 // Expect Bob's requests to all fail. 1478 token := BobToken 1479 bodyBytes := bytes.NewReader([]byte(r.body)) 1480 req, err := http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1481 if err != nil { 1482 t.Fatalf("unexpected error: %v", err) 1483 } 1484 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1485 1486 func() { 1487 resp, err := transport.RoundTrip(req) 1488 if err != nil { 1489 t.Logf("case %v", r) 1490 t.Fatalf("unexpected error: %v", err) 1491 } 1492 defer resp.Body.Close() 1493 // Expect all of Bob's actions to return Forbidden 1494 if resp.StatusCode != http.StatusForbidden { 1495 t.Logf("case %v", r) 1496 t.Errorf("Expected http.Forbidden, but got %s", resp.Status) 1497 } 1498 }() 1499 // Expect Alice's requests to succeed. 1500 token = AliceToken 1501 bodyBytes = bytes.NewReader([]byte(r.body)) 1502 req, err = http.NewRequest(r.verb, kubeConfig.Host+r.URL, bodyBytes) 1503 if err != nil { 1504 t.Fatalf("unexpected error: %v", err) 1505 } 1506 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1507 1508 func() { 1509 resp, err := transport.RoundTrip(req) 1510 if err != nil { 1511 t.Logf("case %v", r) 1512 t.Fatalf("unexpected error: %v", err) 1513 } 1514 defer resp.Body.Close() 1515 // Expect all of Alice's actions to at least get past authn/authz. 1516 if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { 1517 t.Logf("case %v", r) 1518 t.Errorf("Expected something other than Unauthorized/Forbidden, but got %s", resp.Status) 1519 } 1520 }() 1521 } 1522 } 1523 1524 // newTestWebhookTokenAuthServer creates an http token authentication server 1525 // that knows about both Alice and Bob. 1526 func newTestWebhookTokenAuthServer() *httptest.Server { 1527 serveHTTP := func(w http.ResponseWriter, r *http.Request) { 1528 var review authenticationv1beta1.TokenReview 1529 if err := json.NewDecoder(r.Body).Decode(&review); err != nil { 1530 http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest) 1531 return 1532 } 1533 type userInfo struct { 1534 Username string `json:"username"` 1535 UID string `json:"uid"` 1536 Groups []string `json:"groups"` 1537 } 1538 type status struct { 1539 Authenticated bool `json:"authenticated"` 1540 User userInfo `json:"user"` 1541 } 1542 var username, uid string 1543 authenticated := false 1544 if review.Spec.Token == AliceToken { 1545 authenticated, username, uid = true, "alice", "1" 1546 } else if review.Spec.Token == BobToken { 1547 authenticated, username, uid = true, "bob", "2" 1548 } 1549 1550 resp := struct { 1551 APIVersion string `json:"apiVersion"` 1552 Status status `json:"status"` 1553 }{ 1554 APIVersion: authenticationv1beta1.SchemeGroupVersion.String(), 1555 Status: status{ 1556 authenticated, 1557 userInfo{ 1558 Username: username, 1559 UID: uid, 1560 }, 1561 }, 1562 } 1563 w.Header().Set("Content-Type", "application/json") 1564 json.NewEncoder(w).Encode(resp) 1565 } 1566 1567 server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP)) 1568 server.Start() 1569 return server 1570 }