Skip to content

Commit

Permalink
Add integration test exercising webhook selector authz
Browse files Browse the repository at this point in the history
  • Loading branch information
liggitt committed Jul 19, 2024
1 parent 9f8f367 commit 5f22dd7
Showing 1 changed file with 159 additions and 3 deletions.
162 changes: 159 additions & 3 deletions test/integration/auth/authz_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import (
"testing"
"time"

"github.com/google/go-cmp/cmp"

authorizationv1 "k8s.io/api/authorization/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -125,6 +127,7 @@ func TestMultiWebhookAuthzConfig(t *testing.T) {
authzmetrics.ResetMetricsForTest()
defer authzmetrics.ResetMetricsForTest()
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthorizeWithSelectors, true)

dir := t.TempDir()

Expand Down Expand Up @@ -253,6 +256,41 @@ users:
t.Fatal(err)
}

// returns allow responses when called with a label selector containing an allow key, and records the selectors it saw
selectorName := "selector.example.com"
serverSelectorCalled := atomic.Int32{}
var selectorLabelAttributes *authorizationv1.LabelSelectorAttributes
var selectorFieldAttributes *authorizationv1.FieldSelectorAttributes
selectorServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
selectorLabelAttributes = nil
selectorFieldAttributes = nil
serverSelectorCalled.Add(1)
sar := &authorizationv1.SubjectAccessReview{}
if err := json.NewDecoder(req.Body).Decode(sar); err != nil {
t.Error(err)
}
t.Log("selector", sar)
if sar.Spec.ResourceAttributes != nil {
selectorLabelAttributes = sar.Spec.ResourceAttributes.LabelSelector.DeepCopy()
selectorFieldAttributes = sar.Spec.ResourceAttributes.FieldSelector.DeepCopy()
if sar.Spec.ResourceAttributes.LabelSelector != nil {
for _, req := range sar.Spec.ResourceAttributes.LabelSelector.Requirements {
if req.Key == "allow" {
sar.Status.Allowed = true
}
}
}
}
if err := json.NewEncoder(w).Encode(sar); err != nil {
t.Error(err)
}
}))
defer selectorServer.Close()
serverSelectorKubeconfigName := filepath.Join(dir, "selector.yaml")
if err := os.WriteFile(serverSelectorKubeconfigName, []byte(fmt.Sprintf(kubeconfigTemplate, selectorServer.URL)), os.FileMode(0644)); err != nil {
t.Fatal(err)
}

// returns an allow response when called
allowName := "allow.example.com"
serverAllowCalled := atomic.Int32{}
Expand Down Expand Up @@ -303,6 +341,7 @@ users:
serverDenyCalled.Store(0)
serverNoOpinionCalled.Store(0)
serverFailOpenCalled.Store(0)
serverSelectorCalled.Store(0)
serverAllowCalled.Store(0)
serverAllowReloadedCalled.Store(0)
authorizationmetrics.ResetMetricsForTest()
Expand All @@ -311,7 +350,7 @@ users:
}
var adminClient *clientset.Clientset
type counts struct {
errorCount, timeoutCount, denyCount, noOpinionCount, failOpenCount, allowCount, allowReloadedCount, webhookExclusionCount, evalErrorsCount int32
errorCount, timeoutCount, denyCount, noOpinionCount, failOpenCount, selectorCount, allowCount, allowReloadedCount, webhookExclusionCount, evalErrorsCount int32
}
assertCounts := func(c counts) {
t.Helper()
Expand Down Expand Up @@ -346,6 +385,7 @@ users:
if e, a := c.denyCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: denyName}]["denied"]; e != int32(a) {
t.Fatalf("expected deny webhook denied metrics calls: %d, got %d", e, a)
}
assertCount(selectorName, c.selectorCount, &serverSelectorCalled)
assertCount(noOpinionName, c.noOpinionCount, &serverNoOpinionCalled)
assertCount(failOpenName, c.failOpenCount, &serverFailOpenCalled)
expectedFailOpenCounts := map[string]int{}
Expand All @@ -355,6 +395,7 @@ users:
if !reflect.DeepEqual(expectedFailOpenCounts, metrics.whFailOpenTotal) {
t.Fatalf("expected fail open %#v, got %#v", expectedFailOpenCounts, metrics.whFailOpenTotal)
}

