@@ -18,9 +18,15 @@ package auth
18
18
19
19
import (
20
20
"context"
21
+ "encoding/json"
22
+ "fmt"
23
+ "net/http"
24
+ "net/http/httptest"
21
25
"os"
22
26
"path/filepath"
27
+ "sync/atomic"
23
28
"testing"
29
+ "time"
24
30
25
31
authorizationv1 "k8s.io/api/authorization/v1"
26
32
rbacv1 "k8s.io/api/rbac/v1"
@@ -93,3 +99,320 @@ authorizers:
93
99
t .Fatal ("expected allowed, got denied" )
94
100
}
95
101
}
102
+
103
+ func TestMultiWebhookAuthzConfig (t * testing.T ) {
104
+ defer featuregatetesting .SetFeatureGateDuringTest (t , utilfeature .DefaultFeatureGate , features .StructuredAuthorizationConfiguration , true )()
105
+
106
+ dir := t .TempDir ()
107
+
108
+ kubeconfigTemplate := `
109
+ apiVersion: v1
110
+ kind: Config
111
+ clusters:
112
+ - name: integration
113
+ cluster:
114
+ server: %q
115
+ insecure-skip-tls-verify: true
116
+ contexts:
117
+ - name: default-context
118
+ context:
119
+ cluster: integration
120
+ user: test
121
+ current-context: default-context
122
+ users:
123
+ - name: test
124
+ `
125
+
126
+ // returns malformed responses when called
127
+ serverErrorCalled := atomic.Int32 {}
128
+ serverError := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
129
+ serverErrorCalled .Add (1 )
130
+ sar := & authorizationv1.SubjectAccessReview {}
131
+ if err := json .NewDecoder (req .Body ).Decode (sar ); err != nil {
132
+ t .Error (err )
133
+ }
134
+ t .Log ("serverError" , sar )
135
+ if _ , err := w .Write ([]byte (`error response` )); err != nil {
136
+ t .Error (err )
137
+ }
138
+ }))
139
+ defer serverError .Close ()
140
+ serverErrorKubeconfigName := filepath .Join (dir , "serverError.yaml" )
141
+ if err := os .WriteFile (serverErrorKubeconfigName , []byte (fmt .Sprintf (kubeconfigTemplate , serverError .URL )), os .FileMode (0644 )); err != nil {
142
+ t .Fatal (err )
143
+ }
144
+
145
+ // hangs for 2 seconds when called
146
+ serverTimeoutCalled := atomic.Int32 {}
147
+ serverTimeout := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
148
+ serverTimeoutCalled .Add (1 )
149
+ sar := & authorizationv1.SubjectAccessReview {}
150
+ if err := json .NewDecoder (req .Body ).Decode (sar ); err != nil {
151
+ t .Error (err )
152
+ }
153
+ t .Log ("serverTimeout" , sar )
154
+ time .Sleep (2 * time .Second )
155
+ }))
156
+ defer serverTimeout .Close ()
157
+ serverTimeoutKubeconfigName := filepath .Join (dir , "serverTimeout.yaml" )
158
+ if err := os .WriteFile (serverTimeoutKubeconfigName , []byte (fmt .Sprintf (kubeconfigTemplate , serverTimeout .URL )), os .FileMode (0644 )); err != nil {
159
+ t .Fatal (err )
160
+ }
161
+
162
+ // returns a deny response when called
163
+ serverDenyCalled := atomic.Int32 {}
164
+ serverDeny := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
165
+ serverDenyCalled .Add (1 )
166
+ sar := & authorizationv1.SubjectAccessReview {}
167
+ if err := json .NewDecoder (req .Body ).Decode (sar ); err != nil {
168
+ t .Error (err )
169
+ }
170
+ t .Log ("serverDeny" , sar )
171
+ sar .Status .Allowed = false
172
+ sar .Status .Denied = true
173
+ sar .Status .Reason = "denied by webhook"
174
+ if err := json .NewEncoder (w ).Encode (sar ); err != nil {
175
+ t .Error (err )
176
+ }
177
+ }))
178
+ defer serverDeny .Close ()
179
+ serverDenyKubeconfigName := filepath .Join (dir , "serverDeny.yaml" )
180
+ if err := os .WriteFile (serverDenyKubeconfigName , []byte (fmt .Sprintf (kubeconfigTemplate , serverDeny .URL )), os .FileMode (0644 )); err != nil {
181
+ t .Fatal (err )
182
+ }
183
+
184
+ // returns a no opinion response when called
185
+ serverNoOpinionCalled := atomic.Int32 {}
186
+ serverNoOpinion := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
187
+ serverNoOpinionCalled .Add (1 )
188
+ sar := & authorizationv1.SubjectAccessReview {}
189
+ if err := json .NewDecoder (req .Body ).Decode (sar ); err != nil {
190
+ t .Error (err )
191
+ }
192
+ t .Log ("serverNoOpinion" , sar )
193
+ sar .Status .Allowed = false
194
+ sar .Status .Denied = false
195
+ if err := json .NewEncoder (w ).Encode (sar ); err != nil {
196
+ t .Error (err )
197
+ }
198
+ }))
199
+ defer serverNoOpinion .Close ()
200
+ serverNoOpinionKubeconfigName := filepath .Join (dir , "serverNoOpinion.yaml" )
201
+ if err := os .WriteFile (serverNoOpinionKubeconfigName , []byte (fmt .Sprintf (kubeconfigTemplate , serverNoOpinion .URL )), os .FileMode (0644 )); err != nil {
202
+ t .Fatal (err )
203
+ }
204
+
205
+ // returns an allow response when called
206
+ serverAllowCalled := atomic.Int32 {}
207
+ serverAllow := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
208
+ serverAllowCalled .Add (1 )
209
+ sar := & authorizationv1.SubjectAccessReview {}
210
+ if err := json .NewDecoder (req .Body ).Decode (sar ); err != nil {
211
+ t .Error (err )
212
+ }
213
+ t .Log ("serverAllow" , sar )
214
+ sar .Status .Allowed = true
215
+ sar .Status .Reason = "allowed by webhook"
216
+ if err := json .NewEncoder (w ).Encode (sar ); err != nil {
217
+ t .Error (err )
218
+ }
219
+ }))
220
+ defer serverAllow .Close ()
221
+ serverAllowKubeconfigName := filepath .Join (dir , "serverAllow.yaml" )
222
+ if err := os .WriteFile (serverAllowKubeconfigName , []byte (fmt .Sprintf (kubeconfigTemplate , serverAllow .URL )), os .FileMode (0644 )); err != nil {
223
+ t .Fatal (err )
224
+ }
225
+
226
+ resetCounts := func () {
227
+ serverErrorCalled .Store (0 )
228
+ serverTimeoutCalled .Store (0 )
229
+ serverDenyCalled .Store (0 )
230
+ serverNoOpinionCalled .Store (0 )
231
+ serverAllowCalled .Store (0 )
232
+ }
233
+ assertCounts := func (errorCount , timeoutCount , denyCount , noOpinionCount , allowCount int32 ) {
234
+ t .Helper ()
235
+ if e , a := errorCount , serverErrorCalled .Load (); e != a {
236
+ t .Errorf ("expected fail webhook calls: %d, got %d" , e , a )
237
+ }
238
+ if e , a := timeoutCount , serverTimeoutCalled .Load (); e != a {
239
+ t .Errorf ("expected timeout webhook calls: %d, got %d" , e , a )
240
+ }
241
+ if e , a := denyCount , serverDenyCalled .Load (); e != a {
242
+ t .Errorf ("expected deny webhook calls: %d, got %d" , e , a )
243
+ }
244
+ if e , a := noOpinionCount , serverNoOpinionCalled .Load (); e != a {
245
+ t .Errorf ("expected noOpinion webhook calls: %d, got %d" , e , a )
246
+ }
247
+ if e , a := allowCount , serverAllowCalled .Load (); e != a {
248
+ t .Errorf ("expected allow webhook calls: %d, got %d" , e , a )
249
+ }
250
+ resetCounts ()
251
+ }
252
+
253
+ configFileName := filepath .Join (dir , "config.yaml" )
254
+ if err := os .WriteFile (configFileName , []byte (`
255
+ apiVersion: apiserver.config.k8s.io/v1alpha1
256
+ kind: AuthorizationConfiguration
257
+ authorizers:
258
+ - type: Webhook
259
+ name: error.example.com
260
+ webhook:
261
+ timeout: 5s
262
+ failurePolicy: Deny
263
+ subjectAccessReviewVersion: v1
264
+ matchConditionSubjectAccessReviewVersion: v1
265
+ connectionInfo:
266
+ type: KubeConfigFile
267
+ kubeConfigFile: ` + serverErrorKubeconfigName + `
268
+ matchConditions:
269
+ - expression: has(request.resourceAttributes)
270
+ - expression: 'request.resourceAttributes.namespace == "fail"'
271
+ - expression: 'request.resourceAttributes.name == "error"'
272
+
273
+ - type: Webhook
274
+ name: timeout.example.com
275
+ webhook:
276
+ timeout: 1s
277
+ failurePolicy: Deny
278
+ subjectAccessReviewVersion: v1
279
+ matchConditionSubjectAccessReviewVersion: v1
280
+ connectionInfo:
281
+ type: KubeConfigFile
282
+ kubeConfigFile: ` + serverTimeoutKubeconfigName + `
283
+ matchConditions:
284
+ - expression: has(request.resourceAttributes)
285
+ - expression: 'request.resourceAttributes.namespace == "fail"'
286
+ - expression: 'request.resourceAttributes.name == "timeout"'
287
+
288
+ - type: Webhook
289
+ name: deny.example.com
290
+ webhook:
291
+ timeout: 5s
292
+ failurePolicy: NoOpinion
293
+ subjectAccessReviewVersion: v1
294
+ matchConditionSubjectAccessReviewVersion: v1
295
+ connectionInfo:
296
+ type: KubeConfigFile
297
+ kubeConfigFile: ` + serverDenyKubeconfigName + `
298
+ matchConditions:
299
+ - expression: has(request.resourceAttributes)
300
+ - expression: 'request.resourceAttributes.namespace == "fail"'
301
+
302
+ - type: Webhook
303
+ name: noopinion.example.com
304
+ webhook:
305
+ timeout: 5s
306
+ failurePolicy: Deny
307
+ subjectAccessReviewVersion: v1
308
+ connectionInfo:
309
+ type: KubeConfigFile
310
+ kubeConfigFile: ` + serverNoOpinionKubeconfigName + `
311
+
312
+ - type: Webhook
313
+ name: allow.example.com
314
+ webhook:
315
+ timeout: 5s
316
+ failurePolicy: Deny
317
+ subjectAccessReviewVersion: v1
318
+ connectionInfo:
319
+ type: KubeConfigFile
320
+ kubeConfigFile: ` + serverAllowKubeconfigName + `
321
+ ` ), os .FileMode (0644 )); err != nil {
322
+ t .Fatal (err )
323
+ }
324
+
325
+ server := kubeapiservertesting .StartTestServerOrDie (
326
+ t ,
327
+ nil ,
328
+ []string {"--authorization-config=" + configFileName },
329
+ framework .SharedEtcd (),
330
+ )
331
+ t .Cleanup (server .TearDownFn )
332
+
333
+ adminClient := clientset .NewForConfigOrDie (server .ClientConfig )
334
+
335
+ // malformed webhook short circuits
336
+ t .Log ("checking error" )
337
+ if result , err := adminClient .AuthorizationV1 ().SubjectAccessReviews ().Create (context .TODO (), & authorizationv1.SubjectAccessReview {Spec : authorizationv1.SubjectAccessReviewSpec {
338
+ User : "alice" ,
339
+ ResourceAttributes : & authorizationv1.ResourceAttributes {
340
+ Verb : "get" ,
341
+ Group : "" ,
342
+ Version : "v1" ,
343
+ Resource : "configmaps" ,
344
+ Namespace : "fail" ,
345
+ Name : "error" ,
346
+ },
347
+ }}, metav1.CreateOptions {}); err != nil {
348
+ t .Fatal (err )
349
+ } else if result .Status .Allowed {
350
+ t .Fatal ("expected denied, got allowed" )
351
+ } else {
352
+ t .Log (result .Status .Reason )
353
+ assertCounts (1 , 0 , 0 , 0 , 0 )
354
+ }
355
+
356
+ // timeout webhook short circuits
357
+ t .Log ("checking timeout" )
358
+ if result , err := adminClient .AuthorizationV1 ().SubjectAccessReviews ().Create (context .TODO (), & authorizationv1.SubjectAccessReview {Spec : authorizationv1.SubjectAccessReviewSpec {
359
+ User : "alice" ,
360
+ ResourceAttributes : & authorizationv1.ResourceAttributes {
361
+ Verb : "get" ,
362
+ Group : "" ,
363
+ Version : "v1" ,
364
+ Resource : "configmaps" ,
365
+ Namespace : "fail" ,
366
+ Name : "timeout" ,
367
+ },
368
+ }}, metav1.CreateOptions {}); err != nil {
369
+ t .Fatal (err )
370
+ } else if result .Status .Allowed {
371
+ t .Fatal ("expected denied, got allowed" )
372
+ } else {
373
+ t .Log (result .Status .Reason )
374
+ assertCounts (0 , 1 , 0 , 0 , 0 )
375
+ }
376
+
377
+ // deny webhook short circuits
378
+ t .Log ("checking deny" )
379
+ if result , err := adminClient .AuthorizationV1 ().SubjectAccessReviews ().Create (context .TODO (), & authorizationv1.SubjectAccessReview {Spec : authorizationv1.SubjectAccessReviewSpec {
380
+ User : "alice" ,
381
+ ResourceAttributes : & authorizationv1.ResourceAttributes {
382
+ Verb : "list" ,
383
+ Group : "" ,
384
+ Version : "v1" ,
385
+ Resource : "configmaps" ,
386
+ Namespace : "fail" ,
387
+ Name : "" ,
388
+ },
389
+ }}, metav1.CreateOptions {}); err != nil {
390
+ t .Fatal (err )
391
+ } else if result .Status .Allowed {
392
+ t .Fatal ("expected denied, got allowed" )
393
+ } else {
394
+ t .Log (result .Status .Reason )
395
+ assertCounts (0 , 0 , 1 , 0 , 0 )
396
+ }
397
+
398
+ // no-opinion webhook passes through, allow webhook allows
399
+ t .Log ("checking allow" )
400
+ if result , err := adminClient .AuthorizationV1 ().SubjectAccessReviews ().Create (context .TODO (), & authorizationv1.SubjectAccessReview {Spec : authorizationv1.SubjectAccessReviewSpec {
401
+ User : "alice" ,
402
+ ResourceAttributes : & authorizationv1.ResourceAttributes {
403
+ Verb : "list" ,
404
+ Group : "" ,
405
+ Version : "v1" ,
406
+ Resource : "configmaps" ,
407
+ Namespace : "allow" ,
408
+ Name : "" ,
409
+ },
410
+ }}, metav1.CreateOptions {}); err != nil {
411
+ t .Fatal (err )
412
+ } else if ! result .Status .Allowed {
413
+ t .Fatal ("expected allowed, got denied" )
414
+ } else {
415
+ t .Log (result .Status .Reason )
416
+ assertCounts (0 , 0 , 0 , 1 , 1 )
417
+ }
418
+ }
0 commit comments