k8s.io/apiserver@v0.31.1/pkg/authentication/request/headerrequest/requestheader_controller_test.go (about)

     1  /*
     2  Copyright 2020 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 headerrequest
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"k8s.io/apimachinery/pkg/api/equality"
    23  	"testing"
    24  
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/client-go/kubernetes/fake"
    28  	corev1listers "k8s.io/client-go/listers/core/v1"
    29  	"k8s.io/client-go/tools/cache"
    30  )
    31  
    32  const (
    33  	defConfigMapName      = "extension-apiserver-authentication"
    34  	defConfigMapNamespace = "kube-system"
    35  
    36  	defUsernameHeadersKey     = "user-key"
    37  	defGroupHeadersKey        = "group-key"
    38  	defExtraHeaderPrefixesKey = "extra-key"
    39  	defAllowedClientNamesKey  = "names-key"
    40  )
    41  
    42  type expectedHeadersHolder struct {
    43  	usernameHeaders     []string
    44  	groupHeaders        []string
    45  	extraHeaderPrefixes []string
    46  	allowedClientNames  []string
    47  }
    48  
    49  func TestRequestHeaderAuthRequestController(t *testing.T) {
    50  	scenarios := []struct {
    51  		name           string
    52  		cm             *corev1.ConfigMap
    53  		expectedHeader expectedHeadersHolder
    54  		expectErr      bool
    55  	}{
    56  		{
    57  			name: "happy-path: headers values are populated form a config map",
    58  			cm:   defaultConfigMap(t, []string{"user-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}),
    59  			expectedHeader: expectedHeadersHolder{
    60  				usernameHeaders:     []string{"user-val"},
    61  				groupHeaders:        []string{"group-val"},
    62  				extraHeaderPrefixes: []string{"extra-val"},
    63  				allowedClientNames:  []string{"names-val"},
    64  			},
    65  		},
    66  		{
    67  			name: "passing an empty config map doesn't break the controller",
    68  			cm: func() *corev1.ConfigMap {
    69  				c := defaultConfigMap(t, nil, nil, nil, nil)
    70  				c.Data = map[string]string{}
    71  				return c
    72  			}(),
    73  		},
    74  		{
    75  			name: "an invalid config map produces an error",
    76  			cm: func() *corev1.ConfigMap {
    77  				c := defaultConfigMap(t, nil, nil, nil, nil)
    78  				c.Data = map[string]string{
    79  					defUsernameHeadersKey: "incorrect-json-array",
    80  				}
    81  				return c
    82  			}(),
    83  			expectErr: true,
    84  		},
    85  	}
    86  
    87  	for _, scenario := range scenarios {
    88  		t.Run(scenario.name, func(t *testing.T) {
    89  			// test data
    90  			indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
    91  			if err := indexer.Add(scenario.cm); err != nil {
    92  				t.Fatal(err.Error())
    93  			}
    94  			target := newDefaultTarget()
    95  			target.configmapLister = corev1listers.NewConfigMapLister(indexer).ConfigMaps(defConfigMapNamespace)
    96  
    97  			// act
    98  			err := target.sync()
    99  
   100  			if err != nil && !scenario.expectErr {
   101  				t.Errorf("got unexpected error %v", err)
   102  			}
   103  			if err == nil && scenario.expectErr {
   104  				t.Error("expected an error but didn't get one")
   105  			}
   106  
   107  			// validate
   108  			validateExpectedHeaders(t, target, scenario.expectedHeader)
   109  		})
   110  	}
   111  }
   112  
   113  func TestRequestHeaderAuthRequestControllerPreserveState(t *testing.T) {
   114  	scenarios := []struct {
   115  		name           string
   116  		cm             *corev1.ConfigMap
   117  		expectedHeader expectedHeadersHolder
   118  		expectErr      bool
   119  	}{
   120  		{
   121  			name: "scenario 1: headers values are populated form a config map",
   122  			cm:   defaultConfigMap(t, []string{"user-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}),
   123  			expectedHeader: expectedHeadersHolder{
   124  				usernameHeaders:     []string{"user-val"},
   125  				groupHeaders:        []string{"group-val"},
   126  				extraHeaderPrefixes: []string{"extra-val"},
   127  				allowedClientNames:  []string{"names-val"},
   128  			},
   129  		},
   130  		{
   131  			name: "scenario 2: an invalid config map produces an error but doesn't destroy the state (scenario 1)",
   132  			cm: func() *corev1.ConfigMap {
   133  				c := defaultConfigMap(t, nil, nil, nil, nil)
   134  				c.Data = map[string]string{
   135  					defUsernameHeadersKey: "incorrect-json-array",
   136  				}
   137  				return c
   138  			}(),
   139  			expectErr: true,
   140  			expectedHeader: expectedHeadersHolder{
   141  				usernameHeaders:     []string{"user-val"},
   142  				groupHeaders:        []string{"group-val"},
   143  				extraHeaderPrefixes: []string{"extra-val"},
   144  				allowedClientNames:  []string{"names-val"},
   145  			},
   146  		},
   147  		{
   148  			name: "scenario 3: some headers values have changed (prev set by scenario 1)",
   149  			cm:   defaultConfigMap(t, []string{"user-val"}, []string{"group-val-scenario-3"}, []string{"extra-val"}, []string{"names-val"}),
   150  			expectedHeader: expectedHeadersHolder{
   151  				usernameHeaders:     []string{"user-val"},
   152  				groupHeaders:        []string{"group-val-scenario-3"},
   153  				extraHeaderPrefixes: []string{"extra-val"},
   154  				allowedClientNames:  []string{"names-val"},
   155  			},
   156  		},
   157  		{
   158  			name: "scenario 4: all headers values have changed (prev set by scenario 3)",
   159  			cm:   defaultConfigMap(t, []string{"user-val-scenario-4"}, []string{"group-val-scenario-4"}, []string{"extra-val-scenario-4"}, []string{"names-val-scenario-4"}),
   160  			expectedHeader: expectedHeadersHolder{
   161  				usernameHeaders:     []string{"user-val-scenario-4"},
   162  				groupHeaders:        []string{"group-val-scenario-4"},
   163  				extraHeaderPrefixes: []string{"extra-val-scenario-4"},
   164  				allowedClientNames:  []string{"names-val-scenario-4"},
   165  			},
   166  		},
   167  	}
   168  
   169  	target := newDefaultTarget()
   170  
   171  	for _, scenario := range scenarios {
   172  		t.Run(scenario.name, func(t *testing.T) {
   173  			// test data
   174  			if scenario.cm != nil {
   175  				indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
   176  				if err := indexer.Add(scenario.cm); err != nil {
   177  					t.Fatal(err.Error())
   178  				}
   179  				target.configmapLister = corev1listers.NewConfigMapLister(indexer).ConfigMaps(defConfigMapNamespace)
   180  			}
   181  
   182  			// act
   183  			err := target.sync()
   184  
   185  			if err != nil && !scenario.expectErr {
   186  				t.Errorf("got unexpected error %v", err)
   187  			}
   188  			if err == nil && scenario.expectErr {
   189  				t.Error("expected an error but didn't get one")
   190  			}
   191  
   192  			// validate
   193  			validateExpectedHeaders(t, target, scenario.expectedHeader)
   194  		})
   195  	}
   196  }
   197  
   198  func TestRequestHeaderAuthRequestControllerSyncOnce(t *testing.T) {
   199  	scenarios := []struct {
   200  		name           string
   201  		cm             *corev1.ConfigMap
   202  		expectedHeader expectedHeadersHolder
   203  		expectErr      bool
   204  	}{
   205  		{
   206  			name: "headers values are populated form a config map",
   207  			cm:   defaultConfigMap(t, []string{"user-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}),
   208  			expectedHeader: expectedHeadersHolder{
   209  				usernameHeaders:     []string{"user-val"},
   210  				groupHeaders:        []string{"group-val"},
   211  				extraHeaderPrefixes: []string{"extra-val"},
   212  				allowedClientNames:  []string{"names-val"},
   213  			},
   214  		},
   215  	}
   216  
   217  	for _, scenario := range scenarios {
   218  		t.Run(scenario.name, func(t *testing.T) {
   219  			// test data
   220  			target := newDefaultTarget()
   221  			fakeKubeClient := fake.NewSimpleClientset(scenario.cm)
   222  			target.client = fakeKubeClient
   223  
   224  			// act
   225  			ctx := context.TODO()
   226  			err := target.RunOnce(ctx)
   227  
   228  			if err != nil && !scenario.expectErr {
   229  				t.Errorf("got unexpected error %v", err)
   230  			}
   231  			if err == nil && scenario.expectErr {
   232  				t.Error("expected an error but didn't get one")
   233  			}
   234  
   235  			// validate
   236  			validateExpectedHeaders(t, target, scenario.expectedHeader)
   237  		})
   238  	}
   239  }
   240  
   241  func defaultConfigMap(t *testing.T, usernameHeaderVal, groupHeadersVal, extraHeaderPrefixesVal, allowedClientNamesVal []string) *corev1.ConfigMap {
   242  	encode := func(val []string) string {
   243  		encodedVal, err := json.Marshal(val)
   244  		if err != nil {
   245  			t.Fatalf("unable to marshal %q , due to %v", usernameHeaderVal, err)
   246  		}
   247  		return string(encodedVal)
   248  	}
   249  	return &corev1.ConfigMap{
   250  		ObjectMeta: metav1.ObjectMeta{
   251  			Name:      defConfigMapName,
   252  			Namespace: defConfigMapNamespace,
   253  		},
   254  		Data: map[string]string{
   255  			defUsernameHeadersKey:     encode(usernameHeaderVal),
   256  			defGroupHeadersKey:        encode(groupHeadersVal),
   257  			defExtraHeaderPrefixesKey: encode(extraHeaderPrefixesVal),
   258  			defAllowedClientNamesKey:  encode(allowedClientNamesVal),
   259  		},
   260  	}
   261  }
   262  
   263  func newDefaultTarget() *RequestHeaderAuthRequestController {
   264  	return &RequestHeaderAuthRequestController{
   265  		configmapName:          defConfigMapName,
   266  		configmapNamespace:     defConfigMapNamespace,
   267  		usernameHeadersKey:     defUsernameHeadersKey,
   268  		groupHeadersKey:        defGroupHeadersKey,
   269  		extraHeaderPrefixesKey: defExtraHeaderPrefixesKey,
   270  		allowedClientNamesKey:  defAllowedClientNamesKey,
   271  	}
   272  }
   273  
   274  func validateExpectedHeaders(t *testing.T, target *RequestHeaderAuthRequestController, expected expectedHeadersHolder) {
   275  	if !equality.Semantic.DeepEqual(target.UsernameHeaders(), expected.usernameHeaders) {
   276  		t.Fatalf("incorrect usernameHeaders, got %v, wanted %v", target.UsernameHeaders(), expected.usernameHeaders)
   277  	}
   278  	if !equality.Semantic.DeepEqual(target.GroupHeaders(), expected.groupHeaders) {
   279  		t.Fatalf("incorrect groupHeaders, got %v, wanted %v", target.GroupHeaders(), expected.groupHeaders)
   280  	}
   281  	if !equality.Semantic.DeepEqual(target.ExtraHeaderPrefixes(), expected.extraHeaderPrefixes) {
   282  		t.Fatalf("incorrect extraheaderPrefixes, got %v, wanted %v", target.ExtraHeaderPrefixes(), expected.extraHeaderPrefixes)
   283  	}
   284  	if !equality.Semantic.DeepEqual(target.AllowedClientNames(), expected.allowedClientNames) {
   285  		t.Fatalf("incorrect expectedAllowedClientNames, got %v, wanted %v", target.AllowedClientNames(), expected.allowedClientNames)
   286  	}
   287  }