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  }