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  }