...

Source file src/k8s.io/kubectl/pkg/cmd/auth/cani.go

Documentation: k8s.io/kubectl/pkg/cmd/auth

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     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 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  // CanIOptions is the start of the data required to perform the operation.  As new fields are added, add them here instead of
    49  // referencing the cmd.Flags()
    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  	// holds all the server-supported resources that cannot be discovered by clients. i.e. users and groups for the impersonate verb
   109  	nonStandardResourceNames = sets.NewString("users", "groups")
   110  )
   111  
   112  // NewCmdCanI returns an initialized Command for 'auth can-i' sub command
   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  // Complete completes all the required options
   152  func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error {
   153  	// Set default WarningPrinter if not already set.
   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  // Validate makes sure provided values for CanIOptions are valid
   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  // RunAccessList lists all the access current user has
   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  // RunAccessCheck checks if user has access to a certain resource or non resource URL
   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