...

Source file src/sigs.k8s.io/controller-runtime/pkg/webhook/authentication/http_test.go

Documentation: sigs.k8s.io/controller-runtime/pkg/webhook/authentication

     1  /*
     2  Copyright 2021 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 authentication
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/rand"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/http/httptest"
    27  
    28  	. "github.com/onsi/ginkgo/v2"
    29  	. "github.com/onsi/gomega"
    30  	authenticationv1 "k8s.io/api/authentication/v1"
    31  )
    32  
    33  var _ = Describe("Authentication Webhooks", func() {
    34  
    35  	const (
    36  		gvkJSONv1 = `"kind":"TokenReview","apiVersion":"authentication.k8s.io/v1"`
    37  	)
    38  
    39  	Describe("HTTP Handler", func() {
    40  		var respRecorder *httptest.ResponseRecorder
    41  		webhook := &Webhook{
    42  			Handler: nil,
    43  		}
    44  		BeforeEach(func() {
    45  			respRecorder = &httptest.ResponseRecorder{
    46  				Body: bytes.NewBuffer(nil),
    47  			}
    48  		})
    49  
    50  		It("should return bad-request when given an empty body", func() {
    51  			req := &http.Request{Body: nil}
    52  
    53  			expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"request body is empty"}}
    54  `
    55  			webhook.ServeHTTP(respRecorder, req)
    56  			Expect(respRecorder.Body.String()).To(Equal(expected))
    57  		})
    58  
    59  		It("should return bad-request when given the wrong content-type", func() {
    60  			req := &http.Request{
    61  				Header: http.Header{"Content-Type": []string{"application/foo"}},
    62  				Method: http.MethodPost,
    63  				Body:   nopCloser{Reader: bytes.NewBuffer(nil)},
    64  			}
    65  
    66  			expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"contentType=application/foo, expected application/json"}}
    67  `
    68  			webhook.ServeHTTP(respRecorder, req)
    69  			Expect(respRecorder.Body.String()).To(Equal(expected))
    70  		})
    71  
    72  		It("should return bad-request when given an undecodable body", func() {
    73  			req := &http.Request{
    74  				Header: http.Header{"Content-Type": []string{"application/json"}},
    75  				Method: http.MethodPost,
    76  				Body:   nopCloser{Reader: bytes.NewBufferString("{")},
    77  			}
    78  
    79  			expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"couldn't get version/kind; json parse error: unexpected end of JSON input"}}
    80  `
    81  			webhook.ServeHTTP(respRecorder, req)
    82  			Expect(respRecorder.Body.String()).To(Equal(expected))
    83  		})
    84  
    85  		It("should return bad-request when given an undecodable body", func() {
    86  			req := &http.Request{
    87  				Header: http.Header{"Content-Type": []string{"application/json"}},
    88  				Method: http.MethodPost,
    89  				Body:   nopCloser{Reader: bytes.NewBufferString(`{"spec":{"token":""}}`)},
    90  			}
    91  
    92  			expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"token is empty"}}
    93  `
    94  			webhook.ServeHTTP(respRecorder, req)
    95  			Expect(respRecorder.Body.String()).To(Equal(expected))
    96  		})
    97  
    98  		It("should error when given a NoBody", func() {
    99  			req := &http.Request{
   100  				Header: http.Header{"Content-Type": []string{"application/json"}},
   101  				Method: http.MethodPost,
   102  				Body:   http.NoBody,
   103  			}
   104  
   105  			expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"request body is empty"}}
   106  `
   107  			webhook.ServeHTTP(respRecorder, req)
   108  			Expect(respRecorder.Body.String()).To(Equal(expected))
   109  		})
   110  
   111  		It("should error when given an infinite body", func() {
   112  			req := &http.Request{
   113  				Header: http.Header{"Content-Type": []string{"application/json"}},
   114  				Method: http.MethodPost,
   115  				Body:   nopCloser{Reader: rand.Reader},
   116  			}
   117  
   118  			expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"request entity is too large; limit is 1048576 bytes"}}
   119  `
   120  			webhook.ServeHTTP(respRecorder, req)
   121  			Expect(respRecorder.Body.String()).To(Equal(expected))
   122  		})
   123  
   124  		It("should return the response given by the handler with version defaulted to v1", func() {
   125  			req := &http.Request{
   126  				Header: http.Header{"Content-Type": []string{"application/json"}},
   127  				Method: http.MethodPost,
   128  				Body:   nopCloser{Reader: bytes.NewBufferString(`{"spec":{"token":"foobar"}}`)},
   129  			}
   130  			webhook := &Webhook{
   131  				Handler: &fakeHandler{},
   132  			}
   133  
   134  			expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"authenticated":true,"user":{}}}
   135  `, gvkJSONv1)
   136  
   137  			webhook.ServeHTTP(respRecorder, req)
   138  			Expect(respRecorder.Body.String()).To(Equal(expected))
   139  		})
   140  
   141  		It("should return the v1 response given by the handler", func() {
   142  			req := &http.Request{
   143  				Header: http.Header{"Content-Type": []string{"application/json"}},
   144  				Method: http.MethodPost,
   145  				Body:   nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"spec":{"token":"foobar"}}`, gvkJSONv1))},
   146  			}
   147  			webhook := &Webhook{
   148  				Handler: &fakeHandler{},
   149  			}
   150  
   151  			expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"authenticated":true,"user":{}}}
   152  `, gvkJSONv1)
   153  			webhook.ServeHTTP(respRecorder, req)
   154  			Expect(respRecorder.Body.String()).To(Equal(expected))
   155  		})
   156  
   157  		It("should present the Context from the HTTP request, if any", func() {
   158  			req := &http.Request{
   159  				Header: http.Header{"Content-Type": []string{"application/json"}},
   160  				Method: http.MethodPost,
   161  				Body:   nopCloser{Reader: bytes.NewBufferString(`{"spec":{"token":"foobar"}}`)},
   162  			}
   163  			type ctxkey int
   164  			const key ctxkey = 1
   165  			const value = "from-ctx"
   166  			webhook := &Webhook{
   167  				Handler: &fakeHandler{
   168  					fn: func(ctx context.Context, req Request) Response {
   169  						<-ctx.Done()
   170  						return Authenticated(ctx.Value(key).(string), authenticationv1.UserInfo{})
   171  					},
   172  				},
   173  			}
   174  
   175  			expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"authenticated":true,"user":{},"error":%q}}
   176  `, gvkJSONv1, value)
   177  
   178  			ctx, cancel := context.WithCancel(context.WithValue(context.Background(), key, value))
   179  			cancel()
   180  			webhook.ServeHTTP(respRecorder, req.WithContext(ctx))
   181  			Expect(respRecorder.Body.String()).To(Equal(expected))
   182  		})
   183  
   184  		It("should mutate the Context from the HTTP request, if func supplied", func() {
   185  			req := &http.Request{
   186  				Header: http.Header{"Content-Type": []string{"application/json"}},
   187  				Method: http.MethodPost,
   188  				Body:   nopCloser{Reader: bytes.NewBufferString(`{"spec":{"token":"foobar"}}`)},
   189  			}
   190  			type ctxkey int
   191  			const key ctxkey = 1
   192  			webhook := &Webhook{
   193  				Handler: &fakeHandler{
   194  					fn: func(ctx context.Context, req Request) Response {
   195  						return Authenticated(ctx.Value(key).(string), authenticationv1.UserInfo{})
   196  					},
   197  				},
   198  				WithContextFunc: func(ctx context.Context, r *http.Request) context.Context {
   199  					return context.WithValue(ctx, key, r.Header["Content-Type"][0])
   200  				},
   201  			}
   202  
   203  			expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"authenticated":true,"user":{},"error":%q}}
   204  `, gvkJSONv1, "application/json")
   205  
   206  			ctx, cancel := context.WithCancel(context.Background())
   207  			cancel()
   208  			webhook.ServeHTTP(respRecorder, req.WithContext(ctx))
   209  			Expect(respRecorder.Body.String()).To(Equal(expected))
   210  		})
   211  	})
   212  })
   213  
   214  type nopCloser struct {
   215  	io.Reader
   216  }
   217  
   218  func (nopCloser) Close() error { return nil }
   219  
   220  type fakeHandler struct {
   221  	invoked bool
   222  	fn      func(context.Context, Request) Response
   223  }
   224  
   225  func (h *fakeHandler) Handle(ctx context.Context, req Request) Response {
   226  	h.invoked = true
   227  	if h.fn != nil {
   228  		return h.fn(ctx, req)
   229  	}
   230  	return Response{TokenReview: authenticationv1.TokenReview{
   231  		Status: authenticationv1.TokenReviewStatus{
   232  			Authenticated: true,
   233  		},
   234  	}}
   235  }
   236  

View as plain text