sigs.k8s.io/controller-runtime@v0.18.2/pkg/webhook/authentication/http_test.go (about) 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 }