assertCount(allowName, c.allowCount, &serverAllowCalled)
if e, a := c.allowCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: allowName}]["allowed"]; e != int32(a) {
t.Fatalf("expected allow webhook allowed metrics calls: %d, got %d", e, a)
Expand Down Expand Up @@ -428,6 +469,21 @@ authorizers:
- expression: has(request.resourceAttributes)
- expression: 'request.resourceAttributes.namespace == "fail"'
- type: Webhook
name: `+selectorName+`
webhook:
timeout: 5s
failurePolicy: NoOpinion
subjectAccessReviewVersion: v1
matchConditionSubjectAccessReviewVersion: v1
authorizedTTL: 1ms
unauthorizedTTL: 1ms
connectionInfo:
type: KubeConfigFile
kubeConfigFile: `+serverSelectorKubeconfigName+`
matchConditions:
- expression: request.?resourceAttributes.labelSelector.requirements.orValue([]).exists(r, r.key=='testselector')
- type: Webhook
name: `+noOpinionName+`
webhook:
Expand All @@ -453,6 +509,7 @@ authorizers:
type: KubeConfigFile
kubeConfigFile: `+serverFailOpenKubeconfigName+`
- type: Webhook
name: `+allowName+`
webhook:
Expand All @@ -478,6 +535,10 @@ authorizers:

adminClient = clientset.NewForConfigOrDie(server.ClientConfig)

impersonationConfig := rest.CopyConfig(server.ClientConfig)
impersonationConfig.Impersonate.UserName = "alice"
aliceClient := clientset.NewForConfigOrDie(impersonationConfig)

// malformed webhook short circuits
t.Log("checking error")
if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{
Expand Down Expand Up @@ -559,7 +620,7 @@ authorizers:
t.Fatal("expected allowed, got denied")
} else {
t.Log(result.Status.Reason)
assertCounts(counts{noOpinionCount: 1, failOpenCount: 1, allowCount: 1, webhookExclusionCount: 3})
assertCounts(counts{noOpinionCount: 1, failOpenCount: 1, allowCount: 1, webhookExclusionCount: 4})
}

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

disorderedFieldSelector := "spec.nodeName=mynode,metadata.name!=b,metadata.name!=a"
orderedFieldRequirements := &authorizationv1.FieldSelectorAttributes{Requirements: []metav1.FieldSelectorRequirement{
{Key: "metadata.name", Operator: "NotIn", Values: []string{"a"}},
{Key: "metadata.name", Operator: "NotIn", Values: []string{"b"}},
{Key: "spec.nodeName", Operator: "In", Values: []string{"mynode"}}}}
disorderedFieldRequirements := &authorizationv1.FieldSelectorAttributes{Requirements: []metav1.FieldSelectorRequirement{
{Key: "spec.nodeName", Operator: "In", Values: []string{"mynode"}},
{Key: "metadata.name", Operator: "NotIn", Values: []string{"b"}},
{Key: "metadata.name", Operator: "NotIn", Values: []string{"a"}}}}
disorderedUnknownFieldRequirements := disorderedFieldRequirements.DeepCopy()
disorderedUnknownFieldRequirements.Requirements = append(disorderedUnknownFieldRequirements.Requirements, metav1.FieldSelectorRequirement{Key: "x", Operator: "Unknown"})
disorderedLabelSelector := "testselector in (b,a),allow=true"
orderedLabelRequirements := &authorizationv1.LabelSelectorAttributes{Requirements: []metav1.LabelSelectorRequirement{
{Key: "allow", Operator: "In", Values: []string{"true"}},
{Key: "testselector", Operator: "In", Values: []string{"a", "b"}}}}
disorderedLabelRequirements := &authorizationv1.LabelSelectorAttributes{Requirements: []metav1.LabelSelectorRequirement{
{Key: "testselector", Operator: "In", Values: []string{"b", "a"}},
{Key: "allow", Operator: "In", Values: []string{"true"}}}}
disorderedUnknownLabelRequirements := disorderedLabelRequirements.DeepCopy()
disorderedUnknownLabelRequirements.Requirements = append(disorderedUnknownLabelRequirements.Requirements, metav1.LabelSelectorRequirement{Key: "x", Operator: "Unknown"})

// make request matching selector webhook matchCondition
// check fieldSelector and labelSelector are parsed and normalized
_, err := aliceClient.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{FieldSelector: disorderedFieldSelector, LabelSelector: disorderedLabelSelector})
assertCounts(counts{selectorCount: 1, webhookExclusionCount: 3})
if err != nil {
t.Fatalf("expected success, got error: %v", err)
}
if e, a := orderedFieldRequirements.DeepCopy(), selectorFieldAttributes; !reflect.DeepEqual(e, a) {
t.Fatalf("unexpected diff:\n%s", cmp.Diff(a, e))
}
if e, a := orderedLabelRequirements.DeepCopy(), selectorLabelAttributes; !reflect.DeepEqual(e, a) {
t.Fatalf("unexpected diff:\n%s", cmp.Diff(a, e))
}
selectorFieldAttributes = nil
selectorLabelAttributes = nil

