k8s.io/apiserver@v0.31.1/pkg/endpoints/filters/impersonation_test.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package filters 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 "net/http/httptest" 24 "reflect" 25 "strings" 26 "sync" 27 "testing" 28 29 authenticationapi "k8s.io/api/authentication/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 serializer "k8s.io/apimachinery/pkg/runtime/serializer" 32 "k8s.io/apiserver/pkg/authentication/user" 33 "k8s.io/apiserver/pkg/authorization/authorizer" 34 "k8s.io/apiserver/pkg/endpoints/request" 35 ) 36 37 type impersonateAuthorizer struct{} 38 39 func (impersonateAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { 40 user := a.GetUser() 41 42 switch { 43 case user.GetName() == "system:admin": 44 return authorizer.DecisionAllow, "", nil 45 46 case user.GetName() == "tester": 47 return authorizer.DecisionNoOpinion, "", fmt.Errorf("works on my machine") 48 49 case user.GetName() == "deny-me": 50 return authorizer.DecisionNoOpinion, "denied", nil 51 } 52 53 if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "wheel" && a.GetVerb() == "impersonate" && a.GetResource() == "users" { 54 return authorizer.DecisionAllow, "", nil 55 } 56 57 if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "sa-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "serviceaccounts" { 58 return authorizer.DecisionAllow, "", nil 59 } 60 61 if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "regular-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "users" { 62 return authorizer.DecisionAllow, "", nil 63 } 64 65 if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "group-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "groups" { 66 return authorizer.DecisionAllow, "", nil 67 } 68 69 if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-scopes" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" { 70 return authorizer.DecisionAllow, "", nil 71 } 72 73 if len(user.GetGroups()) > 1 && (user.GetGroups()[1] == "escaped-scopes" || user.GetGroups()[1] == "almost-escaped-scopes") { 74 return authorizer.DecisionAllow, "", nil 75 } 76 77 if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-particular-scopes" && 78 a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" && a.GetName() == "scope-a" && a.GetAPIGroup() == "authentication.k8s.io" { 79 return authorizer.DecisionAllow, "", nil 80 } 81 82 if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-project" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "project" && a.GetAPIGroup() == "authentication.k8s.io" { 83 return authorizer.DecisionAllow, "", nil 84 } 85 86 if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "everything-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "users" && a.GetAPIGroup() == "" { 87 return authorizer.DecisionAllow, "", nil 88 } 89 90 if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "everything-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "uids" && a.GetName() == "some-uid" && a.GetAPIGroup() == "authentication.k8s.io" { 91 return authorizer.DecisionAllow, "", nil 92 } 93 94 if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "everything-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "groups" && a.GetAPIGroup() == "" { 95 return authorizer.DecisionAllow, "", nil 96 } 97 98 if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "everything-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" && a.GetAPIGroup() == "authentication.k8s.io" { 99 return authorizer.DecisionAllow, "", nil 100 } 101 102 return authorizer.DecisionNoOpinion, "deny by default", nil 103 } 104 105 func TestImpersonationFilter(t *testing.T) { 106 testCases := []struct { 107 name string 108 user user.Info 109 impersonationUser string 110 impersonationGroups []string 111 impersonationUserExtras map[string][]string 112 impersonationUid string 113 expectedUser user.Info 114 expectedCode int 115 }{ 116 { 117 name: "not-impersonating", 118 user: &user.DefaultInfo{ 119 Name: "tester", 120 }, 121 expectedUser: &user.DefaultInfo{ 122 Name: "tester", 123 }, 124 expectedCode: http.StatusOK, 125 }, 126 { 127 name: "impersonating-error", 128 user: &user.DefaultInfo{ 129 Name: "tester", 130 }, 131 impersonationUser: "anyone", 132 expectedUser: &user.DefaultInfo{ 133 Name: "tester", 134 }, 135 expectedCode: http.StatusForbidden, 136 }, 137 { 138 name: "impersonating-group-without-user", 139 user: &user.DefaultInfo{ 140 Name: "tester", 141 }, 142 impersonationGroups: []string{"some-group"}, 143 expectedUser: &user.DefaultInfo{ 144 Name: "tester", 145 }, 146 expectedCode: http.StatusInternalServerError, 147 }, 148 { 149 name: "impersonating-extra-without-user", 150 user: &user.DefaultInfo{ 151 Name: "tester", 152 }, 153 impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, 154 expectedUser: &user.DefaultInfo{ 155 Name: "tester", 156 }, 157 expectedCode: http.StatusInternalServerError, 158 }, 159 { 160 name: "impersonating-uid-without-user", 161 user: &user.DefaultInfo{ 162 Name: "tester", 163 }, 164 impersonationUid: "some-uid", 165 expectedUser: &user.DefaultInfo{ 166 Name: "tester", 167 }, 168 expectedCode: http.StatusInternalServerError, 169 }, 170 { 171 name: "disallowed-group", 172 user: &user.DefaultInfo{ 173 Name: "dev", 174 Groups: []string{"wheel"}, 175 }, 176 impersonationUser: "system:admin", 177 impersonationGroups: []string{"some-group"}, 178 expectedUser: &user.DefaultInfo{ 179 Name: "dev", 180 Groups: []string{"wheel"}, 181 }, 182 expectedCode: http.StatusForbidden, 183 }, 184 { 185 name: "allowed-group", 186 user: &user.DefaultInfo{ 187 Name: "dev", 188 Groups: []string{"wheel", "group-impersonater"}, 189 }, 190 impersonationUser: "system:admin", 191 impersonationGroups: []string{"some-group"}, 192 expectedUser: &user.DefaultInfo{ 193 Name: "system:admin", 194 Groups: []string{"some-group", "system:authenticated"}, 195 Extra: map[string][]string{}, 196 }, 197 expectedCode: http.StatusOK, 198 }, 199 { 200 name: "disallowed-userextra-1", 201 user: &user.DefaultInfo{ 202 Name: "dev", 203 Groups: []string{"wheel"}, 204 }, 205 impersonationUser: "system:admin", 206 impersonationGroups: []string{"some-group"}, 207 impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, 208 expectedUser: &user.DefaultInfo{ 209 Name: "dev", 210 Groups: []string{"wheel"}, 211 }, 212 expectedCode: http.StatusForbidden, 213 }, 214 { 215 name: "disallowed-userextra-2", 216 user: &user.DefaultInfo{ 217 Name: "dev", 218 Groups: []string{"wheel", "extra-setter-project"}, 219 }, 220 impersonationUser: "system:admin", 221 impersonationGroups: []string{"some-group"}, 222 impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, 223 expectedUser: &user.DefaultInfo{ 224 Name: "dev", 225 Groups: []string{"wheel", "extra-setter-project"}, 226 }, 227 expectedCode: http.StatusForbidden, 228 }, 229 { 230 name: "disallowed-userextra-3", 231 user: &user.DefaultInfo{ 232 Name: "dev", 233 Groups: []string{"wheel", "extra-setter-particular-scopes"}, 234 }, 235 impersonationUser: "system:admin", 236 impersonationGroups: []string{"some-group"}, 237 impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}}, 238 expectedUser: &user.DefaultInfo{ 239 Name: "dev", 240 Groups: []string{"wheel", "extra-setter-particular-scopes"}, 241 }, 242 expectedCode: http.StatusForbidden, 243 }, 244 { 245 name: "allowed-userextras", 246 user: &user.DefaultInfo{ 247 Name: "dev", 248 Groups: []string{"wheel", "extra-setter-scopes"}, 249 }, 250 impersonationUser: "system:admin", 251 impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}}, 252 expectedUser: &user.DefaultInfo{ 253 Name: "system:admin", 254 Groups: []string{"system:authenticated"}, 255 Extra: map[string][]string{"scopes": {"scope-a", "scope-b"}}, 256 }, 257 expectedCode: http.StatusOK, 258 }, 259 { 260 name: "percent-escaped-userextras", 261 user: &user.DefaultInfo{ 262 Name: "dev", 263 Groups: []string{"wheel", "escaped-scopes"}, 264 }, 265 impersonationUser: "system:admin", 266 impersonationUserExtras: map[string][]string{"example.com%2fescaped%e1%9b%84scopes": {"scope-a", "scope-b"}}, 267 expectedUser: &user.DefaultInfo{ 268 Name: "system:admin", 269 Groups: []string{"system:authenticated"}, 270 Extra: map[string][]string{"example.com/escapedᛄscopes": {"scope-a", "scope-b"}}, 271 }, 272 expectedCode: http.StatusOK, 273 }, 274 { 275 name: "almost-percent-escaped-userextras", 276 user: &user.DefaultInfo{ 277 Name: "dev", 278 Groups: []string{"wheel", "almost-escaped-scopes"}, 279 }, 280 impersonationUser: "system:admin", 281 impersonationUserExtras: map[string][]string{"almost%zzpercent%xxencoded": {"scope-a", "scope-b"}}, 282 expectedUser: &user.DefaultInfo{ 283 Name: "system:admin", 284 Groups: []string{"system:authenticated"}, 285 Extra: map[string][]string{"almost%zzpercent%xxencoded": {"scope-a", "scope-b"}}, 286 }, 287 expectedCode: http.StatusOK, 288 }, 289 { 290 name: "allowed-users-impersonation", 291 user: &user.DefaultInfo{ 292 Name: "dev", 293 Groups: []string{"regular-impersonater"}, 294 }, 295 impersonationUser: "tester", 296 expectedUser: &user.DefaultInfo{ 297 Name: "tester", 298 Groups: []string{"system:authenticated"}, 299 Extra: map[string][]string{}, 300 }, 301 expectedCode: http.StatusOK, 302 }, 303 { 304 name: "disallowed-impersonating", 305 user: &user.DefaultInfo{ 306 Name: "dev", 307 Groups: []string{"sa-impersonater"}, 308 }, 309 impersonationUser: "tester", 310 expectedUser: &user.DefaultInfo{ 311 Name: "dev", 312 Groups: []string{"sa-impersonater"}, 313 }, 314 expectedCode: http.StatusForbidden, 315 }, 316 { 317 name: "allowed-sa-impersonating", 318 user: &user.DefaultInfo{ 319 Name: "dev", 320 Groups: []string{"sa-impersonater"}, 321 Extra: map[string][]string{}, 322 }, 323 impersonationUser: "system:serviceaccount:foo:default", 324 expectedUser: &user.DefaultInfo{ 325 Name: "system:serviceaccount:foo:default", 326 Groups: []string{"system:serviceaccounts", "system:serviceaccounts:foo", "system:authenticated"}, 327 Extra: map[string][]string{}, 328 }, 329 expectedCode: http.StatusOK, 330 }, 331 { 332 name: "anonymous-username-prevents-adding-authenticated-group", 333 user: &user.DefaultInfo{ 334 Name: "system:admin", 335 }, 336 impersonationUser: "system:anonymous", 337 expectedUser: &user.DefaultInfo{ 338 Name: "system:anonymous", 339 Groups: []string{"system:unauthenticated"}, 340 Extra: map[string][]string{}, 341 }, 342 expectedCode: http.StatusOK, 343 }, 344 { 345 name: "unauthenticated-group-prevents-adding-authenticated-group", 346 user: &user.DefaultInfo{ 347 Name: "system:admin", 348 }, 349 impersonationUser: "unknown", 350 impersonationGroups: []string{"system:unauthenticated"}, 351 expectedUser: &user.DefaultInfo{ 352 Name: "unknown", 353 Groups: []string{"system:unauthenticated"}, 354 Extra: map[string][]string{}, 355 }, 356 expectedCode: http.StatusOK, 357 }, 358 { 359 name: "unauthenticated-group-prevents-double-adding-authenticated-group", 360 user: &user.DefaultInfo{ 361 Name: "system:admin", 362 }, 363 impersonationUser: "unknown", 364 impersonationGroups: []string{"system:authenticated"}, 365 expectedUser: &user.DefaultInfo{ 366 Name: "unknown", 367 Groups: []string{"system:authenticated"}, 368 Extra: map[string][]string{}, 369 }, 370 expectedCode: http.StatusOK, 371 }, 372 { 373 name: "specified-authenticated-group-prevents-double-adding-authenticated-group", 374 user: &user.DefaultInfo{ 375 Name: "dev", 376 Groups: []string{"wheel", "group-impersonater"}, 377 }, 378 impersonationUser: "system:admin", 379 impersonationGroups: []string{"some-group", "system:authenticated"}, 380 expectedUser: &user.DefaultInfo{ 381 Name: "system:admin", 382 Groups: []string{"some-group", "system:authenticated"}, 383 Extra: map[string][]string{}, 384 }, 385 expectedCode: http.StatusOK, 386 }, 387 { 388 name: "anonymous-user-should-include-unauthenticated-group", 389 user: &user.DefaultInfo{ 390 Name: "system:admin", 391 }, 392 impersonationUser: "system:anonymous", 393 expectedUser: &user.DefaultInfo{ 394 Name: "system:anonymous", 395 Groups: []string{"system:unauthenticated"}, 396 Extra: map[string][]string{}, 397 }, 398 expectedCode: http.StatusOK, 399 }, 400 { 401 name: "anonymous-user-prevents-double-adding-unauthenticated-group", 402 user: &user.DefaultInfo{ 403 Name: "system:admin", 404 }, 405 impersonationUser: "system:anonymous", 406 impersonationGroups: []string{"system:unauthenticated"}, 407 expectedUser: &user.DefaultInfo{ 408 Name: "system:anonymous", 409 Groups: []string{"system:unauthenticated"}, 410 Extra: map[string][]string{}, 411 }, 412 expectedCode: http.StatusOK, 413 }, 414 { 415 name: "allowed-user-impersonation-with-uid", 416 user: &user.DefaultInfo{ 417 Name: "dev", 418 Groups: []string{ 419 "everything-impersonater", 420 }, 421 }, 422 impersonationUser: "tester", 423 impersonationUid: "some-uid", 424 expectedUser: &user.DefaultInfo{ 425 Name: "tester", 426 Groups: []string{"system:authenticated"}, 427 Extra: map[string][]string{}, 428 UID: "some-uid", 429 }, 430 expectedCode: http.StatusOK, 431 }, 432 { 433 name: "disallowed-user-impersonation-with-uid", 434 user: &user.DefaultInfo{ 435 Name: "dev", 436 Groups: []string{ 437 "everything-impersonater", 438 }, 439 }, 440 impersonationUser: "tester", 441 impersonationUid: "disallowed-uid", 442 expectedUser: &user.DefaultInfo{ 443 Name: "dev", 444 Groups: []string{"everything-impersonater"}, 445 }, 446 expectedCode: http.StatusForbidden, 447 }, 448 { 449 name: "allowed-impersonation-with-all-headers", 450 user: &user.DefaultInfo{ 451 Name: "dev", 452 Groups: []string{ 453 "everything-impersonater", 454 }, 455 }, 456 impersonationUser: "tester", 457 impersonationUid: "some-uid", 458 impersonationGroups: []string{"system:authenticated"}, 459 impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}}, 460 expectedUser: &user.DefaultInfo{ 461 Name: "tester", 462 Groups: []string{"system:authenticated"}, 463 UID: "some-uid", 464 Extra: map[string][]string{"scopes": {"scope-a", "scope-b"}}, 465 }, 466 expectedCode: http.StatusOK, 467 }, 468 } 469 470 var ctx context.Context 471 var actualUser user.Info 472 var lock sync.Mutex 473 474 doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 475 currentCtx := req.Context() 476 user, exists := request.UserFrom(currentCtx) 477 if !exists { 478 actualUser = nil 479 return 480 } 481 482 actualUser = user 483 484 if _, ok := req.Header[authenticationapi.ImpersonateUserHeader]; ok { 485 t.Fatal("user header still present") 486 } 487 if _, ok := req.Header[authenticationapi.ImpersonateGroupHeader]; ok { 488 t.Fatal("group header still present") 489 } 490 for key := range req.Header { 491 if strings.HasPrefix(key, authenticationapi.ImpersonateUserExtraHeaderPrefix) { 492 t.Fatalf("extra header still present: %v", key) 493 } 494 } 495 if _, ok := req.Header[authenticationapi.ImpersonateUIDHeader]; ok { 496 t.Fatal("uid header still present") 497 } 498 499 }) 500 handler := func(delegate http.Handler) http.Handler { 501 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 502 defer func() { 503 if r := recover(); r != nil { 504 t.Errorf("Recovered %v", r) 505 } 506 }() 507 lock.Lock() 508 defer lock.Unlock() 509 req = req.WithContext(ctx) 510 currentCtx := req.Context() 511 512 user, exists := request.UserFrom(currentCtx) 513 if !exists { 514 actualUser = nil 515 return 516 } else { 517 actualUser = user 518 } 519 520 delegate.ServeHTTP(w, req) 521 }) 522 }(WithImpersonation(doNothingHandler, impersonateAuthorizer{}, serializer.NewCodecFactory(runtime.NewScheme()))) 523 524 server := httptest.NewServer(handler) 525 defer server.Close() 526 527 for _, tc := range testCases { 528 t.Run(tc.name, func(t *testing.T) { 529 func() { 530 lock.Lock() 531 defer lock.Unlock() 532 ctx = request.WithUser(request.NewContext(), tc.user) 533 }() 534 535 req, err := http.NewRequest("GET", server.URL, nil) 536 if err != nil { 537 t.Errorf("%s: unexpected error: %v", tc.name, err) 538 return 539 } 540 if len(tc.impersonationUser) > 0 { 541 req.Header.Add(authenticationapi.ImpersonateUserHeader, tc.impersonationUser) 542 } 543 for _, group := range tc.impersonationGroups { 544 req.Header.Add(authenticationapi.ImpersonateGroupHeader, group) 545 } 546 for extraKey, values := range tc.impersonationUserExtras { 547 for _, value := range values { 548 req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value) 549 } 550 } 551 if len(tc.impersonationUid) > 0 { 552 req.Header.Add(authenticationapi.ImpersonateUIDHeader, tc.impersonationUid) 553 } 554 555 resp, err := http.DefaultClient.Do(req) 556 if err != nil { 557 t.Errorf("%s: unexpected error: %v", tc.name, err) 558 return 559 } 560 if resp.StatusCode != tc.expectedCode { 561 t.Errorf("%s: expected %v, actual %v", tc.name, tc.expectedCode, resp.StatusCode) 562 return 563 } 564 565 if !reflect.DeepEqual(actualUser, tc.expectedUser) { 566 t.Errorf("%s: expected %#v, actual %#v", tc.name, tc.expectedUser, actualUser) 567 return 568 } 569 }) 570 } 571 }