k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/validating/caching_authorizer_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 validating
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"testing"
    24  
    25  	"k8s.io/apimachinery/pkg/fields"
    26  	"k8s.io/apimachinery/pkg/labels"
    27  	"k8s.io/apiserver/pkg/authentication/user"
    28  	"k8s.io/apiserver/pkg/authorization/authorizer"
    29  	genericfeatures "k8s.io/apiserver/pkg/features"
    30  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    31  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    32  )
    33  
    34  func mustParseLabelSelector(str string) labels.Requirements {
    35  	ret, err := labels.Parse(str)
    36  	if err != nil {
    37  		panic(err)
    38  	}
    39  	retRequirements, _ /*selectable*/ := ret.Requirements()
    40  	return retRequirements
    41  }
    42  
    43  func TestCachingAuthorizer(t *testing.T) {
    44  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
    45  
    46  	type result struct {
    47  		decision authorizer.Decision
    48  		reason   string
    49  		error    error
    50  	}
    51  
    52  	type invocation struct {
    53  		attributes authorizer.Attributes
    54  		expected   result
    55  	}
    56  
    57  	for _, tc := range []struct {
    58  		name    string
    59  		calls   []invocation
    60  		backend []result
    61  	}{
    62  		{
    63  			name: "hit",
    64  			calls: []invocation{
    65  				{
    66  					attributes: authorizer.AttributesRecord{Name: "test name"},
    67  					expected: result{
    68  						decision: authorizer.DecisionAllow,
    69  						reason:   "test reason",
    70  						error:    fmt.Errorf("test error"),
    71  					},
    72  				},
    73  				{
    74  					attributes: authorizer.AttributesRecord{Name: "test name"},
    75  					expected: result{
    76  						decision: authorizer.DecisionAllow,
    77  						reason:   "test reason",
    78  						error:    fmt.Errorf("test error"),
    79  					},
    80  				},
    81  			},
    82  			backend: []result{
    83  				{
    84  					decision: authorizer.DecisionAllow,
    85  					reason:   "test reason",
    86  					error:    fmt.Errorf("test error"),
    87  				},
    88  			},
    89  		},
    90  		{
    91  			name: "hit with differently-ordered groups",
    92  			calls: []invocation{
    93  				{
    94  					attributes: authorizer.AttributesRecord{
    95  						User: &user.DefaultInfo{
    96  							Groups: []string{"a", "b", "c"},
    97  						},
    98  					},
    99  					expected: result{
   100  						decision: authorizer.DecisionAllow,
   101  						reason:   "test reason",
   102  						error:    fmt.Errorf("test error"),
   103  					},
   104  				},
   105  				{
   106  					attributes: authorizer.AttributesRecord{
   107  						User: &user.DefaultInfo{
   108  							Groups: []string{"c", "b", "a"},
   109  						},
   110  					},
   111  					expected: result{
   112  						decision: authorizer.DecisionAllow,
   113  						reason:   "test reason",
   114  						error:    fmt.Errorf("test error"),
   115  					},
   116  				},
   117  			},
   118  			backend: []result{
   119  				{
   120  					decision: authorizer.DecisionAllow,
   121  					reason:   "test reason",
   122  					error:    fmt.Errorf("test error"),
   123  				},
   124  			},
   125  		},
   126  		{
   127  			name: "hit with differently-ordered extra",
   128  			calls: []invocation{
   129  				{
   130  					attributes: authorizer.AttributesRecord{
   131  						User: &user.DefaultInfo{
   132  							Extra: map[string][]string{
   133  								"k": {"a", "b", "c"},
   134  							},
   135  						},
   136  					},
   137  					expected: result{
   138  						decision: authorizer.DecisionAllow,
   139  						reason:   "test reason",
   140  						error:    fmt.Errorf("test error"),
   141  					},
   142  				},
   143  				{
   144  					attributes: authorizer.AttributesRecord{
   145  						User: &user.DefaultInfo{
   146  							Extra: map[string][]string{
   147  								"k": {"c", "b", "a"},
   148  							},
   149  						},
   150  					},
   151  					expected: result{
   152  						decision: authorizer.DecisionAllow,
   153  						reason:   "test reason",
   154  						error:    fmt.Errorf("test error"),
   155  					},
   156  				},
   157  			},
   158  			backend: []result{
   159  				{
   160  					decision: authorizer.DecisionAllow,
   161  					reason:   "test reason",
   162  					error:    fmt.Errorf("test error"),
   163  				},
   164  			},
   165  		},
   166  		{
   167  			name: "miss due to different name",
   168  			calls: []invocation{
   169  				{
   170  					attributes: authorizer.AttributesRecord{Name: "alpha"},
   171  					expected: result{
   172  						decision: authorizer.DecisionAllow,
   173  						reason:   "test reason alpha",
   174  						error:    fmt.Errorf("test error alpha"),
   175  					},
   176  				},
   177  				{
   178  					attributes: authorizer.AttributesRecord{Name: "beta"},
   179  					expected: result{
   180  						decision: authorizer.DecisionDeny,
   181  						reason:   "test reason beta",
   182  						error:    fmt.Errorf("test error beta"),
   183  					},
   184  				},
   185  			},
   186  			backend: []result{
   187  				{
   188  					decision: authorizer.DecisionAllow,
   189  					reason:   "test reason alpha",
   190  					error:    fmt.Errorf("test error alpha"),
   191  				},
   192  				{
   193  					decision: authorizer.DecisionDeny,
   194  					reason:   "test reason beta",
   195  					error:    fmt.Errorf("test error beta"),
   196  				},
   197  			},
   198  		},
   199  		{
   200  			name: "miss due to different user",
   201  			calls: []invocation{
   202  				{
   203  					attributes: authorizer.AttributesRecord{
   204  						User: &user.DefaultInfo{Name: "alpha"},
   205  					},
   206  					expected: result{
   207  						decision: authorizer.DecisionAllow,
   208  						reason:   "test reason alpha",
   209  						error:    fmt.Errorf("test error alpha"),
   210  					},
   211  				},
   212  				{
   213  					attributes: authorizer.AttributesRecord{
   214  						User: &user.DefaultInfo{Name: "beta"},
   215  					},
   216  					expected: result{
   217  						decision: authorizer.DecisionDeny,
   218  						reason:   "test reason beta",
   219  						error:    fmt.Errorf("test error beta"),
   220  					},
   221  				},
   222  			},
   223  			backend: []result{
   224  				{
   225  					decision: authorizer.DecisionAllow,
   226  					reason:   "test reason alpha",
   227  					error:    fmt.Errorf("test error alpha"),
   228  				},
   229  				{
   230  					decision: authorizer.DecisionDeny,
   231  					reason:   "test reason beta",
   232  					error:    fmt.Errorf("test error beta"),
   233  				},
   234  			},
   235  		},
   236  		{
   237  			name: "honor good field selector",
   238  			calls: []invocation{
   239  				{
   240  					attributes: authorizer.AttributesRecord{
   241  						Name:                      "test name",
   242  						FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(),
   243  					},
   244  					expected: result{
   245  						decision: authorizer.DecisionAllow,
   246  						reason:   "test reason",
   247  						error:    fmt.Errorf("test error"),
   248  					},
   249  				},
   250  				{
   251  					attributes: authorizer.AttributesRecord{
   252  						Name: "test name",
   253  					},
   254  					expected: result{
   255  						decision: authorizer.DecisionAllow,
   256  						reason:   "test reason 2",
   257  						error:    fmt.Errorf("test error 2"),
   258  					},
   259  				},
   260  				{
   261  					// now this should be cached
   262  					attributes: authorizer.AttributesRecord{
   263  						Name:                      "test name",
   264  						FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(),
   265  					},
   266  					expected: result{
   267  						decision: authorizer.DecisionAllow,
   268  						reason:   "test reason",
   269  						error:    fmt.Errorf("test error"),
   270  					},
   271  				},
   272  			},
   273  			backend: []result{
   274  				{
   275  					decision: authorizer.DecisionAllow,
   276  					reason:   "test reason",
   277  					error:    fmt.Errorf("test error"),
   278  				},
   279  				{
   280  					decision: authorizer.DecisionAllow,
   281  					reason:   "test reason 2",
   282  					error:    fmt.Errorf("test error 2"),
   283  				},
   284  			},
   285  		},
   286  		{
   287  			name: "ignore malformed field selector first",
   288  			calls: []invocation{
   289  				{
   290  					attributes: authorizer.AttributesRecord{
   291  						Name:                    "test name",
   292  						FieldSelectorParsingErr: errors.New("malformed"),
   293  					},
   294  					expected: result{
   295  						decision: authorizer.DecisionAllow,
   296  						reason:   "test reason",
   297  						error:    fmt.Errorf("test error"),
   298  					},
   299  				},
   300  				{
   301  					// notice that this does not have the malformed field selector.
   302  					// it should use the cached result
   303  					attributes: authorizer.AttributesRecord{
   304  						Name: "test name",
   305  					},
   306  					expected: result{
   307  						decision: authorizer.DecisionAllow,
   308  						reason:   "test reason",
   309  						error:    fmt.Errorf("test error"),
   310  					},
   311  				},
   312  			},
   313  			backend: []result{
   314  				{
   315  					decision: authorizer.DecisionAllow,
   316  					reason:   "test reason",
   317  					error:    fmt.Errorf("test error"),
   318  				},
   319  			},
   320  		},
   321  		{
   322  			name: "ignore malformed field selector second",
   323  			calls: []invocation{
   324  				{
   325  					attributes: authorizer.AttributesRecord{
   326  						Name: "test name",
   327  					},
   328  					expected: result{
   329  						decision: authorizer.DecisionAllow,
   330  						reason:   "test reason",
   331  						error:    fmt.Errorf("test error"),
   332  					},
   333  				},
   334  				{
   335  					// this should use the broader cached value because the selector will be ignored
   336  					attributes: authorizer.AttributesRecord{
   337  						Name:                    "test name",
   338  						FieldSelectorParsingErr: errors.New("malformed"),
   339  					},
   340  					expected: result{
   341  						decision: authorizer.DecisionAllow,
   342  						reason:   "test reason",
   343  						error:    fmt.Errorf("test error"),
   344  					},
   345  				},
   346  			},
   347  			backend: []result{
   348  				{
   349  					decision: authorizer.DecisionAllow,
   350  					reason:   "test reason",
   351  					error:    fmt.Errorf("test error"),
   352  				},
   353  			},
   354  		},
   355  
   356  		{
   357  			name: "honor good label selector",
   358  			calls: []invocation{
   359  				{
   360  					attributes: authorizer.AttributesRecord{
   361  						Name:                      "test name",
   362  						LabelSelectorRequirements: mustParseLabelSelector("foo=bar"),
   363  					},
   364  					expected: result{
   365  						decision: authorizer.DecisionAllow,
   366  						reason:   "test reason",
   367  						error:    fmt.Errorf("test error"),
   368  					},
   369  				},
   370  				{
   371  					attributes: authorizer.AttributesRecord{
   372  						Name: "test name",
   373  					},
   374  					expected: result{
   375  						decision: authorizer.DecisionAllow,
   376  						reason:   "test reason 2",
   377  						error:    fmt.Errorf("test error 2"),
   378  					},
   379  				},
   380  				{
   381  					// now this should be cached
   382  					attributes: authorizer.AttributesRecord{
   383  						Name:                      "test name",
   384  						LabelSelectorRequirements: mustParseLabelSelector("foo=bar"),
   385  					},
   386  					expected: result{
   387  						decision: authorizer.DecisionAllow,
   388  						reason:   "test reason",
   389  						error:    fmt.Errorf("test error"),
   390  					},
   391  				},
   392  				{
   393  					attributes: authorizer.AttributesRecord{
   394  						Name:                      "test name",
   395  						LabelSelectorRequirements: mustParseLabelSelector("diff=zero"),
   396  					},
   397  					expected: result{
   398  						decision: authorizer.DecisionAllow,
   399  						reason:   "test reason 3",
   400  						error:    fmt.Errorf("test error 3"),
   401  					},
   402  				},
   403  			},
   404  			backend: []result{
   405  				{
   406  					decision: authorizer.DecisionAllow,
   407  					reason:   "test reason",
   408  					error:    fmt.Errorf("test error"),
   409  				},
   410  				{
   411  					decision: authorizer.DecisionAllow,
   412  					reason:   "test reason 2",
   413  					error:    fmt.Errorf("test error 2"),
   414  				},
   415  				{
   416  					decision: authorizer.DecisionAllow,
   417  					reason:   "test reason 3",
   418  					error:    fmt.Errorf("test error 3"),
   419  				},
   420  			},
   421  		},
   422  		{
   423  			name: "ignore malformed label selector first",
   424  			calls: []invocation{
   425  				{
   426  					attributes: authorizer.AttributesRecord{
   427  						Name:                    "test name",
   428  						LabelSelectorParsingErr: errors.New("malformed mess"),
   429  					},
   430  					expected: result{
   431  						decision: authorizer.DecisionAllow,
   432  						reason:   "test reason",
   433  						error:    fmt.Errorf("test error"),
   434  					},
   435  				},
   436  				{
   437  					// notice that this does not have the malformed field selector.
   438  					// it should use the cached result
   439  					attributes: authorizer.AttributesRecord{
   440  						Name: "test name",
   441  					},
   442  					expected: result{
   443  						decision: authorizer.DecisionAllow,
   444  						reason:   "test reason",
   445  						error:    fmt.Errorf("test error"),
   446  					},
   447  				},
   448  			},
   449  			backend: []result{
   450  				{
   451  					decision: authorizer.DecisionAllow,
   452  					reason:   "test reason",
   453  					error:    fmt.Errorf("test error"),
   454  				},
   455  			},
   456  		},
   457  		{
   458  			name: "ignore malformed label selector second",
   459  			calls: []invocation{
   460  				{
   461  					attributes: authorizer.AttributesRecord{
   462  						Name: "test name",
   463  					},
   464  					expected: result{
   465  						decision: authorizer.DecisionAllow,
   466  						reason:   "test reason",
   467  						error:    fmt.Errorf("test error"),
   468  					},
   469  				},
   470  				{
   471  					// this should use the broader cached value because the selector will be ignored
   472  					attributes: authorizer.AttributesRecord{
   473  						Name:                    "test name",
   474  						LabelSelectorParsingErr: errors.New("malformed mess"),
   475  					},
   476  					expected: result{
   477  						decision: authorizer.DecisionAllow,
   478  						reason:   "test reason",
   479  						error:    fmt.Errorf("test error"),
   480  					},
   481  				},
   482  			},
   483  			backend: []result{
   484  				{
   485  					decision: authorizer.DecisionAllow,
   486  					reason:   "test reason",
   487  					error:    fmt.Errorf("test error"),
   488  				},
   489  			},
   490  		},
   491  	} {
   492  		t.Run(tc.name, func(t *testing.T) {
   493  			var misses int
   494  			frontend := newCachingAuthorizer(func() authorizer.Authorizer {
   495  				return authorizer.AuthorizerFunc(func(_ context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
   496  					if misses >= len(tc.backend) {
   497  						t.Fatalf("got more than expected %d backend invocations", len(tc.backend))
   498  					}
   499  					result := tc.backend[misses]
   500  					misses++
   501  					return result.decision, result.reason, result.error
   502  				})
   503  			}())
   504  
   505  			for i, invocation := range tc.calls {
   506  				decision, reason, err := frontend.Authorize(context.TODO(), invocation.attributes)
   507  				if decision != invocation.expected.decision {
   508  					t.Errorf("(call %d of %d) expected decision %v, got %v", i+1, len(tc.calls), invocation.expected.decision, decision)
   509  				}
   510  				if reason != invocation.expected.reason {
   511  					t.Errorf("(call %d of %d) expected reason %q, got %q", i+1, len(tc.calls), invocation.expected.reason, reason)
   512  				}
   513  				if err.Error() != invocation.expected.error.Error() {
   514  					t.Errorf("(call %d of %d) expected error %q, got %q", i+1, len(tc.calls), invocation.expected.error.Error(), err.Error())
   515  				}
   516  			}
   517  
   518  			if len(tc.backend) > misses {
   519  				t.Errorf("expected %d backend invocations, got %d", len(tc.backend), misses)
   520  			}
   521  		})
   522  	}
   523  }