1/*
2Copyright 2016 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package webhook
18
19import (
20	"context"
21	"crypto/tls"
22	"crypto/x509"
23	"encoding/json"
24	"fmt"
25	"io/ioutil"
26	"net/http"
27	"net/http/httptest"
28	"net/url"
29	"os"
30	"reflect"
31	"testing"
32	"time"
33
34	authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
35	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36	"k8s.io/apiserver/pkg/authentication/authenticator"
37	"k8s.io/apiserver/pkg/authentication/token/cache"
38	"k8s.io/apiserver/pkg/authentication/user"
39	v1 "k8s.io/client-go/tools/clientcmd/api/v1"
40)
41
42var apiAuds = authenticator.Audiences{"api"}
43
44// V1beta1Service mocks a remote authentication service.
45type V1beta1Service interface {
46	// Review looks at the TokenReviewSpec and provides an authentication
47	// response in the TokenReviewStatus.
48	Review(*authenticationv1beta1.TokenReview)
49	HTTPStatusCode() int
50}
51
52// NewV1beta1TestServer wraps a V1beta1Service as an httptest.Server.
53func NewV1beta1TestServer(s V1beta1Service, cert, key, caCert []byte) (*httptest.Server, error) {
54	const webhookPath = "/testserver"
55	var tlsConfig *tls.Config
56	if cert != nil {
57		cert, err := tls.X509KeyPair(cert, key)
58		if err != nil {
59			return nil, err
60		}
61		tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
62	}
63
64	if caCert != nil {
65		rootCAs := x509.NewCertPool()
66		rootCAs.AppendCertsFromPEM(caCert)
67		if tlsConfig == nil {
68			tlsConfig = &tls.Config{}
69		}
70		tlsConfig.ClientCAs = rootCAs
71		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
72	}
73
74	serveHTTP := func(w http.ResponseWriter, r *http.Request) {
75		if r.Method != "POST" {
76			http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed)
77			return
78		}
79		if r.URL.Path != webhookPath {
80			http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound)
81			return
82		}
83
84		var review authenticationv1beta1.TokenReview
85		bodyData, _ := ioutil.ReadAll(r.Body)
86		if err := json.Unmarshal(bodyData, &review); err != nil {
87			http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
88			return
89		}
90		// ensure we received the serialized tokenreview as expected
91		if review.APIVersion != "authentication.k8s.io/v1beta1" {
92			http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
93			return
94		}
95		// once we have a successful request, always call the review to record that we were called
96		s.Review(&review)
97		if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 {
98			http.Error(w, "HTTP Error", s.HTTPStatusCode())
99			return
100		}
101		type userInfo struct {
102			Username string              `json:"username"`
103			UID      string              `json:"uid"`
104			Groups   []string            `json:"groups"`
105			Extra    map[string][]string `json:"extra"`
106		}
107		type status struct {
108			Authenticated bool     `json:"authenticated"`
109			User          userInfo `json:"user"`
110			Audiences     []string `json:"audiences"`
111		}
112
113		var extra map[string][]string
114		if review.Status.User.Extra != nil {
115			extra = map[string][]string{}
116			for k, v := range review.Status.User.Extra {
117				extra[k] = v
118			}
119		}
120
121		resp := struct {
122			Kind       string `json:"kind"`
123			APIVersion string `json:"apiVersion"`
124			Status     status `json:"status"`
125		}{
126			Kind:       "TokenReview",
127			APIVersion: authenticationv1beta1.SchemeGroupVersion.String(),
128			Status: status{
129				review.Status.Authenticated,
130				userInfo{
131					Username: review.Status.User.Username,
132					UID:      review.Status.User.UID,
133					Groups:   review.Status.User.Groups,
134					Extra:    extra,
135				},
136				review.Status.Audiences,
137			},
138		}
139		w.Header().Set("Content-Type", "application/json")
140		json.NewEncoder(w).Encode(resp)
141	}
142
143	server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
144	server.TLS = tlsConfig
145	server.StartTLS()
146
147	// Adjust the path to point to our custom path
148	serverURL, _ := url.Parse(server.URL)
149	serverURL.Path = webhookPath
150	server.URL = serverURL.String()
151
152	return server, nil
153}
154
155// A service that can be set to say yes or no to authentication requests.
156type mockV1beta1Service struct {
157	allow      bool
158	statusCode int
159	called     int
160}
161
162func (m *mockV1beta1Service) Review(r *authenticationv1beta1.TokenReview) {
163	m.called++
164	r.Status.Authenticated = m.allow
165	if m.allow {
166		r.Status.User.Username = "realHooman@email.com"
167	}
168}
169func (m *mockV1beta1Service) Allow()              { m.allow = true }
170func (m *mockV1beta1Service) Deny()               { m.allow = false }
171func (m *mockV1beta1Service) HTTPStatusCode() int { return m.statusCode }
172
173// newV1beta1TokenAuthenticator creates a temporary kubeconfig file from the provided
174// arguments and attempts to load a new WebhookTokenAuthenticator from it.
175func newV1beta1TokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
176	tempfile, err := ioutil.TempFile("", "")
177	if err != nil {
178		return nil, err
179	}
180	p := tempfile.Name()
181	defer os.Remove(p)
182	config := v1.Config{
183		Clusters: []v1.NamedCluster{
184			{
185				Cluster: v1.Cluster{Server: serverURL, CertificateAuthorityData: ca},
186			},
187		},
188		AuthInfos: []v1.NamedAuthInfo{
189			{
190				AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
191			},
192		},
193	}
194	if err := json.NewEncoder(tempfile).Encode(config); err != nil {
195		return nil, err
196	}
197
198	c, err := tokenReviewInterfaceFromKubeconfig(p, "v1beta1", testRetryBackoff, nil)
199	if err != nil {
200		return nil, err
201	}
202
203	authn, err := newWithBackoff(c, testRetryBackoff, implicitAuds, 10*time.Second, AuthenticatorMetrics{
204		RecordRequestTotal:   noopMetrics{}.RequestTotal,
205		RecordRequestLatency: noopMetrics{}.RequestLatency,
206	})
207	if err != nil {
208		return nil, err
209	}
210
211	return cache.New(authn, false, cacheTime, cacheTime), nil
212}
213
214func TestV1beta1TLSConfig(t *testing.T) {
215	tests := []struct {
216		test                            string
217		clientCert, clientKey, clientCA []byte
218		serverCert, serverKey, serverCA []byte
219		wantErr                         bool
220	}{
221		{
222			test:       "TLS setup between client and server",
223			clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
224			serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
225		},
226		{
227			test:       "Server does not require client auth",
228			clientCA:   caCert,
229			serverCert: serverCert, serverKey: serverKey,
230		},
231		{
232			test:       "Server does not require client auth, client provides it",
233			clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
234			serverCert: serverCert, serverKey: serverKey,
235		},
236		{
237			test:       "Client does not trust server",
238			clientCert: clientCert, clientKey: clientKey,
239			serverCert: serverCert, serverKey: serverKey,
240			wantErr: true,
241		},
242		{
243			test:       "Server does not trust client",
244			clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
245			serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
246			wantErr: true,
247		},
248		{
249			// Plugin does not support insecure configurations.
250			test:    "Server is using insecure connection",
251			wantErr: true,
252		},
253	}
254	for _, tt := range tests {
255		// Use a closure so defer statements trigger between loop iterations.
256		func() {
257			service := new(mockV1beta1Service)
258			service.statusCode = 200
259
260			server, err := NewV1beta1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
261			if err != nil {
262				t.Errorf("%s: failed to create server: %v", tt.test, err)
263				return
264			}
265			defer server.Close()
266
267			wh, err := newV1beta1TokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil)
268			if err != nil {
269				t.Errorf("%s: failed to create client: %v", tt.test, err)
270				return
271			}
272
273			// Allow all and see if we get an error.
274			service.Allow()
275			_, authenticated, err := wh.AuthenticateToken(context.Background(), "t0k3n")
276			if tt.wantErr {
277				if err == nil {
278					t.Errorf("expected error making authorization request: %v", err)
279				}
280				return
281			}
282			if !authenticated {
283				t.Errorf("%s: failed to authenticate token", tt.test)
284				return
285			}
286
287			service.Deny()
288			_, authenticated, err = wh.AuthenticateToken(context.Background(), "t0k3n")
289			if err != nil {
290				t.Errorf("%s: unexpectedly failed AuthenticateToken", tt.test)
291			}
292			if authenticated {
293				t.Errorf("%s: incorrectly authenticated token", tt.test)
294			}
295		}()
296	}
297}
298
299// recorderV1beta1Service records all token review requests, and responds with the
300// provided TokenReviewStatus.
301type recorderV1beta1Service struct {
302	lastRequest authenticationv1beta1.TokenReview
303	response    authenticationv1beta1.TokenReviewStatus
304}
305
306func (rec *recorderV1beta1Service) Review(r *authenticationv1beta1.TokenReview) {
307	rec.lastRequest = *r
308	r.Status = rec.response
309}
310
311func (rec *recorderV1beta1Service) HTTPStatusCode() int { return 200 }
312
313func TestV1beta1WebhookTokenAuthenticator(t *testing.T) {
314	serv := &recorderV1beta1Service{}
315
316	s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert)
317	if err != nil {
318		t.Fatal(err)
319	}
320	defer s.Close()
321
322	expTypeMeta := metav1.TypeMeta{
323		APIVersion: "authentication.k8s.io/v1beta1",
324		Kind:       "TokenReview",
325	}
326
327	tests := []struct {
328		description           string
329		implicitAuds, reqAuds authenticator.Audiences
330		serverResponse        authenticationv1beta1.TokenReviewStatus
331		expectedAuthenticated bool
332		expectedUser          *user.DefaultInfo
333		expectedAuds          authenticator.Audiences
334	}{
335		{
336			description: "successful response should pass through all user info.",
337			serverResponse: authenticationv1beta1.TokenReviewStatus{
338				Authenticated: true,
339				User: authenticationv1beta1.UserInfo{
340					Username: "somebody",
341				},
342			},
343			expectedAuthenticated: true,
344			expectedUser: &user.DefaultInfo{
345				Name: "somebody",
346			},
347		},
348		{
349			description: "successful response should pass through all user info.",
350			serverResponse: authenticationv1beta1.TokenReviewStatus{
351				Authenticated: true,
352				User: authenticationv1beta1.UserInfo{
353					Username: "person@place.com",
354					UID:      "abcd-1234",
355					Groups:   []string{"stuff-dev", "main-eng"},
356					Extra:    map[string]authenticationv1beta1.ExtraValue{"foo": {"bar", "baz"}},
357				},
358			},
359			expectedAuthenticated: true,
360			expectedUser: &user.DefaultInfo{
361				Name:   "person@place.com",
362				UID:    "abcd-1234",
363				Groups: []string{"stuff-dev", "main-eng"},
364				Extra:  map[string][]string{"foo": {"bar", "baz"}},
365			},
366		},
367		{
368			description: "unauthenticated shouldn't even include extra provided info.",
369			serverResponse: authenticationv1beta1.TokenReviewStatus{
370				Authenticated: false,
371				User: authenticationv1beta1.UserInfo{
372					Username: "garbage",
373					UID:      "abcd-1234",
374					Groups:   []string{"not-actually-used"},
375				},
376			},
377			expectedAuthenticated: false,
378			expectedUser:          nil,
379		},
380		{
381			description: "unauthenticated shouldn't even include extra provided info.",
382			serverResponse: authenticationv1beta1.TokenReviewStatus{
383				Authenticated: false,
384			},
385			expectedAuthenticated: false,
386			expectedUser:          nil,
387		},
388		{
389			description:  "good audience",
390			implicitAuds: apiAuds,
391			reqAuds:      apiAuds,
392			serverResponse: authenticationv1beta1.TokenReviewStatus{
393				Authenticated: true,
394				User: authenticationv1beta1.UserInfo{
395					Username: "somebody",
396				},
397			},
398			expectedAuthenticated: true,
399			expectedUser: &user.DefaultInfo{
400				Name: "somebody",
401			},
402			expectedAuds: apiAuds,
403		},
404		{
405			description:  "good audience",
406			implicitAuds: append(apiAuds, "other"),
407			reqAuds:      apiAuds,
408			serverResponse: authenticationv1beta1.TokenReviewStatus{
409				Authenticated: true,
410				User: authenticationv1beta1.UserInfo{
411					Username: "somebody",
412				},
413			},
414			expectedAuthenticated: true,
415			expectedUser: &user.DefaultInfo{
416				Name: "somebody",
417			},
418			expectedAuds: apiAuds,
419		},
420		{
421			description:  "bad audiences",
422			implicitAuds: apiAuds,
423			reqAuds:      authenticator.Audiences{"other"},
424			serverResponse: authenticationv1beta1.TokenReviewStatus{
425				Authenticated: false,
426			},
427			expectedAuthenticated: false,
428		},
429		{
430			description:  "bad audiences",
431			implicitAuds: apiAuds,
432			reqAuds:      authenticator.Audiences{"other"},
433			// webhook authenticator hasn't been upgraded to support audience.
434			serverResponse: authenticationv1beta1.TokenReviewStatus{
435				Authenticated: true,
436				User: authenticationv1beta1.UserInfo{
437					Username: "somebody",
438				},
439			},
440			expectedAuthenticated: false,
441		},
442		{
443			description:  "audience aware backend",
444			implicitAuds: apiAuds,
445			reqAuds:      apiAuds,
446			serverResponse: authenticationv1beta1.TokenReviewStatus{
447				Authenticated: true,
448				User: authenticationv1beta1.UserInfo{
449					Username: "somebody",
450				},
451				Audiences: []string(apiAuds),
452			},
453			expectedAuthenticated: true,
454			expectedUser: &user.DefaultInfo{
455				Name: "somebody",
456			},
457			expectedAuds: apiAuds,
458		},
459		{
460			description: "audience aware backend",
461			serverResponse: authenticationv1beta1.TokenReviewStatus{
462				Authenticated: true,
463				User: authenticationv1beta1.UserInfo{
464					Username: "somebody",
465				},
466				Audiences: []string(apiAuds),
467			},
468			expectedAuthenticated: true,
469			expectedUser: &user.DefaultInfo{
470				Name: "somebody",
471			},
472		},
473		{
474			description:  "audience aware backend",
475			implicitAuds: apiAuds,
476			reqAuds:      apiAuds,
477			serverResponse: authenticationv1beta1.TokenReviewStatus{
478				Authenticated: true,
479				User: authenticationv1beta1.UserInfo{
480					Username: "somebody",
481				},
482				Audiences: []string{"other"},
483			},
484			expectedAuthenticated: false,
485		},
486	}
487	token := "my-s3cr3t-t0ken" // Fake token for testing.
488	for _, tt := range tests {
489		t.Run(tt.description, func(t *testing.T) {
490			wh, err := newV1beta1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds)
491			if err != nil {
492				t.Fatal(err)
493			}
494
495			ctx := context.Background()
496			if tt.reqAuds != nil {
497				ctx = authenticator.WithAudiences(ctx, tt.reqAuds)
498			}
499
500			serv.response = tt.serverResponse
501			resp, authenticated, err := wh.AuthenticateToken(ctx, token)
502			if err != nil {
503				t.Fatalf("authentication failed: %v", err)
504			}
505			if serv.lastRequest.Spec.Token != token {
506				t.Errorf("Server did not see correct token. Got %q, expected %q.",
507					serv.lastRequest.Spec.Token, token)
508			}
509			if !reflect.DeepEqual(serv.lastRequest.TypeMeta, expTypeMeta) {
510				t.Errorf("Server did not see correct TypeMeta. Got %v, expected %v",
511					serv.lastRequest.TypeMeta, expTypeMeta)
512			}
513			if authenticated != tt.expectedAuthenticated {
514				t.Errorf("Plugin returned incorrect authentication response. Got %t, expected %t.",
515					authenticated, tt.expectedAuthenticated)
516			}
517			if resp != nil && tt.expectedUser != nil && !reflect.DeepEqual(resp.User, tt.expectedUser) {
518				t.Errorf("Plugin returned incorrect user. Got %#v, expected %#v",
519					resp.User, tt.expectedUser)
520			}
521			if resp != nil && tt.expectedAuds != nil && !reflect.DeepEqual(resp.Audiences, tt.expectedAuds) {
522				t.Errorf("Plugin returned incorrect audiences. Got %#v, expected %#v",
523					resp.Audiences, tt.expectedAuds)
524			}
525		})
526	}
527}
528
529type authenticationV1beta1UserInfo authenticationv1beta1.UserInfo
530
531func (a *authenticationV1beta1UserInfo) GetName() string     { return a.Username }
532func (a *authenticationV1beta1UserInfo) GetUID() string      { return a.UID }
533func (a *authenticationV1beta1UserInfo) GetGroups() []string { return a.Groups }
534
535func (a *authenticationV1beta1UserInfo) GetExtra() map[string][]string {
536	if a.Extra == nil {
537		return nil
538	}
539	ret := map[string][]string{}
540	for k, v := range a.Extra {
541		ret[k] = []string(v)
542	}
543
544	return ret
545}
546
547// Ensure authenticationv1beta1.UserInfo contains the fields necessary to implement the
548// user.Info interface.
549var _ user.Info = (*authenticationV1beta1UserInfo)(nil)
550
551// TestWebhookCache verifies that error responses from the server are not
552// cached, but successful responses are. It also ensures that the webhook
553// call is retried on 429 and 500+ errors
554func TestV1beta1WebhookCacheAndRetry(t *testing.T) {
555	serv := new(mockV1beta1Service)
556	s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert)
557	if err != nil {
558		t.Fatal(err)
559	}
560	defer s.Close()
561
562	// Create an authenticator that caches successful responses "forever" (100 days).
563	wh, err := newV1beta1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil)
564	if err != nil {
565		t.Fatal(err)
566	}
567
568	testcases := []struct {
569		description string
570
571		token string
572		allow bool
573		code  int
574
575		expectError bool
576		expectOk    bool
577		expectCalls int
578	}{
579		{
580			description: "t0k3n, 500 error, retries and fails",
581
582			token: "t0k3n",
583			allow: false,
584			code:  500,
585
586			expectError: true,
587			expectOk:    false,
588			expectCalls: 5,
589		},
590		{
591			description: "t0k3n, 404 error, fails (but no retry)",
592
593			token: "t0k3n",
594			allow: false,
595			code:  404,
596
597			expectError: true,
598			expectOk:    false,
599			expectCalls: 1,
600		},
601		{
602			description: "t0k3n, 200 response, allowed, succeeds with a single call",
603
604			token: "t0k3n",
605			allow: true,
606			code:  200,
607
608			expectError: false,
609			expectOk:    true,
610			expectCalls: 1,
611		},
612		{
613			description: "t0k3n, 500 response, disallowed, but never called because previous 200 response was cached",
614
615			token: "t0k3n",
616			allow: false,
617			code:  500,
618
619			expectError: false,
620			expectOk:    true,
621			expectCalls: 0,
622		},
623
624		{
625			description: "an0th3r_t0k3n, 500 response, disallowed, should be called again with retries",
626
627			token: "an0th3r_t0k3n",
628			allow: false,
629			code:  500,
630
631			expectError: true,
632			expectOk:    false,
633			expectCalls: 5,
634		},
635		{
636			description: "an0th3r_t0k3n, 429 response, disallowed, should be called again with retries",
637
638			token: "an0th3r_t0k3n",
639			allow: false,
640			code:  429,
641
642			expectError: true,
643			expectOk:    false,
644			expectCalls: 5,
645		},
646		{
647			description: "an0th3r_t0k3n, 200 response, allowed, succeeds with a single call",
648
649			token: "an0th3r_t0k3n",
650			allow: true,
651			code:  200,
652
653			expectError: false,
654			expectOk:    true,
655			expectCalls: 1,
656		},
657		{
658			description: "an0th3r_t0k3n, 500 response, disallowed, but never called because previous 200 response was cached",
659
660			token: "an0th3r_t0k3n",
661			allow: false,
662			code:  500,
663
664			expectError: false,
665			expectOk:    true,
666			expectCalls: 0,
667		},
668	}
669
670	for _, testcase := range testcases {
671		t.Run(testcase.description, func(t *testing.T) {
672			serv.allow = testcase.allow
673			serv.statusCode = testcase.code
674			serv.called = 0
675
676			_, ok, err := wh.AuthenticateToken(context.Background(), testcase.token)
677			hasError := err != nil
678			if hasError != testcase.expectError {
679				t.Errorf("Webhook returned HTTP %d, expected error=%v, but got error %v", testcase.code, testcase.expectError, err)
680			}
681			if serv.called != testcase.expectCalls {
682				t.Errorf("Expected %d calls, got %d", testcase.expectCalls, serv.called)
683			}
684			if ok != testcase.expectOk {
685				t.Errorf("Expected ok=%v, got %v", testcase.expectOk, ok)
686			}
687		})
688	}
689}
690