Skip to content

Commit 0112d91

Browse files
committed
Add multi-webhook integration test
1 parent 44d89c8 commit 0112d91

File tree

1 file changed

+323
-0
lines changed

1 file changed

+323
-0
lines changed

test/integration/auth/authz_config_test.go

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ package auth
1818

1919
import (
2020
"context"
21+
"encoding/json"
22+
"fmt"
23+
"net/http"
24+
"net/http/httptest"
2125
"os"
2226
"path/filepath"
27+
"sync/atomic"
2328
"testing"
29+
"time"
2430

2531
authorizationv1 "k8s.io/api/authorization/v1"
2632
rbacv1 "k8s.io/api/rbac/v1"
@@ -93,3 +99,320 @@ authorizers:
9399
t.Fatal("expected allowed, got denied")
94100
}
95101
}
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

Comments
 (0)