@@ -33,6 +33,8 @@ import (
33
33
"testing"
34
34
"time"
35
35
36
+ "github.com/google/go-cmp/cmp"
37
+
36
38
authorizationv1 "k8s.io/api/authorization/v1"
37
39
rbacv1 "k8s.io/api/rbac/v1"
38
40
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -125,6 +127,7 @@ func TestMultiWebhookAuthzConfig(t *testing.T) {
125
127
authzmetrics .ResetMetricsForTest ()
126
128
defer authzmetrics .ResetMetricsForTest ()
127
129
featuregatetesting .SetFeatureGateDuringTest (t , utilfeature .DefaultFeatureGate , features .StructuredAuthorizationConfiguration , true )
130
+ featuregatetesting .SetFeatureGateDuringTest (t , utilfeature .DefaultFeatureGate , features .AuthorizeWithSelectors , true )
128
131
129
132
dir := t .TempDir ()
130
133
@@ -253,6 +256,41 @@ users:
253
256
t .Fatal (err )
254
257
}
255
258
259
+ // returns allow responses when called with a label selector containing an allow key, and records the selectors it saw
260
+ selectorName := "selector.example.com"
261
+ serverSelectorCalled := atomic.Int32 {}
262
+ var selectorLabelAttributes * authorizationv1.LabelSelectorAttributes
263
+ var selectorFieldAttributes * authorizationv1.FieldSelectorAttributes
264
+ selectorServer := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
265
+ selectorLabelAttributes = nil
266
+ selectorFieldAttributes = nil
267
+ serverSelectorCalled .Add (1 )
268
+ sar := & authorizationv1.SubjectAccessReview {}
269
+ if err := json .NewDecoder (req .Body ).Decode (sar ); err != nil {
270
+ t .Error (err )
271
+ }
272
+ t .Log ("selector" , sar )
273
+ if sar .Spec .ResourceAttributes != nil {
274
+ selectorLabelAttributes = sar .Spec .ResourceAttributes .LabelSelector .DeepCopy ()
275
+ selectorFieldAttributes = sar .Spec .ResourceAttributes .FieldSelector .DeepCopy ()
276
+ if sar .Spec .ResourceAttributes .LabelSelector != nil {
277
+ for _ , req := range sar .Spec .ResourceAttributes .LabelSelector .Requirements {
278
+ if req .Key == "allow" {
279
+ sar .Status .Allowed = true
280
+ }
281
+ }
282
+ }
283
+ }
284
+ if err := json .NewEncoder (w ).Encode (sar ); err != nil {
285
+ t .Error (err )
286
+ }
287
+ }))
288
+ defer selectorServer .Close ()
289
+ serverSelectorKubeconfigName := filepath .Join (dir , "selector.yaml" )
290
+ if err := os .WriteFile (serverSelectorKubeconfigName , []byte (fmt .Sprintf (kubeconfigTemplate , selectorServer .URL )), os .FileMode (0644 )); err != nil {
291
+ t .Fatal (err )
292
+ }
293
+
256
294
// returns an allow response when called
257
295
allowName := "allow.example.com"
258
296
serverAllowCalled := atomic.Int32 {}
@@ -303,6 +341,7 @@ users:
303
341
serverDenyCalled .Store (0 )
304
342
serverNoOpinionCalled .Store (0 )
305
343
serverFailOpenCalled .Store (0 )
344
+ serverSelectorCalled .Store (0 )
306
345
serverAllowCalled .Store (0 )
307
346
serverAllowReloadedCalled .Store (0 )
308
347
authorizationmetrics .ResetMetricsForTest ()
@@ -311,7 +350,7 @@ users:
311
350
}
312
351
var adminClient * clientset.Clientset
313
352
type counts struct {
314
- errorCount , timeoutCount , denyCount , noOpinionCount , failOpenCount , allowCount , allowReloadedCount , webhookExclusionCount , evalErrorsCount int32
353
+ errorCount , timeoutCount , denyCount , noOpinionCount , failOpenCount , selectorCount , allowCount , allowReloadedCount , webhookExclusionCount , evalErrorsCount int32
315
354
}
316
355
assertCounts := func (c counts ) {
317
356
t .Helper ()
@@ -346,6 +385,7 @@ users:
346
385
if e , a := c .denyCount , metrics .decisions [authorizerKey {authorizerType : "Webhook" , authorizerName : denyName }]["denied" ]; e != int32 (a ) {
347
386
t .Fatalf ("expected deny webhook denied metrics calls: %d, got %d" , e , a )
348
387
}
388
+ assertCount (selectorName , c .selectorCount , & serverSelectorCalled )
349
389
assertCount (noOpinionName , c .noOpinionCount , & serverNoOpinionCalled )
350
390
assertCount (failOpenName , c .failOpenCount , & serverFailOpenCalled )
351
391
expectedFailOpenCounts := map [string ]int {}
@@ -355,6 +395,7 @@ users:
355
395
if ! reflect .DeepEqual (expectedFailOpenCounts , metrics .whFailOpenTotal ) {
356
396
t .Fatalf ("expected fail open %#v, got %#v" , expectedFailOpenCounts , metrics .whFailOpenTotal )
357
397
}
398
+
358
399
assertCount (allowName , c .allowCount , & serverAllowCalled )
359
400
if e , a := c .allowCount , metrics .decisions [authorizerKey {authorizerType : "Webhook" , authorizerName : allowName }]["allowed" ]; e != int32 (a ) {
360
401
t .Fatalf ("expected allow webhook allowed metrics calls: %d, got %d" , e , a )
@@ -428,6 +469,21 @@ authorizers:
428
469
- expression: has(request.resourceAttributes)
429
470
- expression: 'request.resourceAttributes.namespace == "fail"'
430
471
472
+ - type: Webhook
473
+ name: ` + selectorName + `
474
+ webhook:
475
+ timeout: 5s
476
+ failurePolicy: NoOpinion
477
+ subjectAccessReviewVersion: v1
478
+ matchConditionSubjectAccessReviewVersion: v1
479
+ authorizedTTL: 1ms
480
+ unauthorizedTTL: 1ms
481
+ connectionInfo:
482
+ type: KubeConfigFile
483
+ kubeConfigFile: ` + serverSelectorKubeconfigName + `
484
+ matchConditions:
485
+ - expression: request.?resourceAttributes.labelSelector.requirements.orValue([]).exists(r, r.key=='testselector')
486
+
431
487
- type: Webhook
432
488
name: ` + noOpinionName + `
433
489
webhook:
@@ -453,6 +509,7 @@ authorizers:
453
509
type: KubeConfigFile
454
510
kubeConfigFile: ` + serverFailOpenKubeconfigName + `
455
511
512
+
456
513
- type: Webhook
457
514
name: ` + allowName + `
458
515
webhook:
@@ -478,6 +535,10 @@ authorizers:
478
535
479
536
adminClient = clientset .NewForConfigOrDie (server .ClientConfig )
480
537
538
+ impersonationConfig := rest .CopyConfig (server .ClientConfig )
539
+ impersonationConfig .Impersonate .UserName = "alice"
540
+ aliceClient := clientset .NewForConfigOrDie (impersonationConfig )
541
+
481
542
// malformed webhook short circuits
482
543
t .Log ("checking error" )
483
544
if result , err := adminClient .AuthorizationV1 ().SubjectAccessReviews ().Create (context .TODO (), & authorizationv1.SubjectAccessReview {Spec : authorizationv1.SubjectAccessReviewSpec {
@@ -559,7 +620,7 @@ authorizers:
559
620
t .Fatal ("expected allowed, got denied" )
560
621
} else {
561
622
t .Log (result .Status .Reason )
562
- assertCounts (counts {noOpinionCount : 1 , failOpenCount : 1 , allowCount : 1 , webhookExclusionCount : 3 })
623
+ assertCounts (counts {noOpinionCount : 1 , failOpenCount : 1 , allowCount : 1 , webhookExclusionCount : 4 })
563
624
}
564
625
565
626
// the timeout webhook results in match condition eval errors when evaluating a non-resource request
@@ -581,6 +642,101 @@ authorizers:
581
642
assertCounts (counts {webhookExclusionCount : 1 , evalErrorsCount : 1 })
582
643
}
583
644
645
+ disorderedFieldSelector := "spec.nodeName=mynode,metadata.name!=b,metadata.name!=a"
646
+ orderedFieldRequirements := & authorizationv1.FieldSelectorAttributes {Requirements : []metav1.FieldSelectorRequirement {
647
+ {Key : "metadata.name" , Operator : "NotIn" , Values : []string {"a" }},
648
+ {Key : "metadata.name" , Operator : "NotIn" , Values : []string {"b" }},
649
+ {Key : "spec.nodeName" , Operator : "In" , Values : []string {"mynode" }}}}
650
+ disorderedFieldRequirements := & authorizationv1.FieldSelectorAttributes {Requirements : []metav1.FieldSelectorRequirement {
651
+ {Key : "spec.nodeName" , Operator : "In" , Values : []string {"mynode" }},
652
+ {Key : "metadata.name" , Operator : "NotIn" , Values : []string {"b" }},
653
+ {Key : "metadata.name" , Operator : "NotIn" , Values : []string {"a" }}}}
654
+ disorderedUnknownFieldRequirements := disorderedFieldRequirements .DeepCopy ()
655
+ disorderedUnknownFieldRequirements .Requirements = append (disorderedUnknownFieldRequirements .Requirements , metav1.FieldSelectorRequirement {Key : "x" , Operator : "Unknown" })
656
+ disorderedLabelSelector := "testselector in (b,a),allow=true"
657
+ orderedLabelRequirements := & authorizationv1.LabelSelectorAttributes {Requirements : []metav1.LabelSelectorRequirement {
658
+ {Key : "allow" , Operator : "In" , Values : []string {"true" }},
659
+ {Key : "testselector" , Operator : "In" , Values : []string {"a" , "b" }}}}
660
+ disorderedLabelRequirements := & authorizationv1.LabelSelectorAttributes {Requirements : []metav1.LabelSelectorRequirement {
661
+ {Key : "testselector" , Operator : "In" , Values : []string {"b" , "a" }},
662
+ {Key : "allow" , Operator : "In" , Values : []string {"true" }}}}
663
+ disorderedUnknownLabelRequirements := disorderedLabelRequirements .DeepCopy ()
664
+ disorderedUnknownLabelRequirements .Requirements = append (disorderedUnknownLabelRequirements .Requirements , metav1.LabelSelectorRequirement {Key : "x" , Operator : "Unknown" })
665
+
666
+ // make request matching selector webhook matchCondition
667
+ // check fieldSelector and labelSelector are parsed and normalized
668
+ _ , err := aliceClient .CoreV1 ().Pods ("" ).List (context .Background (), metav1.ListOptions {FieldSelector : disorderedFieldSelector , LabelSelector : disorderedLabelSelector })
669
+ assertCounts (counts {selectorCount : 1 , webhookExclusionCount : 3 })
670
+ if err != nil {
671
+ t .Fatalf ("expected success, got error: %v" , err )
672
+ }
673
+ if e , a := orderedFieldRequirements .DeepCopy (), selectorFieldAttributes ; ! reflect .DeepEqual (e , a ) {
674
+ t .Fatalf ("unexpected diff:\n %s" , cmp .Diff (a , e ))
675
+ }
676
+ if e , a := orderedLabelRequirements .DeepCopy (), selectorLabelAttributes ; ! reflect .DeepEqual (e , a ) {
677
+ t .Fatalf ("unexpected diff:\n %s" , cmp .Diff (a , e ))
678
+ }
679
+ selectorFieldAttributes = nil
680
+ selectorLabelAttributes = nil
681
+
682
+ // make subjectaccessreview request containing fieldSelector and labelSelector requirements
683
+ // check known fieldSelector and labelSelector requirements get passed through to the webhook as-is
684
+ if result , err := adminClient .AuthorizationV1 ().SubjectAccessReviews ().Create (context .TODO (), & authorizationv1.SubjectAccessReview {Spec : authorizationv1.SubjectAccessReviewSpec {
685
+ User : "alice" ,
686
+ ResourceAttributes : & authorizationv1.ResourceAttributes {
687
+ Verb : "list" ,
688
+ Version : "v1" ,
689
+ Resource : "pods" ,
690
+ FieldSelector : disorderedUnknownFieldRequirements .DeepCopy (),
691
+ LabelSelector : disorderedUnknownLabelRequirements .DeepCopy (),
692
+ },
693
+ }}, metav1.CreateOptions {}); err != nil {
694
+ t .Fatal (err )
695
+ } else if ! result .Status .Allowed {
696
+ t .Fatal ("expected allowed, got denied" )
697
+ } else {
698
+ t .Log (result .Status .Reason )
699
+ t .Log (result .Status .EvaluationError )
700
+ assertCounts (counts {selectorCount : 1 , webhookExclusionCount : 3 })
701
+ if e , a := disorderedFieldRequirements .DeepCopy (), selectorFieldAttributes ; ! reflect .DeepEqual (e , a ) {
702
+ t .Fatalf ("unexpected diff:\n %s" , cmp .Diff (a , e ))
703
+ }
704
+ if e , a := disorderedLabelRequirements .DeepCopy (), selectorLabelAttributes ; ! reflect .DeepEqual (e , a ) {
705
+ t .Fatalf ("unexpected diff:\n %s" , cmp .Diff (a , e ))
706
+ }
707
+ }
708
+ selectorFieldAttributes = nil
709
+ selectorLabelAttributes = nil
710
+
711
+ // make subjectaccessreview request containing fieldSelector and labelSelector rawSelector
712
+ // check fieldSelector and labelSelector rawSelector get parsed and passed to the webhook
713
+ if result , err := adminClient .AuthorizationV1 ().SubjectAccessReviews ().Create (context .TODO (), & authorizationv1.SubjectAccessReview {Spec : authorizationv1.SubjectAccessReviewSpec {
714
+ User : "alice" ,
715
+ ResourceAttributes : & authorizationv1.ResourceAttributes {
716
+ Verb : "list" ,
717
+ Version : "v1" ,
718
+ Resource : "pods" ,
719
+ FieldSelector : & authorizationv1.FieldSelectorAttributes {RawSelector : disorderedFieldSelector },
720
+ LabelSelector : & authorizationv1.LabelSelectorAttributes {RawSelector : disorderedLabelSelector },
721
+ },
722
+ }}, metav1.CreateOptions {}); err != nil {
723
+ t .Fatal (err )
724
+ } else if ! result .Status .Allowed {
725
+ t .Fatal ("expected allowed, got denied" )
726
+ } else {
727
+ t .Log (result .Status .Reason )
728
+ t .Log (result .Status .EvaluationError )
729
+ assertCounts (counts {selectorCount : 1 , webhookExclusionCount : 3 })
730
+ if e , a := orderedFieldRequirements .DeepCopy (), selectorFieldAttributes ; ! reflect .DeepEqual (e , a ) {
731
+ t .Fatalf ("unexpected diff:\n %s" , cmp .Diff (a , e ))
732
+ }
733
+ if e , a := orderedLabelRequirements .DeepCopy (), selectorLabelAttributes ; ! reflect .DeepEqual (e , a ) {
734
+ t .Fatalf ("unexpected diff:\n %s" , cmp .Diff (a , e ))
735
+ }
736
+ }
737
+ selectorFieldAttributes = nil
738
+ selectorLabelAttributes = nil
739
+
584
740
// check last loaded success/failure metric timestamps, ensure success is present, failure is not
585
741
initialMetrics , err := getMetrics (t , adminClient )
586
742
if err != nil {
@@ -642,7 +798,7 @@ authorizers:
642
798
t .Fatal ("expected allowed, got denied" )
643
799
} else {
644
800
t .Log (result .Status .Reason )
645
- assertCounts (counts {noOpinionCount : 1 , failOpenCount : 1 , allowCount : 1 , webhookExclusionCount : 3 })
801
+ assertCounts (counts {noOpinionCount : 1 , failOpenCount : 1 , allowCount : 1 , webhookExclusionCount : 4 })
646
802
}
647
803
648
804
// write good config with different webhook
0 commit comments