Skip to content

Commit d53adc1

Browse files
committed
app: add unit tests for the authorizer setup
In particular it seems to be useful to verify the order of authorization as well.
1 parent 670377f commit d53adc1

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
/*
2+
Copyright 2025 the kube-rbac-proxy maintainers. All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package app
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"net/http"
23+
"testing"
24+
25+
"github.com/brancz/kube-rbac-proxy/pkg/authorization"
26+
"github.com/brancz/kube-rbac-proxy/pkg/authorization/rewrite"
27+
"github.com/brancz/kube-rbac-proxy/pkg/authorization/static"
28+
"github.com/brancz/kube-rbac-proxy/pkg/server"
29+
"k8s.io/apiserver/pkg/authentication/user"
30+
"k8s.io/apiserver/pkg/authorization/authorizer"
31+
serverconfig "k8s.io/apiserver/pkg/server"
32+
)
33+
34+
type mockAuthorizer func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error)
35+
36+
func (m mockAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
37+
return m(ctx, a)
38+
}
39+
40+
func TestSetupAuthorizer_AllowPathsWithRewriteAndStaticAuth(t *testing.T) {
41+
mockDelegated := mockAuthorizer(
42+
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
43+
if a.GetUser().GetName() == "system:serviceaccount:default:client-with-rbac" {
44+
return authorizer.DecisionAllow, "delegated allow", nil
45+
}
46+
return authorizer.DecisionNoOpinion, "i don't care", nil
47+
},
48+
)
49+
50+
mockErr := mockAuthorizer(
51+
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
52+
return authorizer.DecisionDeny, "delegated deny", fmt.Errorf("delegated error")
53+
},
54+
)
55+
56+
krbInfo := &server.KubeRBACProxyInfo{
57+
AllowPaths: []string{"/metrics"},
58+
Authorization: &authorization.AuthzConfig{
59+
RewriteAttributesConfig: &rewrite.RewriteAttributesConfig{
60+
Rewrites: &rewrite.SubjectAccessReviewRewrites{
61+
ByHTTPHeader: &rewrite.HTTPHeaderRewriteConfig{
62+
Name: "x-namespace",
63+
},
64+
},
65+
ResourceAttributes: &rewrite.ResourceAttributes{
66+
Resource: "namespaces",
67+
Namespace: "{{ .Value }}",
68+
},
69+
},
70+
Static: []static.StaticAuthorizationConfig{
71+
{
72+
User: static.UserConfig{
73+
Name: "system:serviceaccount:default:client-with-static",
74+
},
75+
ResourceRequest: true,
76+
Resource: "namespaces",
77+
Namespace: "kube-system",
78+
Verb: "get",
79+
},
80+
},
81+
},
82+
}
83+
84+
for _, tt := range []struct {
85+
name string
86+
user string
87+
verb string
88+
path string
89+
headerValue string
90+
mockAuthz authorizer.Authorizer
91+
expectDecision authorizer.Decision
92+
expectAuthzError bool
93+
}{
94+
{
95+
name: "non-allow-path should be denied",
96+
user: "system:serviceaccount:default:client-with-static",
97+
verb: "get",
98+
path: "/forbidden",
99+
expectDecision: authorizer.DecisionDeny,
100+
},
101+
{
102+
name: "allow-path with static auth match should be allowed",
103+
user: "system:serviceaccount:default:client-with-static",
104+
verb: "get",
105+
path: "/metrics",
106+
headerValue: "kube-system",
107+
expectDecision: authorizer.DecisionAllow,
108+
},
109+
{
110+
name: "allow-path with no static match but delegated match should be allowed",
111+
user: "system:serviceaccount:default:client-with-rbac",
112+
verb: "get",
113+
path: "/metrics",
114+
headerValue: "default",
115+
expectDecision: authorizer.DecisionAllow,
116+
},
117+
{
118+
name: "allow-path with no static match and no delegated match should be denied",
119+
user: "other-user",
120+
verb: "get",
121+
path: "/metrics",
122+
headerValue: "other-namespace",
123+
expectDecision: authorizer.DecisionDeny, // The krp logic is to deny on no-opinion
124+
},
125+
{
126+
name: "everything looks fine, except that we receive an error from the delegated authz",
127+
user: "system:serviceaccount:default:client-with-rbac",
128+
verb: "get",
129+
path: "/metrics",
130+
headerValue: "default",
131+
mockAuthz: mockErr,
132+
expectDecision: authorizer.DecisionDeny,
133+
expectAuthzError: true,
134+
},
135+
} {
136+
t.Run(tt.name, func(t *testing.T) {
137+
if tt.name == "allow-path with no static match and no delegated match should be denied" {
138+
fmt.Println("watch me")
139+
}
140+
141+
if tt.mockAuthz == nil {
142+
tt.mockAuthz = mockDelegated
143+
}
144+
145+
authz, err := setupAuthorizer(krbInfo, &serverconfig.AuthorizationInfo{
146+
Authorizer: tt.mockAuthz,
147+
})
148+
if err != nil {
149+
t.Fatalf("setupAuthorizer failed: %v", err)
150+
}
151+
152+
req, _ := http.NewRequest("GET", "http://example.com"+tt.path, nil)
153+
if tt.headerValue != "" {
154+
req.Header.Set("x-namespace", tt.headerValue)
155+
}
156+
157+
params := []string{}
158+
if tt.headerValue != "" {
159+
params = append(params, tt.headerValue)
160+
}
161+
ctx := rewrite.WithKubeRBACProxyParams(context.Background(), params)
162+
163+
attr := authorizer.AttributesRecord{
164+
User: &user.DefaultInfo{Name: tt.user},
165+
Verb: tt.verb,
166+
Path: tt.path,
167+
ResourceRequest: false,
168+
}
169+
170+
decision, reason, err := authz.Authorize(ctx, &attr)
171+
if err != nil && !tt.expectAuthzError {
172+
t.Fatalf("Authorization failed: %v", err)
173+
}
174+
if err == nil && tt.expectAuthzError {
175+
t.Fatalf("Authorization should have failed, but didn't")
176+
}
177+
if decision != tt.expectDecision {
178+
t.Errorf("Expected decision %v, got %v (reason: %s)", tt.expectDecision, decision, reason)
179+
}
180+
})
181+
}
182+
}
183+
184+
func TestSetupAuthorizer_IgnorePathsWithResourceAttributes(t *testing.T) {
185+
mockDelegated := mockAuthorizer(
186+
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
187+
if a.GetUser().GetName() == "system:serviceaccount:default:client-with-rbac" {
188+
return authorizer.DecisionAllow, "delegated allow", nil
189+
}
190+
return authorizer.DecisionNoOpinion, "i don't care", nil
191+
},
192+
)
193+
194+
krbInfo := &server.KubeRBACProxyInfo{
195+
IgnorePaths: []string{"/healthz"},
196+
Authorization: &authorization.AuthzConfig{
197+
RewriteAttributesConfig: &rewrite.RewriteAttributesConfig{
198+
ResourceAttributes: &rewrite.ResourceAttributes{
199+
Resource: "namespaces",
200+
Namespace: "kube-system",
201+
},
202+
},
203+
Static: []static.StaticAuthorizationConfig{
204+
{
205+
User: static.UserConfig{
206+
Name: "system:serviceaccount:default:client-with-static",
207+
},
208+
ResourceRequest: true,
209+
Resource: "namespaces",
210+
Namespace: "kube-system",
211+
Verb: "get",
212+
},
213+
},
214+
},
215+
}
216+
217+
delegatedAuthz := &serverconfig.AuthorizationInfo{
218+
Authorizer: mockDelegated,
219+
}
220+
221+
authz, err := setupAuthorizer(krbInfo, delegatedAuthz)
222+
if err != nil {
223+
t.Fatalf("setupAuthorizer failed: %v", err)
224+
}
225+
226+
for _, tt := range []struct {
227+
name string
228+
user user.Info
229+
verb string
230+
path string
231+
expectDecision authorizer.Decision
232+
}{
233+
{
234+
name: "ignore-path should be allowed",
235+
user: &user.DefaultInfo{Name: "any-user"},
236+
verb: "get",
237+
path: "/healthz",
238+
expectDecision: authorizer.DecisionAllow,
239+
},
240+
{
241+
name: "non-ignore-path with static auth match should be allowed",
242+
user: &user.DefaultInfo{Name: "system:serviceaccount:default:client-with-static"},
243+
verb: "get",
244+
path: "/metrics",
245+
expectDecision: authorizer.DecisionAllow,
246+
},
247+
{
248+
name: "non-ignore-path with no static match but delegated match should be allowed",
249+
user: &user.DefaultInfo{Name: "system:serviceaccount:default:client-with-rbac"},
250+
verb: "get",
251+
path: "/metrics",
252+
expectDecision: authorizer.DecisionAllow,
253+
},
254+
{
255+
name: "non-ignore-path with no static and no delegated match should be denied",
256+
user: &user.DefaultInfo{Name: "unknown-user"},
257+
verb: "get",
258+
path: "/metrics",
259+
expectDecision: authorizer.DecisionDeny,
260+
},
261+
} {
262+
t.Run(tt.name, func(t *testing.T) {
263+
ctx := context.Background()
264+
265+
attr := authorizer.AttributesRecord{
266+
User: tt.user,
267+
Verb: tt.verb,
268+
Path: tt.path,
269+
ResourceRequest: false,
270+
}
271+
272+
decision, reason, err := authz.Authorize(ctx, &attr)
273+
if err != nil {
274+
t.Fatalf("Authorization failed: %v", err)
275+
}
276+
277+
if decision != tt.expectDecision {
278+
t.Errorf("Expected decision %v, got %v (reason: %s)", tt.expectDecision, decision, reason)
279+
}
280+
})
281+
}
282+
}

0 commit comments

Comments
 (0)