1
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