// make subjectaccessreview request containing fieldSelector and labelSelector requirements
// check known fieldSelector and labelSelector requirements get passed through to the webhook as-is
if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{
User: "alice",
ResourceAttributes: &authorizationv1.ResourceAttributes{
Verb: "list",
Version: "v1",
Resource: "pods",
FieldSelector: disorderedUnknownFieldRequirements.DeepCopy(),
LabelSelector: disorderedUnknownLabelRequirements.DeepCopy(),
},
}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
} else if !result.Status.Allowed {
t.Fatal("expected allowed, got denied")
} else {
t.Log(result.Status.Reason)
t.Log(result.Status.EvaluationError)
assertCounts(counts{selectorCount: 1, webhookExclusionCount: 3})
if e, a := disorderedFieldRequirements.DeepCopy(), selectorFieldAttributes; !reflect.DeepEqual(e, a) {
t.Fatalf("unexpected diff:\n%s", cmp.Diff(a, e))
}
if e, a := disorderedLabelRequirements.DeepCopy(), selectorLabelAttributes; !reflect.DeepEqual(e, a) {
t.Fatalf("unexpected diff:\n%s", cmp.Diff(a, e))
}
}
selectorFieldAttributes = nil
selectorLabelAttributes = nil

// make subjectaccessreview request containing fieldSelector and labelSelector rawSelector
// check fieldSelector and labelSelector rawSelector get parsed and passed to the webhook
if result, err := adminClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &authorizationv1.SubjectAccessReview{Spec: authorizationv1.SubjectAccessReviewSpec{
User: "alice",
ResourceAttributes: &authorizationv1.ResourceAttributes{
Verb: "list",
Version: "v1",
Resource: "pods",
FieldSelector: &authorizationv1.FieldSelectorAttributes{RawSelector: disorderedFieldSelector},
LabelSelector: &authorizationv1.LabelSelectorAttributes{RawSelector: disorderedLabelSelector},
},
}}, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
} else if !result.Status.Allowed {
t.Fatal("expected allowed, got denied")
} else {
t.Log(result.Status.Reason)
t.Log(result.Status.EvaluationError)
assertCounts(counts{selectorCount: 1, webhookExclusionCount: 3})
if e, a := orderedFieldRequirements.DeepCopy(), selectorFieldAttributes; !reflect.DeepEqual(e, a) {
t.Fatalf("unexpected diff:\n%s", cmp.Diff(a, e))
}
if e, a := orderedLabelRequirements.DeepCopy(), selectorLabelAttributes; !reflect.DeepEqual(e, a) {
t.Fatalf("unexpected diff:\n%s", cmp.Diff(a, e))
}
}
selectorFieldAttributes = nil
selectorLabelAttributes = nil

// check last loaded success/failure metric timestamps, ensure success is present, failure is not
initialMetrics, err := getMetrics(t, adminClient)
if err != nil {
Expand Down Expand Up @@ -642,7 +798,7 @@ authorizers:
t.Fatal("expected allowed, got denied")
} else {
t.Log(result.Status.Reason)
assertCounts(counts{noOpinionCount: 1, failOpenCount: 1, allowCount: 1, webhookExclusionCount: 3})
assertCounts(counts{noOpinionCount: 1, failOpenCount: 1, allowCount: 1, webhookExclusionCount: 4})
}

// write good config with different webhook
Expand Down

0 comments on commit 5f22dd7

Please sign in to comment.