1
16
17 package auth
18
19 import (
20 "context"
21 "errors"
22 "fmt"
23 "io"
24 "os"
25 "sort"
26 "strings"
27
28 "github.com/spf13/cobra"
29
30 authorizationv1 "k8s.io/api/authorization/v1"
31 rbacv1 "k8s.io/api/rbac/v1"
32 "k8s.io/apimachinery/pkg/api/meta"
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/apimachinery/pkg/runtime/schema"
35 utilerrors "k8s.io/apimachinery/pkg/util/errors"
36 "k8s.io/apimachinery/pkg/util/sets"
37 "k8s.io/cli-runtime/pkg/genericiooptions"
38 "k8s.io/cli-runtime/pkg/printers"
39 discovery "k8s.io/client-go/discovery"
40 authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
41 cmdutil "k8s.io/kubectl/pkg/cmd/util"
42 "k8s.io/kubectl/pkg/describe"
43 rbacutil "k8s.io/kubectl/pkg/util/rbac"
44 "k8s.io/kubectl/pkg/util/templates"
45 "k8s.io/kubectl/pkg/util/term"
46 )
47
48
49
50 type CanIOptions struct {
51 AllNamespaces bool
52 Quiet bool
53 NoHeaders bool
54 Namespace string
55 AuthClient authorizationv1client.AuthorizationV1Interface
56 DiscoveryClient discovery.DiscoveryInterface
57
58 Verb string
59 Resource schema.GroupVersionResource
60 NonResourceURL string
61 Subresource string
62 ResourceName string
63 List bool
64
65 genericiooptions.IOStreams
66 WarningPrinter *printers.WarningPrinter
67 }
68
69 var (
70 canILong = templates.LongDesc(`
71 Check whether an action is allowed.
72
73 VERB is a logical Kubernetes API verb like 'get', 'list', 'watch', 'delete', etc.
74 TYPE is a Kubernetes resource. Shortcuts and groups will be resolved.
75 NONRESOURCEURL is a partial URL that starts with "/".
76 NAME is the name of a particular Kubernetes resource.
77 This command pairs nicely with impersonation. See --as global flag.`)
78
79 canIExample = templates.Examples(`
80 # Check to see if I can create pods in any namespace
81 kubectl auth can-i create pods --all-namespaces
82
83 # Check to see if I can list deployments in my current namespace
84 kubectl auth can-i list deployments.apps
85
86 # Check to see if service account "foo" of namespace "dev" can list pods
87 # in the namespace "prod".
88 # You must be allowed to use impersonation for the global option "--as".
89 kubectl auth can-i list pods --as=system:serviceaccount:dev:foo -n prod
90
91 # Check to see if I can do everything in my current namespace ("*" means all)
92 kubectl auth can-i '*' '*'
93
94 # Check to see if I can get the job named "bar" in namespace "foo"
95 kubectl auth can-i list jobs.batch/bar -n foo
96
97 # Check to see if I can read pod logs
98 kubectl auth can-i get pods --subresource=log
99
100 # Check to see if I can access the URL /logs/
101 kubectl auth can-i get /logs/
102
103 # List all allowed actions in namespace "foo"
104 kubectl auth can-i --list --namespace=foo`)
105
106 resourceVerbs = sets.NewString("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "use", "bind", "impersonate", "*")
107 nonResourceURLVerbs = sets.NewString("get", "put", "post", "head", "options", "delete", "patch", "*")
108
109 nonStandardResourceNames = sets.NewString("users", "groups")
110 )
111
112
113 func NewCmdCanI(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
114 o := &CanIOptions{
115 IOStreams: streams,
116 }
117
118 cmd := &cobra.Command{
119 Use: "can-i VERB [TYPE | TYPE/NAME | NONRESOURCEURL]",
120 DisableFlagsInUseLine: true,
121 Short: "Check whether an action is allowed",
122 Long: canILong,
123 Example: canIExample,
124 Run: func(cmd *cobra.Command, args []string) {
125 cmdutil.CheckErr(o.Complete(f, args))
126 cmdutil.CheckErr(o.Validate())
127 var err error
128 if o.List {
129 err = o.RunAccessList()
130 } else {
131 var allowed bool
132 allowed, err = o.RunAccessCheck()
133 if err == nil {
134 if !allowed {
135 os.Exit(1)
136 }
137 }
138 }
139 cmdutil.CheckErr(err)
140 },
141 }
142
143 cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If true, check the specified action in all namespaces.")
144 cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "If true, suppress output and just return the exit code.")
145 cmd.Flags().StringVar(&o.Subresource, "subresource", o.Subresource, "SubResource such as pod/log or deployment/scale")
146 cmd.Flags().BoolVar(&o.List, "list", o.List, "If true, prints all allowed actions.")
147 cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If true, prints allowed actions without headers")
148 return cmd
149 }
150
151
152 func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error {
153
154 if o.WarningPrinter == nil {
155 o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)})
156 }
157
158 if o.List {
159 if len(args) != 0 {
160 return errors.New("list option must be specified with no arguments")
161 }
162 } else {
163 if o.Quiet {
164 o.Out = io.Discard
165 }
166
167 switch len(args) {
168 case 2:
169 o.Verb = args[0]
170 if strings.HasPrefix(args[1], "/") {
171 o.NonResourceURL = args[1]
172 break
173 }
174 resourceTokens := strings.SplitN(args[1], "/", 2)
175 restMapper, err := f.ToRESTMapper()
176 if err != nil {
177 return err
178 }
179 o.Resource = o.resourceFor(restMapper, resourceTokens[0])
180 if len(resourceTokens) > 1 {
181 o.ResourceName = resourceTokens[1]
182 }
183 default:
184 errString := "you must specify two arguments: verb resource or verb resource/resourceName."
185 usageString := "See 'kubectl auth can-i -h' for help and examples."
186 return errors.New(fmt.Sprintf("%s\n%s", errString, usageString))
187 }
188 }
189
190 var err error
191 client, err := f.KubernetesClientSet()
192 if err != nil {
193 return err
194 }
195 o.AuthClient = client.AuthorizationV1()
196 o.DiscoveryClient = client.Discovery()
197 o.Namespace = ""
198 if !o.AllNamespaces {
199 o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
200 if err != nil {
201 return err
202 }
203 }
204
205 return nil
206 }
207
208
209 func (o *CanIOptions) Validate() error {
210 if o.List {
211 if o.Quiet || o.AllNamespaces || o.Subresource != "" {
212 return errors.New("list option can't be specified with neither quiet, all-namespaces nor subresource options")
213 }
214 return nil
215 }
216
217 if o.WarningPrinter == nil {
218 return fmt.Errorf("WarningPrinter can not be used without initialization")
219 }
220
221 if o.NonResourceURL != "" {
222 if o.Subresource != "" {
223 return fmt.Errorf("--subresource can not be used with NonResourceURL")
224 }
225 if o.Resource != (schema.GroupVersionResource{}) || o.ResourceName != "" {
226 return fmt.Errorf("NonResourceURL and ResourceName can not specified together")
227 }
228 if !isKnownNonResourceVerb(o.Verb) {
229 o.WarningPrinter.Print(fmt.Sprintf("verb '%s' is not a known verb\n", o.Verb))
230 }
231 } else if !o.Resource.Empty() && !o.AllNamespaces && o.DiscoveryClient != nil {
232 if namespaced, err := isNamespaced(o.Resource, o.DiscoveryClient); err == nil && !namespaced {
233 if len(o.Resource.Group) == 0 {
234 o.WarningPrinter.Print(fmt.Sprintf("resource '%s' is not namespace scoped\n", o.Resource.Resource))
235 } else {
236 o.WarningPrinter.Print(fmt.Sprintf("resource '%s' is not namespace scoped in group '%s'\n", o.Resource.Resource, o.Resource.Group))
237 }
238 }
239 if !isKnownResourceVerb(o.Verb) {
240 o.WarningPrinter.Print(fmt.Sprintf("verb '%s' is not a known verb\n", o.Verb))
241 }
242 }
243
244 if o.NoHeaders {
245 return fmt.Errorf("--no-headers cannot be set without --list specified")
246 }
247 return nil
248 }
249
250
251 func (o *CanIOptions) RunAccessList() error {
252 sar := &authorizationv1.SelfSubjectRulesReview{
253 Spec: authorizationv1.SelfSubjectRulesReviewSpec{
254 Namespace: o.Namespace,
255 },
256 }
257 response, err := o.AuthClient.SelfSubjectRulesReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
258 if err != nil {
259 return err
260 }
261
262 return o.printStatus(response.Status)
263 }
264
265
266 func (o *CanIOptions) RunAccessCheck() (bool, error) {
267 var sar *authorizationv1.SelfSubjectAccessReview
268 if o.NonResourceURL == "" {
269 sar = &authorizationv1.SelfSubjectAccessReview{
270 Spec: authorizationv1.SelfSubjectAccessReviewSpec{
271 ResourceAttributes: &authorizationv1.ResourceAttributes{
272 Namespace: o.Namespace,
273 Verb: o.Verb,
274 Group: o.Resource.Group,
275 Resource: o.Resource.Resource,
276 Subresource: o.Subresource,
277 Name: o.ResourceName,
278 },
279 },
280 }
281 } else {
282 sar = &authorizationv1.SelfSubjectAccessReview{
283 Spec: authorizationv1.SelfSubjectAccessReviewSpec{
284 NonResourceAttributes: &authorizationv1.NonResourceAttributes{
285 Verb: o.Verb,
286 Path: o.NonResourceURL,
287 },
288 },
289 }
290 }
291
292 response, err := o.AuthClient.SelfSubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
293 if err != nil {
294 return false, err
295 }
296 if response.Status.Allowed {
297 fmt.Fprintln(o.Out, "yes")
298 } else {
299 fmt.Fprint(o.Out, "no")
300 if len(response.Status.Reason) > 0 {
301 fmt.Fprintf(o.Out, " - %v", response.Status.Reason)
302 }
303 if len(response.Status.EvaluationError) > 0 {
304 fmt.Fprintf(o.Out, " - %v", response.Status.EvaluationError)
305 }
306 fmt.Fprintln(o.Out)
307 }
308
309 return response.Status.Allowed, nil
310 }
311
312 func (o *CanIOptions) resourceFor(mapper meta.RESTMapper, resourceArg string) schema.GroupVersionResource {
313 if resourceArg == "*" {
314 return schema.GroupVersionResource{Resource: resourceArg}
315 }
316
317 fullySpecifiedGVR, groupResource := schema.ParseResourceArg(strings.ToLower(resourceArg))
318 gvr := schema.GroupVersionResource{}
319 if fullySpecifiedGVR != nil {
320 gvr, _ = mapper.ResourceFor(*fullySpecifiedGVR)
321 }
322 if gvr.Empty() {
323 var err error
324 gvr, err = mapper.ResourceFor(groupResource.WithVersion(""))
325 if err != nil {
326 if !nonStandardResourceNames.Has(groupResource.String()) {
327 if len(groupResource.Group) == 0 {
328 o.WarningPrinter.Print(fmt.Sprintf("the server doesn't have a resource type '%s'\n", groupResource.Resource))
329 } else {
330 o.WarningPrinter.Print(fmt.Sprintf("the server doesn't have a resource type '%s' in group '%s'\n", groupResource.Resource, groupResource.Group))
331 }
332 }
333 return schema.GroupVersionResource{Resource: resourceArg}
334 }
335 }
336
337 return gvr
338 }
339
340 func (o *CanIOptions) printStatus(status authorizationv1.SubjectRulesReviewStatus) error {
341 if status.Incomplete {
342 o.WarningPrinter.Print(fmt.Sprintf("the list may be incomplete: %v", status.EvaluationError))
343 }
344
345 breakdownRules := []rbacv1.PolicyRule{}
346 for _, rule := range convertToPolicyRule(status) {
347 breakdownRules = append(breakdownRules, rbacutil.BreakdownRule(rule)...)
348 }
349
350 compactRules, err := rbacutil.CompactRules(breakdownRules)
351 if err != nil {
352 return err
353 }
354 sort.Stable(rbacutil.SortableRuleSlice(compactRules))
355
356 w := printers.GetNewTabWriter(o.Out)
357 defer w.Flush()
358
359 allErrs := []error{}
360 if !o.NoHeaders {
361 if err := printAccessHeaders(w); err != nil {
362 allErrs = append(allErrs, err)
363 }
364 }
365
366 if err := printAccess(w, compactRules); err != nil {
367 allErrs = append(allErrs, err)
368 }
369 return utilerrors.NewAggregate(allErrs)
370 }
371
372 func convertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbacv1.PolicyRule {
373 ret := []rbacv1.PolicyRule{}
374 for _, resource := range status.ResourceRules {
375 ret = append(ret, rbacv1.PolicyRule{
376 Verbs: resource.Verbs,
377 APIGroups: resource.APIGroups,
378 Resources: resource.Resources,
379 ResourceNames: resource.ResourceNames,
380 })
381 }
382
383 for _, nonResource := range status.NonResourceRules {
384 ret = append(ret, rbacv1.PolicyRule{
385 Verbs: nonResource.Verbs,
386 NonResourceURLs: nonResource.NonResourceURLs,
387 })
388 }
389
390 return ret
391 }
392
393 func printAccessHeaders(out io.Writer) error {
394 columnNames := []string{"Resources", "Non-Resource URLs", "Resource Names", "Verbs"}
395 _, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t"))
396 return err
397 }
398
399 func printAccess(out io.Writer, rules []rbacv1.PolicyRule) error {
400 for _, r := range rules {
401 if _, err := fmt.Fprintf(out, "%s\t%v\t%v\t%v\n", describe.CombineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs); err != nil {
402 return err
403 }
404 }
405 return nil
406 }
407
408 func isNamespaced(gvr schema.GroupVersionResource, discoveryClient discovery.DiscoveryInterface) (bool, error) {
409 if gvr.Resource == "*" {
410 return true, nil
411 }
412 apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(schema.GroupVersion{
413 Group: gvr.Group, Version: gvr.Version,
414 }.String())
415 if err != nil {
416 return true, err
417 }
418
419 for _, resource := range apiResourceList.APIResources {
420 if resource.Name == gvr.Resource {
421 return resource.Namespaced, nil
422 }
423 }
424
425 return false, fmt.Errorf("the server doesn't have a resource type '%s' in group '%s'", gvr.Resource, gvr.Group)
426 }
427
428 func isKnownResourceVerb(s string) bool {
429 return resourceVerbs.Has(s)
430 }
431
432 func isKnownNonResourceVerb(s string) bool {
433 return nonResourceURLVerbs.Has(s)
434 }
435
View as plain text