1
16
17 package auth
18
19 import (
20 "bytes"
21 "fmt"
22 "io"
23 "net/http"
24 "strings"
25 "testing"
26
27 authorizationv1 "k8s.io/api/authorization/v1"
28 "k8s.io/apimachinery/pkg/runtime"
29 "k8s.io/apimachinery/pkg/runtime/schema"
30 "k8s.io/cli-runtime/pkg/genericiooptions"
31 "k8s.io/cli-runtime/pkg/printers"
32 restclient "k8s.io/client-go/rest"
33 "k8s.io/client-go/rest/fake"
34 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
35 "k8s.io/kubectl/pkg/scheme"
36 )
37
38 func TestRunAccessCheck(t *testing.T) {
39 tests := []struct {
40 name string
41 o *CanIOptions
42 args []string
43 allowed bool
44 serverErr error
45
46 expectedBodyStrings []string
47 }{
48 {
49 name: "restmapping for args",
50 o: &CanIOptions{},
51 args: []string{"get", "replicaset"},
52 allowed: true,
53 expectedBodyStrings: []string{
54 `{"resourceAttributes":{"namespace":"test","verb":"get","group":"extensions","resource":"replicasets"}}`,
55 },
56 },
57 {
58 name: "simple success",
59 o: &CanIOptions{},
60 args: []string{"get", "deployments.extensions/foo"},
61 allowed: true,
62 expectedBodyStrings: []string{
63 `{"resourceAttributes":{"namespace":"test","verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`,
64 },
65 },
66 {
67 name: "all namespaces",
68 o: &CanIOptions{
69 AllNamespaces: true,
70 },
71 args: []string{"get", "deployments.extensions/foo"},
72 allowed: true,
73 expectedBodyStrings: []string{
74 `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`,
75 },
76 },
77 {
78 name: "disallowed",
79 o: &CanIOptions{
80 AllNamespaces: true,
81 },
82 args: []string{"get", "deployments.extensions/foo"},
83 allowed: false,
84 expectedBodyStrings: []string{
85 `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`,
86 },
87 },
88 {
89 name: "forcedError",
90 o: &CanIOptions{
91 AllNamespaces: true,
92 },
93 args: []string{"get", "deployments.extensions/foo"},
94 allowed: false,
95 serverErr: fmt.Errorf("forcedError"),
96 expectedBodyStrings: []string{
97 `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`,
98 },
99 },
100 {
101 name: "sub resource",
102 o: &CanIOptions{
103 AllNamespaces: true,
104 Subresource: "log",
105 },
106 args: []string{"get", "pods"},
107 allowed: true,
108 expectedBodyStrings: []string{
109 `{"resourceAttributes":{"verb":"get","resource":"pods","subresource":"log"}}`,
110 },
111 },
112 {
113 name: "nonResourceURL",
114 o: &CanIOptions{},
115 args: []string{"get", "/logs"},
116 allowed: true,
117 expectedBodyStrings: []string{
118 `{"nonResourceAttributes":{"path":"/logs","verb":"get"}}`,
119 },
120 },
121 }
122
123 for _, test := range tests {
124 t.Run(test.name, func(t *testing.T) {
125 test.o.Out = io.Discard
126 test.o.ErrOut = io.Discard
127
128 tf := cmdtesting.NewTestFactory().WithNamespace("test")
129 defer tf.Cleanup()
130
131 ns := scheme.Codecs.WithoutConversion()
132
133 tf.Client = &fake.RESTClient{
134 GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
135 NegotiatedSerializer: ns,
136 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
137 expectPath := "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews"
138 if req.URL.Path != expectPath {
139 t.Errorf("%s: expected %v, got %v", test.name, expectPath, req.URL.Path)
140 return nil, nil
141 }
142 bodyBits, err := io.ReadAll(req.Body)
143 if err != nil {
144 t.Errorf("%s: %v", test.name, err)
145 return nil, nil
146 }
147 body := string(bodyBits)
148
149 for _, expectedBody := range test.expectedBodyStrings {
150 if !strings.Contains(body, expectedBody) {
151 t.Errorf("%s expecting %s in %s", test.name, expectedBody, body)
152 }
153 }
154
155 return &http.Response{
156 StatusCode: http.StatusOK,
157 Body: io.NopCloser(bytes.NewBufferString(
158 fmt.Sprintf(`{"kind":"SelfSubjectAccessReview","apiVersion":"authorization.k8s.io/v1","status":{"allowed":%v}}`, test.allowed),
159 )),
160 },
161 test.serverErr
162 }),
163 }
164 tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}}
165
166 if err := test.o.Complete(tf, test.args); err != nil {
167 t.Errorf("%s: %v", test.name, err)
168 return
169 }
170
171 actualAllowed, err := test.o.RunAccessCheck()
172 switch {
173 case test.serverErr == nil && err == nil:
174
175 case err != nil && test.serverErr != nil && strings.Contains(err.Error(), test.serverErr.Error()):
176
177 default:
178 t.Errorf("%s: expected %v, got %v", test.name, test.serverErr, err)
179 return
180 }
181 if actualAllowed != test.allowed {
182 t.Errorf("%s: expected %v, got %v", test.name, test.allowed, actualAllowed)
183 return
184 }
185 })
186 }
187 }
188
189 func TestRunAccessList(t *testing.T) {
190 t.Run("test access list", func(t *testing.T) {
191 options := &CanIOptions{List: true}
192 expectedOutput := "Resources Non-Resource URLs Resource Names Verbs\n" +
193 "job.* [] [test-resource] [get list]\n" +
194 "pod.* [] [test-resource] [get list]\n" +
195 " [/apis/*] [] [get]\n" +
196 " [/version] [] [get]\n"
197
198 tf := cmdtesting.NewTestFactory().WithNamespace("test")
199 defer tf.Cleanup()
200
201 ns := scheme.Codecs.WithoutConversion()
202 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
203
204 tf.Client = &fake.RESTClient{
205 GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
206 NegotiatedSerializer: ns,
207 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
208 switch req.URL.Path {
209 case "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews":
210 body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, getSelfSubjectRulesReview()))))
211 return &http.Response{StatusCode: http.StatusOK, Body: body}, nil
212 default:
213 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
214 return nil, nil
215 }
216 }),
217 }
218 ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
219 options.IOStreams = ioStreams
220 if err := options.Complete(tf, []string{}); err != nil {
221 t.Errorf("got unexpected error when do Complete(): %v", err)
222 return
223 }
224
225 err := options.RunAccessList()
226 if err != nil {
227 t.Errorf("got unexpected error when do RunAccessList(): %v", err)
228 } else if buf.String() != expectedOutput {
229 t.Errorf("expected %v\n but got %v\n", expectedOutput, buf.String())
230 }
231 })
232 }
233
234 func TestRunResourceFor(t *testing.T) {
235 tests := []struct {
236 name string
237 o *CanIOptions
238 resourceArg string
239
240 expectGVR schema.GroupVersionResource
241 expectedErrOut string
242 }{
243 {
244 name: "any resources",
245 o: &CanIOptions{},
246 resourceArg: "*",
247 expectGVR: schema.GroupVersionResource{
248 Resource: "*",
249 },
250 },
251 {
252 name: "server-supported standard resources without group",
253 o: &CanIOptions{},
254 resourceArg: "pods",
255 expectGVR: schema.GroupVersionResource{
256 Version: "v1",
257 Resource: "pods",
258 },
259 },
260 {
261 name: "server-supported standard resources with group",
262 o: &CanIOptions{},
263 resourceArg: "jobs",
264 expectGVR: schema.GroupVersionResource{
265 Group: "batch",
266 Version: "v1",
267 Resource: "jobs",
268 },
269 },
270 {
271 name: "server-supported nonstandard resources",
272 o: &CanIOptions{},
273 resourceArg: "users",
274 expectGVR: schema.GroupVersionResource{
275 Resource: "users",
276 },
277 },
278 {
279 name: "invalid resources",
280 o: &CanIOptions{},
281 resourceArg: "invalid",
282 expectGVR: schema.GroupVersionResource{
283 Resource: "invalid",
284 },
285 expectedErrOut: "Warning: the server doesn't have a resource type 'invalid'\n\n",
286 },
287 }
288
289 for _, test := range tests {
290 t.Run(test.name, func(t *testing.T) {
291 tf := cmdtesting.NewTestFactory().WithNamespace("test")
292 defer tf.Cleanup()
293
294 ioStreams, _, _, buf := genericiooptions.NewTestIOStreams()
295 test.o.IOStreams = ioStreams
296 test.o.WarningPrinter = printers.NewWarningPrinter(test.o.IOStreams.ErrOut, printers.WarningPrinterOptions{Color: false})
297
298 restMapper, err := tf.ToRESTMapper()
299 if err != nil {
300 t.Errorf("got unexpected error when do tf.ToRESTMapper(): %v", err)
301 return
302 }
303 gvr := test.o.resourceFor(restMapper, test.resourceArg)
304 if gvr != test.expectGVR {
305 t.Errorf("expected %v\n but got %v\n", test.expectGVR, gvr)
306 }
307 if buf.String() != test.expectedErrOut {
308 t.Errorf("expected %v\n but got %v\n", test.expectedErrOut, buf.String())
309 }
310 })
311 }
312 }
313
314 func getSelfSubjectRulesReview() *authorizationv1.SelfSubjectRulesReview {
315 return &authorizationv1.SelfSubjectRulesReview{
316 Status: authorizationv1.SubjectRulesReviewStatus{
317 ResourceRules: []authorizationv1.ResourceRule{
318 {
319 Verbs: []string{"get", "list"},
320 APIGroups: []string{"*"},
321 Resources: []string{"pod", "job"},
322 ResourceNames: []string{"test-resource"},
323 },
324 },
325 NonResourceRules: []authorizationv1.NonResourceRule{
326 {
327 Verbs: []string{"get"},
328 NonResourceURLs: []string{"/apis/*", "/version"},
329 },
330 },
331 },
332 }
333 }
334
View as plain text