Skip to content

Commit a92729a

Browse files
authored
Merge pull request kubernetes#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
2 parents 5442980 + d4f5228 commit a92729a

File tree

4 files changed

+226
-34
lines changed

4 files changed

+226
-34
lines changed

pkg/kubectl/cmd/auth/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ go_library(
1717
],
1818
deps = [
1919
"//pkg/kubectl/cmd/util:go_default_library",
20+
"//pkg/kubectl/describe/versioned:go_default_library",
2021
"//pkg/kubectl/scheme:go_default_library",
22+
"//pkg/kubectl/util/printers:go_default_library",
23+
"//pkg/kubectl/util/rbac:go_default_library",
2124
"//pkg/kubectl/util/templates:go_default_library",
2225
"//pkg/registry/rbac/reconciliation:go_default_library",
2326
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
@@ -27,6 +30,7 @@ go_library(
2730
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
2831
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
2932
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
33+
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
3034
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
3135
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/printers:go_default_library",
3236
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource:go_default_library",
@@ -60,7 +64,10 @@ go_test(
6064
deps = [
6165
"//pkg/kubectl/cmd/testing:go_default_library",
6266
"//pkg/kubectl/scheme:go_default_library",
67+
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
68+
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
6369
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
70+
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
6471
"//staging/src/k8s.io/client-go/rest:go_default_library",
6572
"//staging/src/k8s.io/client-go/rest/fake:go_default_library",
6673
],

pkg/kubectl/cmd/auth/cani.go

Lines changed: 147 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,25 @@ package auth
1919
import (
2020
"errors"
2121
"fmt"
22+
"io"
2223
"io/ioutil"
2324
"os"
25+
"sort"
2426
"strings"
2527

2628
"github.com/spf13/cobra"
2729

2830
authorizationv1 "k8s.io/api/authorization/v1"
31+
rbacv1 "k8s.io/api/rbac/v1"
2932
"k8s.io/apimachinery/pkg/api/meta"
3033
"k8s.io/apimachinery/pkg/runtime/schema"
34+
utilerrors "k8s.io/apimachinery/pkg/util/errors"
3135
"k8s.io/cli-runtime/pkg/genericclioptions"
3236
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
3337
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
38+
describeutil "k8s.io/kubernetes/pkg/kubectl/describe/versioned"
39+
"k8s.io/kubernetes/pkg/kubectl/util/printers"
40+
rbacutil "k8s.io/kubernetes/pkg/kubectl/util/rbac"
3441
"k8s.io/kubernetes/pkg/kubectl/util/templates"
3542
)
3643

@@ -39,14 +46,16 @@ import (
3946
type CanIOptions struct {
4047
AllNamespaces bool
4148
Quiet bool
49+
NoHeaders bool
4250
Namespace string
43-
SelfSARClient authorizationv1client.SelfSubjectAccessReviewsGetter
51+
AuthClient authorizationv1client.AuthorizationV1Interface
4452

4553
Verb string
4654
Resource schema.GroupVersionResource
4755
NonResourceURL string
4856
Subresource string
4957
ResourceName string
58+
List bool
5059

5160
genericclioptions.IOStreams
5261
}
@@ -77,7 +86,10 @@ var (
7786
kubectl auth can-i get pods --subresource=log
7887
7988
# Check to see if I can access the URL /logs/
80-
kubectl auth can-i get /logs/`)
89+
kubectl auth can-i get /logs/
90+
91+
# List all allowed actions in namespace "foo"
92+
kubectl auth can-i --list --namespace=foo`)
8193
)
8294

8395
// NewCmdCanI returns an initialized Command for 'auth can-i' sub command
@@ -95,57 +107,68 @@ func NewCmdCanI(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C
95107
Run: func(cmd *cobra.Command, args []string) {
96108
cmdutil.CheckErr(o.Complete(f, args))
97109
cmdutil.CheckErr(o.Validate())
98-
99-
allowed, err := o.RunAccessCheck()
100-
if err == nil {
101-
if !allowed {
102-
os.Exit(1)
110+
var err error
111+
if o.List {
112+
err = o.RunAccessList()
113+
} else {
114+
var allowed bool
115+
allowed, err = o.RunAccessCheck()
116+
if err == nil {
117+
if !allowed {
118+
os.Exit(1)
119+
}
103120
}
104121
}
105-
106122
cmdutil.CheckErr(err)
107123
},
108124
}
109125

110126
cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If true, check the specified action in all namespaces.")
111127
cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "If true, suppress output and just return the exit code.")
112128
cmd.Flags().StringVar(&o.Subresource, "subresource", o.Subresource, "SubResource such as pod/log or deployment/scale")
129+
cmd.Flags().BoolVar(&o.List, "list", o.List, "If true, prints all allowed actions.")
130+
cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If true, prints allowed actions without headers")
113131
return cmd
114132
}
115133

116134
// Complete completes all the required options
117135
func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error {
118-
if o.Quiet {
119-
o.Out = ioutil.Discard
120-
}
121-
122-
switch len(args) {
123-
case 2:
124-
o.Verb = args[0]
125-
if strings.HasPrefix(args[1], "/") {
126-
o.NonResourceURL = args[1]
127-
break
136+
if o.List {
137+
if len(args) != 0 {
138+
return errors.New("list option must be specified with no arguments")
128139
}
129-
resourceTokens := strings.SplitN(args[1], "/", 2)
130-
restMapper, err := f.ToRESTMapper()
131-
if err != nil {
132-
return err
140+
} else {
141+
if o.Quiet {
142+
o.Out = ioutil.Discard
133143
}
134-
o.Resource = o.resourceFor(restMapper, resourceTokens[0])
135-
if len(resourceTokens) > 1 {
136-
o.ResourceName = resourceTokens[1]
144+
145+
switch len(args) {
146+
case 2:
147+
o.Verb = args[0]
148+
if strings.HasPrefix(args[1], "/") {
149+
o.NonResourceURL = args[1]
150+
break
151+
}
152+
resourceTokens := strings.SplitN(args[1], "/", 2)
153+
restMapper, err := f.ToRESTMapper()
154+
if err != nil {
155+
return err
156+
}
157+
o.Resource = o.resourceFor(restMapper, resourceTokens[0])
158+
if len(resourceTokens) > 1 {
159+
o.ResourceName = resourceTokens[1]
160+
}
161+
default:
162+
return errors.New("you must specify two or three arguments: verb, resource, and optional resourceName")
137163
}
138-
default:
139-
return errors.New("you must specify two or three arguments: verb, resource, and optional resourceName")
140164
}
141165

142166
var err error
143167
client, err := f.KubernetesClientSet()
144168
if err != nil {
145169
return err
146170
}
147-
o.SelfSARClient = client.AuthorizationV1()
148-
171+
o.AuthClient = client.AuthorizationV1()
149172
o.Namespace = ""
150173
if !o.AllNamespaces {
151174
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
@@ -159,6 +182,13 @@ func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error {
159182

160183
// Validate makes sure provided values for CanIOptions are valid
161184
func (o *CanIOptions) Validate() error {
185+
if o.List {
186+
if o.Quiet || o.AllNamespaces || o.Subresource != "" {
187+
return errors.New("list option can't be specified with neither quiet, all-namespaces nor subresource options")
188+
}
189+
return nil
190+
}
191+
162192
if o.NonResourceURL != "" {
163193
if o.Subresource != "" {
164194
return fmt.Errorf("--subresource can not be used with NonResourceURL")
@@ -167,9 +197,28 @@ func (o *CanIOptions) Validate() error {
167197
return fmt.Errorf("NonResourceURL and ResourceName can not specified together")
168198
}
169199
}
200+
201+
if o.NoHeaders {
202+
return fmt.Errorf("--no-headers cannot be set without --list specified")
203+
}
170204
return nil
171205
}
172206

207+
// RunAccessList lists all the access current user has
208+
func (o *CanIOptions) RunAccessList() error {
209+
sar := &authorizationv1.SelfSubjectRulesReview{
210+
Spec: authorizationv1.SelfSubjectRulesReviewSpec{
211+
Namespace: o.Namespace,
212+
},
213+
}
214+
response, err := o.AuthClient.SelfSubjectRulesReviews().Create(sar)
215+
if err != nil {
216+
return err
217+
}
218+
219+
return o.printStatus(response.Status)
220+
}
221+
173222
// RunAccessCheck checks if user has access to a certain resource or non resource URL
174223
func (o *CanIOptions) RunAccessCheck() (bool, error) {
175224
var sar *authorizationv1.SelfSubjectAccessReview
@@ -195,10 +244,9 @@ func (o *CanIOptions) RunAccessCheck() (bool, error) {
195244
},
196245
},
197246
}
198-
199247
}
200248

201-
response, err := o.SelfSARClient.SelfSubjectAccessReviews().Create(sar)
249+
response, err := o.AuthClient.SelfSubjectAccessReviews().Create(sar)
202250
if err != nil {
203251
return false, err
204252
}
@@ -244,3 +292,71 @@ func (o *CanIOptions) resourceFor(mapper meta.RESTMapper, resourceArg string) sc
244292

245293
return gvr
246294
}
295+
296+
func (o *CanIOptions) printStatus(status authorizationv1.SubjectRulesReviewStatus) error {
297+
if status.Incomplete {
298+
fmt.Fprintf(o.ErrOut, "warning: the list may be incomplete: %v\n", status.EvaluationError)
299+
}
300+
301+
breakdownRules := []rbacv1.PolicyRule{}
302+
for _, rule := range convertToPolicyRule(status) {
303+
breakdownRules = append(breakdownRules, rbacutil.BreakdownRule(rule)...)
304+
}
305+
306+
compactRules, err := rbacutil.CompactRules(breakdownRules)
307+
if err != nil {
308+
return err
309+
}
310+
sort.Stable(rbacutil.SortableRuleSlice(compactRules))
311+
312+
w := printers.GetNewTabWriter(o.Out)
313+
defer w.Flush()
314+
315+
allErrs := []error{}
316+
if !o.NoHeaders {
317+
if err := printAccessHeaders(w); err != nil {
318+
allErrs = append(allErrs, err)
319+
}
320+
}
321+
322+
if err := printAccess(w, compactRules); err != nil {
323+
allErrs = append(allErrs, err)
324+
}
325+
return utilerrors.NewAggregate(allErrs)
326+
}
327+
328+
func convertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbacv1.PolicyRule {
329+
ret := []rbacv1.PolicyRule{}
330+
for _, resource := range status.ResourceRules {
331+
ret = append(ret, rbacv1.PolicyRule{
332+
Verbs: resource.Verbs,
333+
APIGroups: resource.APIGroups,
334+
Resources: resource.Resources,
335+
ResourceNames: resource.ResourceNames,
336+
})
337+
}
338+
339+
for _, nonResource := range status.NonResourceRules {
340+
ret = append(ret, rbacv1.PolicyRule{
341+
Verbs: nonResource.Verbs,
342+
NonResourceURLs: nonResource.NonResourceURLs,
343+
})
344+
}
345+
346+
return ret
347+
}
348+
349+
func printAccessHeaders(out io.Writer) error {
350+
columnNames := []string{"Resources", "Non-Resource URLs", "Resource Names", "Verbs"}
351+
_, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t"))
352+
return err
353+
}
354+
355+
func printAccess(out io.Writer, rules []rbacv1.PolicyRule) error {
356+
for _, r := range rules {
357+
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 {
358+
return err
359+
}
360+
}
361+
return nil
362+
}

pkg/kubectl/cmd/auth/cani_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import (
2424
"strings"
2525
"testing"
2626

27+
authorizationv1 "k8s.io/api/authorization/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
2729
"k8s.io/apimachinery/pkg/runtime/schema"
30+
"k8s.io/cli-runtime/pkg/genericclioptions"
2831
restclient "k8s.io/client-go/rest"
2932
"k8s.io/client-go/rest/fake"
3033
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
@@ -181,3 +184,69 @@ func TestRunAccessCheck(t *testing.T) {
181184
})
182185
}
183186
}
187+
188+
func TestRunAccessList(t *testing.T) {
189+
t.Run("test access list", func(t *testing.T) {
190+
options := &CanIOptions{List: true}
191+
expectedOutput := "Resources Non-Resource URLs Resource Names Verbs\n" +
192+
"job.* [] [test-resource] [get list]\n" +
193+
"pod.* [] [test-resource] [get list]\n" +
194+
" [/apis/*] [] [get]\n" +
195+
" [/version] [] [get]\n"
196+
197+
tf := cmdtesting.NewTestFactory().WithNamespace("test")
198+
defer tf.Cleanup()
199+
200+
ns := scheme.Codecs
201+
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
202+
203+
tf.Client = &fake.RESTClient{
204+
GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
205+
NegotiatedSerializer: ns,
206+
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
207+
switch req.URL.Path {
208+
case "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews":
209+
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, getSelfSubjectRulesReview()))))
210+
return &http.Response{StatusCode: http.StatusOK, Body: body}, nil
211+
default:
212+
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
213+
return nil, nil
214+
}
215+
}),
216+
}
217+
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
218+
options.IOStreams = ioStreams
219+
if err := options.Complete(tf, []string{}); err != nil {
220+
t.Errorf("got unexpected error when do Complete(): %v", err)
221+
return
222+
}
223+
224+
err := options.RunAccessList()
225+
if err != nil {
226+
t.Errorf("got unexpected error when do RunAccessList(): %v", err)
227+
} else if buf.String() != expectedOutput {
228+
t.Errorf("expected %v\n but got %v\n", expectedOutput, buf.String())
229+
}
230+
})
231+
}
232+
233+
func getSelfSubjectRulesReview() *authorizationv1.SelfSubjectRulesReview {
234+
return &authorizationv1.SelfSubjectRulesReview{
235+
Status: authorizationv1.SubjectRulesReviewStatus{
236+
ResourceRules: []authorizationv1.ResourceRule{
237+
{
238+
Verbs: []string{"get", "list"},
239+
APIGroups: []string{"*"},
240+
Resources: []string{"pod", "job"},
241+
ResourceNames: []string{"test-resource"},
242+
},
243+
},
244+
NonResourceRules: []authorizationv1.NonResourceRule{
245+
{
246+
Verbs: []string{"get"},
247+
NonResourceURLs: []string{"/apis/*", "/version"},
248+
},
249+
},
250+
},
251+
}
252+
}

0 commit comments

Comments
 (0)