Merge pull request #64820 from WanLinghao/ctl_selfsubjectrulesreview_support

Add `kubectl auth can-i --list` option which could help users know what actions they can do in specific namespace
pull/564/head
Kubernetes Prow Robot 2019-02-12 23:22:23 -08:00 committed by GitHub
commit a92729a301
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 227 additions and 35 deletions

View File

@ -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",
],

View File

@ -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
}

View File

@ -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"},
},
},
},
}
}

View File

@ -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 ""
}