diff --git a/pkg/kubectl/cmd/auth/BUILD b/pkg/kubectl/cmd/auth/BUILD index 03b02da1b9..dacb97ed84 100644 --- a/pkg/kubectl/cmd/auth/BUILD +++ b/pkg/kubectl/cmd/auth/BUILD @@ -17,7 +17,10 @@ go_library( ], deps = [ "//pkg/kubectl/cmd/util:go_default_library", + "//pkg/kubectl/describe/versioned:go_default_library", "//pkg/kubectl/scheme:go_default_library", + "//pkg/kubectl/util/printers:go_default_library", + "//pkg/kubectl/util/rbac:go_default_library", "//pkg/kubectl/util/templates:go_default_library", "//pkg/registry/rbac/reconciliation:go_default_library", "//staging/src/k8s.io/api/authorization/v1:go_default_library", @@ -27,6 +30,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/printers:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource:go_default_library", @@ -60,7 +64,10 @@ go_test( deps = [ "//pkg/kubectl/cmd/testing:go_default_library", "//pkg/kubectl/scheme:go_default_library", + "//staging/src/k8s.io/api/authorization/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/rest/fake:go_default_library", ], diff --git a/pkg/kubectl/cmd/auth/cani.go b/pkg/kubectl/cmd/auth/cani.go index 5b20a240a5..4aa28c18c4 100644 --- a/pkg/kubectl/cmd/auth/cani.go +++ b/pkg/kubectl/cmd/auth/cani.go @@ -19,18 +19,25 @@ package auth import ( "errors" "fmt" + "io" "io/ioutil" "os" + "sort" "strings" "github.com/spf13/cobra" authorizationv1 "k8s.io/api/authorization/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" + utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + describeutil "k8s.io/kubernetes/pkg/kubectl/describe/versioned" + "k8s.io/kubernetes/pkg/kubectl/util/printers" + rbacutil "k8s.io/kubernetes/pkg/kubectl/util/rbac" "k8s.io/kubernetes/pkg/kubectl/util/templates" ) @@ -39,14 +46,16 @@ import ( type CanIOptions struct { AllNamespaces bool Quiet bool + NoHeaders bool Namespace string - SelfSARClient authorizationv1client.SelfSubjectAccessReviewsGetter + AuthClient authorizationv1client.AuthorizationV1Interface Verb string Resource schema.GroupVersionResource NonResourceURL string Subresource string ResourceName string + List bool genericclioptions.IOStreams } @@ -77,7 +86,10 @@ var ( kubectl auth can-i get pods --subresource=log # Check to see if I can access the URL /logs/ - kubectl auth can-i get /logs/`) + kubectl auth can-i get /logs/ + + # List all allowed actions in namespace "foo" + kubectl auth can-i --list --namespace=foo`) ) // NewCmdCanI returns an initialized Command for 'auth can-i' sub command @@ -95,14 +107,18 @@ func NewCmdCanI(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, args)) cmdutil.CheckErr(o.Validate()) - - allowed, err := o.RunAccessCheck() - if err == nil { - if !allowed { - os.Exit(1) + var err error + if o.List { + err = o.RunAccessList() + } else { + var allowed bool + allowed, err = o.RunAccessCheck() + if err == nil { + if !allowed { + os.Exit(1) + } } } - cmdutil.CheckErr(err) }, } @@ -110,33 +126,41 @@ func NewCmdCanI(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If true, check the specified action in all namespaces.") cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "If true, suppress output and just return the exit code.") cmd.Flags().StringVar(&o.Subresource, "subresource", o.Subresource, "SubResource such as pod/log or deployment/scale") + cmd.Flags().BoolVar(&o.List, "list", o.List, "If true, prints all allowed actions.") + cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If true, prints allowed actions without headers") return cmd } // Complete completes all the required options func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error { - if o.Quiet { - o.Out = ioutil.Discard - } + if o.List { + if len(args) != 0 { + return errors.New("list option must be specified with no arguments") + } + } else { + if o.Quiet { + o.Out = ioutil.Discard + } - switch len(args) { - case 2: - o.Verb = args[0] - if strings.HasPrefix(args[1], "/") { - o.NonResourceURL = args[1] - break + switch len(args) { + case 2: + o.Verb = args[0] + if strings.HasPrefix(args[1], "/") { + o.NonResourceURL = args[1] + break + } + resourceTokens := strings.SplitN(args[1], "/", 2) + restMapper, err := f.ToRESTMapper() + if err != nil { + return err + } + o.Resource = o.resourceFor(restMapper, resourceTokens[0]) + if len(resourceTokens) > 1 { + o.ResourceName = resourceTokens[1] + } + default: + return errors.New("you must specify two or three arguments: verb, resource, and optional resourceName") } - resourceTokens := strings.SplitN(args[1], "/", 2) - restMapper, err := f.ToRESTMapper() - if err != nil { - return err - } - o.Resource = o.resourceFor(restMapper, resourceTokens[0]) - if len(resourceTokens) > 1 { - o.ResourceName = resourceTokens[1] - } - default: - return errors.New("you must specify two or three arguments: verb, resource, and optional resourceName") } var err error @@ -144,8 +168,7 @@ func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error { if err != nil { return err } - o.SelfSARClient = client.AuthorizationV1() - + o.AuthClient = client.AuthorizationV1() o.Namespace = "" if !o.AllNamespaces { o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() @@ -159,6 +182,13 @@ func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error { // Validate makes sure provided values for CanIOptions are valid func (o *CanIOptions) Validate() error { + if o.List { + if o.Quiet || o.AllNamespaces || o.Subresource != "" { + return errors.New("list option can't be specified with neither quiet, all-namespaces nor subresource options") + } + return nil + } + if o.NonResourceURL != "" { if o.Subresource != "" { return fmt.Errorf("--subresource can not be used with NonResourceURL") @@ -167,9 +197,28 @@ func (o *CanIOptions) Validate() error { return fmt.Errorf("NonResourceURL and ResourceName can not specified together") } } + + if o.NoHeaders { + return fmt.Errorf("--no-headers cannot be set without --list specified") + } return nil } +// RunAccessList lists all the access current user has +func (o *CanIOptions) RunAccessList() error { + sar := &authorizationv1.SelfSubjectRulesReview{ + Spec: authorizationv1.SelfSubjectRulesReviewSpec{ + Namespace: o.Namespace, + }, + } + response, err := o.AuthClient.SelfSubjectRulesReviews().Create(sar) + if err != nil { + return err + } + + return o.printStatus(response.Status) +} + // RunAccessCheck checks if user has access to a certain resource or non resource URL func (o *CanIOptions) RunAccessCheck() (bool, error) { var sar *authorizationv1.SelfSubjectAccessReview @@ -195,10 +244,9 @@ func (o *CanIOptions) RunAccessCheck() (bool, error) { }, }, } - } - response, err := o.SelfSARClient.SelfSubjectAccessReviews().Create(sar) + response, err := o.AuthClient.SelfSubjectAccessReviews().Create(sar) if err != nil { return false, err } @@ -244,3 +292,71 @@ func (o *CanIOptions) resourceFor(mapper meta.RESTMapper, resourceArg string) sc return gvr } + +func (o *CanIOptions) printStatus(status authorizationv1.SubjectRulesReviewStatus) error { + if status.Incomplete { + fmt.Fprintf(o.ErrOut, "warning: the list may be incomplete: %v\n", status.EvaluationError) + } + + breakdownRules := []rbacv1.PolicyRule{} + for _, rule := range convertToPolicyRule(status) { + breakdownRules = append(breakdownRules, rbacutil.BreakdownRule(rule)...) + } + + compactRules, err := rbacutil.CompactRules(breakdownRules) + if err != nil { + return err + } + sort.Stable(rbacutil.SortableRuleSlice(compactRules)) + + w := printers.GetNewTabWriter(o.Out) + defer w.Flush() + + allErrs := []error{} + if !o.NoHeaders { + if err := printAccessHeaders(w); err != nil { + allErrs = append(allErrs, err) + } + } + + if err := printAccess(w, compactRules); err != nil { + allErrs = append(allErrs, err) + } + return utilerrors.NewAggregate(allErrs) +} + +func convertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbacv1.PolicyRule { + ret := []rbacv1.PolicyRule{} + for _, resource := range status.ResourceRules { + ret = append(ret, rbacv1.PolicyRule{ + Verbs: resource.Verbs, + APIGroups: resource.APIGroups, + Resources: resource.Resources, + ResourceNames: resource.ResourceNames, + }) + } + + for _, nonResource := range status.NonResourceRules { + ret = append(ret, rbacv1.PolicyRule{ + Verbs: nonResource.Verbs, + NonResourceURLs: nonResource.NonResourceURLs, + }) + } + + return ret +} + +func printAccessHeaders(out io.Writer) error { + columnNames := []string{"Resources", "Non-Resource URLs", "Resource Names", "Verbs"} + _, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t")) + return err +} + +func printAccess(out io.Writer, rules []rbacv1.PolicyRule) error { + for _, r := range rules { + if _, err := fmt.Fprintf(out, "%s\t%v\t%v\t%v\n", describeutil.CombineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs); err != nil { + return err + } + } + return nil +} diff --git a/pkg/kubectl/cmd/auth/cani_test.go b/pkg/kubectl/cmd/auth/cani_test.go index 325485497c..b553356b45 100644 --- a/pkg/kubectl/cmd/auth/cani_test.go +++ b/pkg/kubectl/cmd/auth/cani_test.go @@ -24,7 +24,10 @@ import ( "strings" "testing" + authorizationv1 "k8s.io/api/authorization/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" @@ -181,3 +184,69 @@ func TestRunAccessCheck(t *testing.T) { }) } } + +func TestRunAccessList(t *testing.T) { + t.Run("test access list", func(t *testing.T) { + options := &CanIOptions{List: true} + expectedOutput := "Resources Non-Resource URLs Resource Names Verbs\n" + + "job.* [] [test-resource] [get list]\n" + + "pod.* [] [test-resource] [get list]\n" + + " [/apis/*] [] [get]\n" + + " [/version] [] [get]\n" + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + + ns := scheme.Codecs + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + tf.Client = &fake.RESTClient{ + GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews": + body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, getSelfSubjectRulesReview())))) + return &http.Response{StatusCode: http.StatusOK, Body: body}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams() + options.IOStreams = ioStreams + if err := options.Complete(tf, []string{}); err != nil { + t.Errorf("got unexpected error when do Complete(): %v", err) + return + } + + err := options.RunAccessList() + if err != nil { + t.Errorf("got unexpected error when do RunAccessList(): %v", err) + } else if buf.String() != expectedOutput { + t.Errorf("expected %v\n but got %v\n", expectedOutput, buf.String()) + } + }) +} + +func getSelfSubjectRulesReview() *authorizationv1.SelfSubjectRulesReview { + return &authorizationv1.SelfSubjectRulesReview{ + Status: authorizationv1.SubjectRulesReviewStatus{ + ResourceRules: []authorizationv1.ResourceRule{ + { + Verbs: []string{"get", "list"}, + APIGroups: []string{"*"}, + Resources: []string{"pod", "job"}, + ResourceNames: []string{"test-resource"}, + }, + }, + NonResourceRules: []authorizationv1.NonResourceRule{ + { + Verbs: []string{"get"}, + NonResourceURLs: []string{"/apis/*", "/version"}, + }, + }, + }, + } +} diff --git a/pkg/kubectl/describe/versioned/describe.go b/pkg/kubectl/describe/versioned/describe.go index 6add1c2123..31866dbf61 100644 --- a/pkg/kubectl/describe/versioned/describe.go +++ b/pkg/kubectl/describe/versioned/describe.go @@ -2677,7 +2677,7 @@ func (d *RoleDescriber) Describe(namespace, name string, describerSettings descr w.Write(LEVEL_1, "Resources\tNon-Resource URLs\tResource Names\tVerbs\n") w.Write(LEVEL_1, "---------\t-----------------\t--------------\t-----\n") for _, r := range compactRules { - w.Write(LEVEL_1, "%s\t%v\t%v\t%v\n", combineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs) + w.Write(LEVEL_1, "%s\t%v\t%v\t%v\n", CombineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs) } return nil @@ -2716,14 +2716,14 @@ func (d *ClusterRoleDescriber) Describe(namespace, name string, describerSetting w.Write(LEVEL_1, "Resources\tNon-Resource URLs\tResource Names\tVerbs\n") w.Write(LEVEL_1, "---------\t-----------------\t--------------\t-----\n") for _, r := range compactRules { - w.Write(LEVEL_1, "%s\t%v\t%v\t%v\n", combineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs) + w.Write(LEVEL_1, "%s\t%v\t%v\t%v\n", CombineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs) } return nil }) } -func combineResourceGroup(resource, group []string) string { +func CombineResourceGroup(resource, group []string) string { if len(resource) == 0 { return "" }