mirror of https://github.com/k3s-io/k3s
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 namespacepull/564/head
commit
a92729a301
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue