k8s.io/apiserver@v0.31.1/pkg/endpoints/filters/authorization_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  	"errors"
    22  	"k8s.io/apimachinery/pkg/fields"
    23  	"k8s.io/apimachinery/pkg/labels"
    24  	"k8s.io/apimachinery/pkg/selection"
    25  	genericfeatures "k8s.io/apiserver/pkg/features"
    26  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    27  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    28  	"net/http"
    29  	"net/http/httptest"
    30  	"reflect"
    31  	"testing"
    32  
    33  	"github.com/stretchr/testify/assert"
    34  	batch "k8s.io/api/batch/v1"
    35  	"k8s.io/apimachinery/pkg/runtime"
    36  	"k8s.io/apimachinery/pkg/runtime/serializer"
    37  	auditinternal "k8s.io/apiserver/pkg/apis/audit"
    38  	"k8s.io/apiserver/pkg/audit"
    39  	"k8s.io/apiserver/pkg/authorization/authorizer"
    40  )
    41  
    42  func TestGetAuthorizerAttributes(t *testing.T) {
    43  	basicLabelRequirement, err := labels.NewRequirement("foo", selection.DoubleEquals, []string{"bar"})
    44  	if err != nil {
    45  		t.Fatal(err)
    46  	}
    47  
    48  	testcases := map[string]struct {
    49  		Verb                        string
    50  		Path                        string
    51  		ExpectedAttributes          *authorizer.AttributesRecord
    52  		EnableAuthorizationSelector bool
    53  	}{
    54  		"non-resource root": {
    55  			Verb: "POST",
    56  			Path: "/",
    57  			ExpectedAttributes: &authorizer.AttributesRecord{
    58  				Verb: "post",
    59  				Path: "/",
    60  			},
    61  		},
    62  		"non-resource api prefix": {
    63  			Verb: "GET",
    64  			Path: "/api/",
    65  			ExpectedAttributes: &authorizer.AttributesRecord{
    66  				Verb: "get",
    67  				Path: "/api/",
    68  			},
    69  		},
    70  		"non-resource group api prefix": {
    71  			Verb: "GET",
    72  			Path: "/apis/extensions/",
    73  			ExpectedAttributes: &authorizer.AttributesRecord{
    74  				Verb: "get",
    75  				Path: "/apis/extensions/",
    76  			},
    77  		},
    78  
    79  		"resource": {
    80  			Verb: "POST",
    81  			Path: "/api/v1/nodes/mynode",
    82  			ExpectedAttributes: &authorizer.AttributesRecord{
    83  				Verb:            "create",
    84  				Path:            "/api/v1/nodes/mynode",
    85  				ResourceRequest: true,
    86  				Resource:        "nodes",
    87  				APIVersion:      "v1",
    88  				Name:            "mynode",
    89  			},
    90  		},
    91  		"namespaced resource": {
    92  			Verb: "PUT",
    93  			Path: "/api/v1/namespaces/myns/pods/mypod",
    94  			ExpectedAttributes: &authorizer.AttributesRecord{
    95  				Verb:            "update",
    96  				Path:            "/api/v1/namespaces/myns/pods/mypod",
    97  				ResourceRequest: true,
    98  				Namespace:       "myns",
    99  				Resource:        "pods",
   100  				APIVersion:      "v1",
   101  				Name:            "mypod",
   102  			},
   103  		},
   104  		"API group resource": {
   105  			Verb: "GET",
   106  			Path: "/apis/batch/v1/namespaces/myns/jobs",
   107  			ExpectedAttributes: &authorizer.AttributesRecord{
   108  				Verb:            "list",
   109  				Path:            "/apis/batch/v1/namespaces/myns/jobs",
   110  				ResourceRequest: true,
   111  				APIGroup:        batch.GroupName,
   112  				APIVersion:      "v1",
   113  				Namespace:       "myns",
   114  				Resource:        "jobs",
   115  			},
   116  		},
   117  		"disabled, ignore good field selector": {
   118  			Verb: "GET",
   119  			Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector%=foo%3Dbar",
   120  			ExpectedAttributes: &authorizer.AttributesRecord{
   121  				Verb:            "list",
   122  				Path:            "/apis/batch/v1/namespaces/myns/jobs",
   123  				ResourceRequest: true,
   124  				APIGroup:        batch.GroupName,
   125  				APIVersion:      "v1",
   126  				Namespace:       "myns",
   127  				Resource:        "jobs",
   128  			},
   129  		},
   130  		"enabled, good field selector": {
   131  			Verb: "GET",
   132  			Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=foo%3D%3Dbar",
   133  			ExpectedAttributes: &authorizer.AttributesRecord{
   134  				Verb:            "list",
   135  				Path:            "/apis/batch/v1/namespaces/myns/jobs",
   136  				ResourceRequest: true,
   137  				APIGroup:        batch.GroupName,
   138  				APIVersion:      "v1",
   139  				Namespace:       "myns",
   140  				Resource:        "jobs",
   141  				FieldSelectorRequirements: fields.Requirements{
   142  					fields.OneTermEqualSelector("foo", "bar").Requirements()[0],
   143  				},
   144  			},
   145  			EnableAuthorizationSelector: true,
   146  		},
   147  		"enabled, bad field selector": {
   148  			Verb: "GET",
   149  			Path: "/apis/batch/v1/namespaces/myns/jobs?fieldSelector=%2Abar",
   150  			ExpectedAttributes: &authorizer.AttributesRecord{
   151  				Verb:                    "list",
   152  				Path:                    "/apis/batch/v1/namespaces/myns/jobs",
   153  				ResourceRequest:         true,
   154  				APIGroup:                batch.GroupName,
   155  				APIVersion:              "v1",
   156  				Namespace:               "myns",
   157  				Resource:                "jobs",
   158  				FieldSelectorParsingErr: errors.New("invalid selector: '*bar'; can't understand '*bar'"),
   159  			},
   160  			EnableAuthorizationSelector: true,
   161  		},
   162  		"disabled, ignore good label selector": {
   163  			Verb: "GET",
   164  			Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector%=foo%3Dbar",
   165  			ExpectedAttributes: &authorizer.AttributesRecord{
   166  				Verb:            "list",
   167  				Path:            "/apis/batch/v1/namespaces/myns/jobs",
   168  				ResourceRequest: true,
   169  				APIGroup:        batch.GroupName,
   170  				APIVersion:      "v1",
   171  				Namespace:       "myns",
   172  				Resource:        "jobs",
   173  			},
   174  		},
   175  		"enabled, good label selector": {
   176  			Verb: "GET",
   177  			Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=foo%3D%3Dbar",
   178  			ExpectedAttributes: &authorizer.AttributesRecord{
   179  				Verb:            "list",
   180  				Path:            "/apis/batch/v1/namespaces/myns/jobs",
   181  				ResourceRequest: true,
   182  				APIGroup:        batch.GroupName,
   183  				APIVersion:      "v1",
   184  				Namespace:       "myns",
   185  				Resource:        "jobs",
   186  				LabelSelectorRequirements: labels.Requirements{
   187  					*basicLabelRequirement,
   188  				},
   189  			},
   190  			EnableAuthorizationSelector: true,
   191  		},
   192  		"enabled, bad label selector": {
   193  			Verb: "GET",
   194  			Path: "/apis/batch/v1/namespaces/myns/jobs?labelSelector=%2Abar",
   195  			ExpectedAttributes: &authorizer.AttributesRecord{
   196  				Verb:                    "list",
   197  				Path:                    "/apis/batch/v1/namespaces/myns/jobs",
   198  				ResourceRequest:         true,
   199  				APIGroup:                batch.GroupName,
   200  				APIVersion:              "v1",
   201  				Namespace:               "myns",
   202  				Resource:                "jobs",
   203  				LabelSelectorParsingErr: errors.New("unable to parse requirement: <nil>: Invalid value: \"*bar\": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
   204  			},
   205  			EnableAuthorizationSelector: true,
   206  		},
   207  	}
   208  
   209  	for k, tc := range testcases {
   210  		t.Run(k, func(t *testing.T) {
   211  			if tc.EnableAuthorizationSelector {
   212  				featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
   213  			}
   214  
   215  			req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
   216  			req.RemoteAddr = "127.0.0.1"
   217  
   218  			var attribs authorizer.Attributes
   219  			var err error
   220  			var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   221  				ctx := req.Context()
   222  				attribs, err = GetAuthorizerAttributes(ctx)
   223  			})
   224  			handler = WithRequestInfo(handler, newTestRequestInfoResolver())
   225  			handler.ServeHTTP(httptest.NewRecorder(), req)
   226  
   227  			if err != nil {
   228  				t.Errorf("%s: unexpected error: %v", k, err)
   229  			} else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
   230  				t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs)
   231  			}
   232  		})
   233  	}
   234  }
   235  
   236  type fakeAuthorizer struct {
   237  	decision authorizer.Decision
   238  	reason   string
   239  	err      error
   240  }
   241  
   242  func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
   243  	return f.decision, f.reason, f.err
   244  }
   245  
   246  func TestAuditAnnotation(t *testing.T) {
   247  	testcases := map[string]struct {
   248  		authorizer         fakeAuthorizer
   249  		decisionAnnotation string
   250  		reasonAnnotation   string
   251  	}{
   252  		"decision allow": {
   253  			fakeAuthorizer{
   254  				authorizer.DecisionAllow,
   255  				"RBAC: allowed to patch pod",
   256  				nil,
   257  			},
   258  			"allow",
   259  			"RBAC: allowed to patch pod",
   260  		},
   261  		"decision forbid": {
   262  			fakeAuthorizer{
   263  				authorizer.DecisionDeny,
   264  				"RBAC: not allowed to patch pod",
   265  				nil,
   266  			},
   267  			"forbid",
   268  			"RBAC: not allowed to patch pod",
   269  		},
   270  		"error": {
   271  			fakeAuthorizer{
   272  				authorizer.DecisionNoOpinion,
   273  				"",
   274  				errors.New("can't parse user info"),
   275  			},
   276  			"",
   277  			reasonError,
   278  		},
   279  	}
   280  
   281  	scheme := runtime.NewScheme()
   282  	negotiatedSerializer := serializer.NewCodecFactory(scheme).WithoutConversion()
   283  	for k, tc := range testcases {
   284  		handler := WithAuthorization(&fakeHTTPHandler{}, tc.authorizer, negotiatedSerializer)
   285  		// TODO: fake audit injector
   286  
   287  		req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil)
   288  		req = withTestContext(req, nil, &auditinternal.Event{Level: auditinternal.LevelMetadata})
   289  		ae := audit.AuditEventFrom(req.Context())
   290  		req.RemoteAddr = "127.0.0.1"
   291  		handler.ServeHTTP(httptest.NewRecorder(), req)
   292  		assert.Equal(t, tc.decisionAnnotation, ae.Annotations[decisionAnnotationKey], k+": unexpected decision annotation")
   293  		assert.Equal(t, tc.reasonAnnotation, ae.Annotations[reasonAnnotationKey], k+": unexpected reason annotation")
   294  	}
   295  
   296  }