Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kubectl auth can-i --list option which could help users know what actions they can do in specific namespace #64820

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/kubectl/cmd/auth/BUILD
Expand Up @@ -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",
Expand All @@ -26,6 +29,7 @@ go_library(
"//staging/src/k8s.io/api/rbac/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/meta: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",
Expand Down Expand Up @@ -59,7 +63,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",
],
Expand Down
178 changes: 147 additions & 31 deletions pkg/kubectl/cmd/auth/cani.go
Expand Up @@ -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"
)

Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -95,57 +107,68 @@ 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)
},
}

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
}

switch len(args) {
case 2:
o.Verb = args[0]
if strings.HasPrefix(args[1], "/") {
o.NonResourceURL = args[1]
break
if o.List {
if len(args) != 0 {
return errors.New("list option must be specified with no arguments")
}
resourceTokens := strings.SplitN(args[1], "/", 2)
restMapper, err := f.ToRESTMapper()
if err != nil {
return err
} else {
if o.Quiet {
o.Out = ioutil.Discard
}
o.Resource = o.resourceFor(restMapper, resourceTokens[0])
if len(resourceTokens) > 1 {
o.ResourceName = resourceTokens[1]

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")
}
default:
return errors.New("you must specify two or three arguments: verb, resource, and optional resourceName")
}

var err error
client, err := f.KubernetesClientSet()
if err != nil {
return err
}
o.SelfSARClient = client.AuthorizationV1()

o.AuthClient = client.AuthorizationV1()
o.Namespace = ""
if !o.AllNamespaces {
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
Expand All @@ -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")
Expand All @@ -167,9 +197,28 @@ func (o *CanIOptions) Validate() error {
return fmt.Errorf("NonResourceURL and ResourceName can not specified together")
}
}

if o.NoHeaders {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can't be set w/o list flag, yet you're not checking its value so how do you know it wasn't set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soltysh I checked List flag in Line 185

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sorry I missed the return nil after the internal condition.

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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
69 changes: 69 additions & 0 deletions pkg/kubectl/cmd/auth/cani_test.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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"},
},
},
},
}
}