Skip to content

Commit 5f22dd7

Browse files
committed
Add integration test exercising webhook selector authz
1 parent 9f8f367 commit 5f22dd7

File tree

1 file changed

+159
-3
lines changed

1 file changed

+159
-3
lines changed

test/integration/auth/authz_config_test.go

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import (
3333
"testing"
3434
"time"
3535

36+
"github.com/google/go-cmp/cmp"
37+
3638
authorizationv1 "k8s.io/api/authorization/v1"
3739
rbacv1 "k8s.io/api/rbac/v1"
3840
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -125,6 +127,7 @@ func TestMultiWebhookAuthzConfig(t *testing.T) {
125127
authzmetrics.ResetMetricsForTest()
126128
defer authzmetrics.ResetMetricsForTest()
127129
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)
130+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthorizeWithSelectors, true)
128131

129132
dir := t.TempDir()
130133

@@ -253,6 +256,41 @@ users:
253256
t.Fatal(err)
254257
}
255258

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+
256294
// returns an allow response when called
257295
allowName := "allow.example.com"
258296
serverAllowCalled := atomic.Int32{}
@@ -303,6 +341,7 @@ users:
303341
serverDenyCalled.Store(0)
304342
serverNoOpinionCalled.Store(0)
305343
serverFailOpenCalled.Store(0)
344+
serverSelectorCalled.Store(0)
306345
serverAllowCalled.Store(0)
307346
serverAllowReloadedCalled.Store(0)
308347
authorizationmetrics.ResetMetricsForTest()
@@ -311,7 +350,7 @@ users:
311350
}
312351
var adminClient *clientset.Clientset
313352
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
315354
}
316355
assertCounts := func(c counts) {
317356
t.Helper()
@@ -346,6 +385,7 @@ users:
346385
if e, a := c.denyCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: denyName}]["denied"]; e != int32(a) {
347386
t.Fatalf("expected deny webhook denied metrics calls: %d, got %d", e, a)
348387
}
388+
assertCount(selectorName, c.selectorCount, &serverSelectorCalled)
349389
assertCount(noOpinionName, c.noOpinionCount, &serverNoOpinionCalled)
350390
assertCount(failOpenName, c.failOpenCount, &serverFailOpenCalled)
351391
expectedFailOpenCounts := map[string]int{}
@@ -355,6 +395,7 @@ users:
355395
if !reflect.DeepEqual(expectedFailOpenCounts, metrics.whFailOpenTotal) {
356396
t.Fatalf("expected fail open %#v, got %#v", expectedFailOpenCounts, metrics.whFailOpenTotal)
357397
}
398+
358399
assertCount(allowName, c.allowCount, &serverAllowCalled)
359400
if e, a := c.allowCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: allowName}]["allowed"]; e != int32(a) {
360401
t.Fatalf("expected allow webhook allowed metrics calls: %d, got %d", e, a)
@@ -428,6 +469,21 @@ authorizers:
428469
- expression: has(request.resourceAttributes)
429470
- expression: 'request.resourceAttributes.namespace == "fail"'
430471
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+
431487
- type: Webhook
432488
name: `+noOpinionName+`
433489
webhook:
@@ -453,6 +509,7 @@ authorizers:
453509
type: KubeConfigFile
454510
kubeConfigFile: `+serverFailOpenKubeconfigName+`
455511
512+
456513
- type: Webhook
457514
name: `+allowName+`
458515
webhook:
@@ -478,6 +535,10 @@ authorizers:
478535

479536
adminClient = clientset.NewForConfigOrDie(server.ClientConfig)
480537

538+
impersonationConfig := rest.CopyConfig(server.ClientConfig)
539+
impersonationConfig.Impersonate.UserName = "alice"
540+
aliceClient := clientset.NewForConfigOrDie(impersonationConfig)
541+
481542
// malformed webhook short circuits
482543
t.Log("checking error")
483544
if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{
@@ -559,7 +620,7 @@ authorizers:
559620
t.Fatal("expected allowed, got denied")
560621
} else {
561622
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})
563624
}
564625

565626
// the timeout webhook results in match condition eval errors when evaluating a non-resource request
@@ -581,6 +642,101 @@ authorizers:
581642
assertCounts(counts{webhookExclusionCount: 1, evalErrorsCount: 1})
582643
}
583644

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+
584740
// check last loaded success/failure metric timestamps, ensure success is present, failure is not
585741
initialMetrics, err := getMetrics(t, adminClient)
586742
if err != nil {
@@ -642,7 +798,7 @@ authorizers:
642798
t.Fatal("expected allowed, got denied")
643799
} else {
644800
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})
646802
}
647803

648804
// write good config with different webhook

0 commit comments

Comments
 (0)