k8s.io/kubernetes@v1.29.3/test/integration/auth/authz_config_test.go (about) 1 /* 2 Copyright 2023 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package auth 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "net/http" 25 "net/http/httptest" 26 "os" 27 "path/filepath" 28 "sync/atomic" 29 "testing" 30 "time" 31 32 authorizationv1 "k8s.io/api/authorization/v1" 33 rbacv1 "k8s.io/api/rbac/v1" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apiserver/pkg/features" 36 utilfeature "k8s.io/apiserver/pkg/util/feature" 37 clientset "k8s.io/client-go/kubernetes" 38 "k8s.io/client-go/rest" 39 featuregatetesting "k8s.io/component-base/featuregate/testing" 40 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 41 "k8s.io/kubernetes/test/integration/authutil" 42 "k8s.io/kubernetes/test/integration/framework" 43 ) 44 45 func TestAuthzConfig(t *testing.T) { 46 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)() 47 48 dir := t.TempDir() 49 configFileName := filepath.Join(dir, "config.yaml") 50 if err := os.WriteFile(configFileName, []byte(` 51 apiVersion: apiserver.config.k8s.io/v1alpha1 52 kind: AuthorizationConfiguration 53 authorizers: 54 - type: RBAC 55 name: rbac 56 `), os.FileMode(0644)); err != nil { 57 t.Fatal(err) 58 } 59 60 server := kubeapiservertesting.StartTestServerOrDie( 61 t, 62 nil, 63 []string{"--authorization-config=" + configFileName}, 64 framework.SharedEtcd(), 65 ) 66 t.Cleanup(server.TearDownFn) 67 68 // Make sure anonymous requests work 69 anonymousClient := clientset.NewForConfigOrDie(rest.AnonymousClientConfig(server.ClientConfig)) 70 healthzResult, err := anonymousClient.DiscoveryClient.RESTClient().Get().AbsPath("/healthz").Do(context.TODO()).Raw() 71 if !bytes.Equal(healthzResult, []byte(`ok`)) { 72 t.Fatalf("expected 'ok', got %s", string(healthzResult)) 73 } 74 if err != nil { 75 t.Fatal(err) 76 } 77 78 adminClient := clientset.NewForConfigOrDie(server.ClientConfig) 79 80 sar := &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{ 81 User: "alice", 82 ResourceAttributes: &authorizationv1.ResourceAttributes{ 83 Namespace: "foo", 84 Verb: "create", 85 Group: "", 86 Version: "v1", 87 Resource: "configmaps", 88 }, 89 }} 90 result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{}) 91 if err != nil { 92 t.Fatal(err) 93 } 94 if result.Status.Allowed { 95 t.Fatal("expected denied, got allowed") 96 } 97 98 authutil.GrantUserAuthorization(t, context.TODO(), adminClient, "alice", 99 rbacv1.PolicyRule{ 100 Verbs: []string{"create"}, 101 APIGroups: []string{""}, 102 Resources: []string{"configmaps"}, 103 }, 104 ) 105 106 result, err = adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{}) 107 if err != nil { 108 t.Fatal(err) 109 } 110 if !result.Status.Allowed { 111 t.Fatal("expected allowed, got denied") 112 } 113 } 114 115 func TestMultiWebhookAuthzConfig(t *testing.T) { 116 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)() 117 118 dir := t.TempDir() 119 120 kubeconfigTemplate := ` 121 apiVersion: v1 122 kind: Config 123 clusters: 124 - name: integration 125 cluster: 126 server: %q 127 insecure-skip-tls-verify: true 128 contexts: 129 - name: default-context 130 context: 131 cluster: integration 132 user: test 133 current-context: default-context 134 users: 135 - name: test 136 ` 137 138 // returns malformed responses when called 139 serverErrorCalled := atomic.Int32{} 140 serverError := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 141 serverErrorCalled.Add(1) 142 sar := &authorizationv1.SubjectAccessReview{} 143 if err := json.NewDecoder(req.Body).Decode(sar); err != nil { 144 t.Error(err) 145 } 146 t.Log("serverError", sar) 147 if _, err := w.Write([]byte(`error response`)); err != nil { 148 t.Error(err) 149 } 150 })) 151 defer serverError.Close() 152 serverErrorKubeconfigName := filepath.Join(dir, "serverError.yaml") 153 if err := os.WriteFile(serverErrorKubeconfigName, []byte(fmt.Sprintf(kubeconfigTemplate, serverError.URL)), os.FileMode(0644)); err != nil { 154 t.Fatal(err) 155 } 156 157 // hangs for 2 seconds when called 158 serverTimeoutCalled := atomic.Int32{} 159 serverTimeout := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 160 serverTimeoutCalled.Add(1) 161 sar := &authorizationv1.SubjectAccessReview{} 162 if err := json.NewDecoder(req.Body).Decode(sar); err != nil { 163 t.Error(err) 164 } 165 t.Log("serverTimeout", sar) 166 time.Sleep(2 * time.Second) 167 })) 168 defer serverTimeout.Close() 169 serverTimeoutKubeconfigName := filepath.Join(dir, "serverTimeout.yaml") 170 if err := os.WriteFile(serverTimeoutKubeconfigName, []byte(fmt.Sprintf(kubeconfigTemplate, serverTimeout.URL)), os.FileMode(0644)); err != nil { 171 t.Fatal(err) 172 } 173 174 // returns a deny response when called 175 serverDenyCalled := atomic.Int32{} 176 serverDeny := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 177 serverDenyCalled.Add(1) 178 sar := &authorizationv1.SubjectAccessReview{} 179 if err := json.NewDecoder(req.Body).Decode(sar); err != nil { 180 t.Error(err) 181 } 182 t.Log("serverDeny", sar) 183 sar.Status.Allowed = false 184 sar.Status.Denied = true 185 sar.Status.Reason = "denied by webhook" 186 if err := json.NewEncoder(w).Encode(sar); err != nil { 187 t.Error(err) 188 } 189 })) 190 defer serverDeny.Close() 191 serverDenyKubeconfigName := filepath.Join(dir, "serverDeny.yaml") 192 if err := os.WriteFile(serverDenyKubeconfigName, []byte(fmt.Sprintf(kubeconfigTemplate, serverDeny.URL)), os.FileMode(0644)); err != nil { 193 t.Fatal(err) 194 } 195 196 // returns a no opinion response when called 197 serverNoOpinionCalled := atomic.Int32{} 198 serverNoOpinion := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 199 serverNoOpinionCalled.Add(1) 200 sar := &authorizationv1.SubjectAccessReview{} 201 if err := json.NewDecoder(req.Body).Decode(sar); err != nil { 202 t.Error(err) 203 } 204 t.Log("serverNoOpinion", sar) 205 sar.Status.Allowed = false 206 sar.Status.Denied = false 207 if err := json.NewEncoder(w).Encode(sar); err != nil { 208 t.Error(err) 209 } 210 })) 211 defer serverNoOpinion.Close() 212 serverNoOpinionKubeconfigName := filepath.Join(dir, "serverNoOpinion.yaml") 213 if err := os.WriteFile(serverNoOpinionKubeconfigName, []byte(fmt.Sprintf(kubeconfigTemplate, serverNoOpinion.URL)), os.FileMode(0644)); err != nil { 214 t.Fatal(err) 215 } 216 217 // returns an allow response when called 218 serverAllowCalled := atomic.Int32{} 219 serverAllow := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 220 serverAllowCalled.Add(1) 221 sar := &authorizationv1.SubjectAccessReview{} 222 if err := json.NewDecoder(req.Body).Decode(sar); err != nil { 223 t.Error(err) 224 } 225 t.Log("serverAllow", sar) 226 sar.Status.Allowed = true 227 sar.Status.Reason = "allowed by webhook" 228 if err := json.NewEncoder(w).Encode(sar); err != nil { 229 t.Error(err) 230 } 231 })) 232 defer serverAllow.Close() 233 serverAllowKubeconfigName := filepath.Join(dir, "serverAllow.yaml") 234 if err := os.WriteFile(serverAllowKubeconfigName, []byte(fmt.Sprintf(kubeconfigTemplate, serverAllow.URL)), os.FileMode(0644)); err != nil { 235 t.Fatal(err) 236 } 237 238 resetCounts := func() { 239 serverErrorCalled.Store(0) 240 serverTimeoutCalled.Store(0) 241 serverDenyCalled.Store(0) 242 serverNoOpinionCalled.Store(0) 243 serverAllowCalled.Store(0) 244 } 245 assertCounts := func(errorCount, timeoutCount, denyCount, noOpinionCount, allowCount int32) { 246 t.Helper() 247 if e, a := errorCount, serverErrorCalled.Load(); e != a { 248 t.Errorf("expected fail webhook calls: %d, got %d", e, a) 249 } 250 if e, a := timeoutCount, serverTimeoutCalled.Load(); e != a { 251 t.Errorf("expected timeout webhook calls: %d, got %d", e, a) 252 } 253 if e, a := denyCount, serverDenyCalled.Load(); e != a { 254 t.Errorf("expected deny webhook calls: %d, got %d", e, a) 255 } 256 if e, a := noOpinionCount, serverNoOpinionCalled.Load(); e != a { 257 t.Errorf("expected noOpinion webhook calls: %d, got %d", e, a) 258 } 259 if e, a := allowCount, serverAllowCalled.Load(); e != a { 260 t.Errorf("expected allow webhook calls: %d, got %d", e, a) 261 } 262 resetCounts() 263 } 264 265 configFileName := filepath.Join(dir, "config.yaml") 266 if err := os.WriteFile(configFileName, []byte(` 267 apiVersion: apiserver.config.k8s.io/v1alpha1 268 kind: AuthorizationConfiguration 269 authorizers: 270 - type: Webhook 271 name: error.example.com 272 webhook: 273 timeout: 5s 274 failurePolicy: Deny 275 subjectAccessReviewVersion: v1 276 matchConditionSubjectAccessReviewVersion: v1 277 connectionInfo: 278 type: KubeConfigFile 279 kubeConfigFile: `+serverErrorKubeconfigName+` 280 matchConditions: 281 - expression: has(request.resourceAttributes) 282 - expression: 'request.resourceAttributes.namespace == "fail"' 283 - expression: 'request.resourceAttributes.name == "error"' 284 285 - type: Webhook 286 name: timeout.example.com 287 webhook: 288 timeout: 1s 289 failurePolicy: Deny 290 subjectAccessReviewVersion: v1 291 matchConditionSubjectAccessReviewVersion: v1 292 connectionInfo: 293 type: KubeConfigFile 294 kubeConfigFile: `+serverTimeoutKubeconfigName+` 295 matchConditions: 296 - expression: has(request.resourceAttributes) 297 - expression: 'request.resourceAttributes.namespace == "fail"' 298 - expression: 'request.resourceAttributes.name == "timeout"' 299 300 - type: Webhook 301 name: deny.example.com 302 webhook: 303 timeout: 5s 304 failurePolicy: NoOpinion 305 subjectAccessReviewVersion: v1 306 matchConditionSubjectAccessReviewVersion: v1 307 connectionInfo: 308 type: KubeConfigFile 309 kubeConfigFile: `+serverDenyKubeconfigName+` 310 matchConditions: 311 - expression: has(request.resourceAttributes) 312 - expression: 'request.resourceAttributes.namespace == "fail"' 313 314 - type: Webhook 315 name: noopinion.example.com 316 webhook: 317 timeout: 5s 318 failurePolicy: Deny 319 subjectAccessReviewVersion: v1 320 connectionInfo: 321 type: KubeConfigFile 322 kubeConfigFile: `+serverNoOpinionKubeconfigName+` 323 324 - type: Webhook 325 name: allow.example.com 326 webhook: 327 timeout: 5s 328 failurePolicy: Deny 329 subjectAccessReviewVersion: v1 330 connectionInfo: 331 type: KubeConfigFile 332 kubeConfigFile: `+serverAllowKubeconfigName+` 333 `), os.FileMode(0644)); err != nil { 334 t.Fatal(err) 335 } 336 337 server := kubeapiservertesting.StartTestServerOrDie( 338 t, 339 nil, 340 []string{"--authorization-config=" + configFileName}, 341 framework.SharedEtcd(), 342 ) 343 t.Cleanup(server.TearDownFn) 344 345 adminClient := clientset.NewForConfigOrDie(server.ClientConfig) 346 347 // malformed webhook short circuits 348 t.Log("checking error") 349 if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{ 350 User: "alice", 351 ResourceAttributes: &authorizationv1.ResourceAttributes{ 352 Verb: "get", 353 Group: "", 354 Version: "v1", 355 Resource: "configmaps", 356 Namespace: "fail", 357 Name: "error", 358 }, 359 }}, metav1.CreateOptions{}); err != nil { 360 t.Fatal(err) 361 } else if result.Status.Allowed { 362 t.Fatal("expected denied, got allowed") 363 } else { 364 t.Log(result.Status.Reason) 365 assertCounts(1, 0, 0, 0, 0) 366 } 367 368 // timeout webhook short circuits 369 t.Log("checking timeout") 370 if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{ 371 User: "alice", 372 ResourceAttributes: &authorizationv1.ResourceAttributes{ 373 Verb: "get", 374 Group: "", 375 Version: "v1", 376 Resource: "configmaps", 377 Namespace: "fail", 378 Name: "timeout", 379 }, 380 }}, metav1.CreateOptions{}); err != nil { 381 t.Fatal(err) 382 } else if result.Status.Allowed { 383 t.Fatal("expected denied, got allowed") 384 } else { 385 t.Log(result.Status.Reason) 386 assertCounts(0, 1, 0, 0, 0) 387 } 388 389 // deny webhook short circuits 390 t.Log("checking deny") 391 if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{ 392 User: "alice", 393 ResourceAttributes: &authorizationv1.ResourceAttributes{ 394 Verb: "list", 395 Group: "", 396 Version: "v1", 397 Resource: "configmaps", 398 Namespace: "fail", 399 Name: "", 400 }, 401 }}, metav1.CreateOptions{}); err != nil { 402 t.Fatal(err) 403 } else if result.Status.Allowed { 404 t.Fatal("expected denied, got allowed") 405 } else { 406 t.Log(result.Status.Reason) 407 assertCounts(0, 0, 1, 0, 0) 408 } 409 410 // no-opinion webhook passes through, allow webhook allows 411 t.Log("checking allow") 412 if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{ 413 User: "alice", 414 ResourceAttributes: &authorizationv1.ResourceAttributes{ 415 Verb: "list", 416 Group: "", 417 Version: "v1", 418 Resource: "configmaps", 419 Namespace: "allow", 420 Name: "", 421 }, 422 }}, metav1.CreateOptions{}); err != nil { 423 t.Fatal(err) 424 } else if !result.Status.Allowed { 425 t.Fatal("expected allowed, got denied") 426 } else { 427 t.Log(result.Status.Reason) 428 assertCounts(0, 0, 0, 1, 1) 429 } 430 }