Skip to content

Commit 01427bc

Browse files
danielpacaklizrice
authored andcommitted
fix: Match RoleBindings bound to ClusterRoles (#34)
* refactor: Move Roles/ClusterRoles filtering logic to PolicyRuleMatcher to further facilitate unit testing * test: Add unit tests for GetRoleFor(Action) and GetClusterRoleFor(Action) methods * test: Add unit tests for GetRoleBindings() and GetClusterRoleBindings() methods * fix: Match RoleBindings referring to ClusterRoles Resolves: #31 * test: Add more scenarios to the integration test suite
1 parent 7430706 commit 01427bc

6 files changed

+769
-323
lines changed

.goreleaser.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ before:
33
- go mod download
44
builds:
55
- # Path to main.go file or main package.
6-
main: ./cmd/kubectl-who-can.go
6+
main: ./cmd/kubectl-who-can/main.go
77
# Custom environment variables to be set during the builds.
88
env:
99
- CGO_ENABLED=0
@@ -35,3 +35,4 @@ changelog:
3535
- '^docs:'
3636
- '^test:'
3737
- '^chore:'
38+
- '^refactor:'

pkg/cmd/list.go

+75-135
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,15 @@ NONRESOURCEURL is a partial URL that starts with "/".`
4949
5050
# List who can access the URL /logs/
5151
kubectl who-can get /logs`
52-
)
53-
54-
type role struct {
55-
name string
56-
isClusterRole bool
57-
}
5852

59-
type roles map[role]struct{}
53+
// RoleKind is the RoleRef's Kind referencing a Role.
54+
RoleKind = "Role"
55+
// ClusterRoleKind is the RoleRef's Kind referencing a ClusterRole.
56+
ClusterRoleKind = "ClusterRole"
57+
)
6058

61-
type whoCan struct {
59+
// Action represents an action a subject can be given permission to.
60+
type Action struct {
6261
verb string
6362
resource string
6463
nonResourceURL string
@@ -67,6 +66,16 @@ type whoCan struct {
6766

6867
namespace string
6968
allNamespaces bool
69+
}
70+
71+
// roles is a set of Role names matching the specified Action.
72+
type roles map[string]struct{}
73+
74+
// clusterRoles is a set of ClusterRole names matching the specified Action.
75+
type clusterRoles map[string]struct{}
76+
77+
type whoCan struct {
78+
Action
7079

7180
configFlags *clioptions.ConfigFlags
7281
clientConfig clientcmd.ClientConfig
@@ -76,8 +85,7 @@ type whoCan struct {
7685
namespaceValidator NamespaceValidator
7786
resourceResolver ResourceResolver
7887
accessChecker AccessChecker
79-
80-
r roles
88+
policyRuleMatcher PolicyRuleMatcher
8189

8290
clioptions.IOStreams
8391
}
@@ -89,6 +97,7 @@ func NewWhoCanOptions(configFlags *clioptions.ConfigFlags,
8997
namespaceValidator NamespaceValidator,
9098
resourceResolver ResourceResolver,
9199
accessChecker AccessChecker,
100+
policyRuleMatcher PolicyRuleMatcher,
92101
streams clioptions.IOStreams) *whoCan {
93102
return &whoCan{
94103
configFlags: configFlags,
@@ -98,6 +107,7 @@ func NewWhoCanOptions(configFlags *clioptions.ConfigFlags,
98107
namespaceValidator: namespaceValidator,
99108
resourceResolver: resourceResolver,
100109
accessChecker: accessChecker,
110+
policyRuleMatcher: policyRuleMatcher,
101111
IOStreams: streams,
102112
}
103113
}
@@ -132,6 +142,7 @@ func NewCmdWhoCan(streams clioptions.IOStreams) (*cobra.Command, error) {
132142
namespaceValidator,
133143
resourceResolver,
134144
accessChecker,
145+
NewPolicyRuleMatcher(),
135146
streams)
136147

137148
cmd := &cobra.Command{
@@ -179,6 +190,7 @@ func (w *whoCan) Complete(args []string) error {
179190
if err != nil {
180191
return fmt.Errorf("resolving resource: %v", err)
181192
}
193+
glog.V(3).Infof("Resolved resource `%s`", w.resource)
182194
}
183195

184196
err = w.resolveNamespace()
@@ -197,11 +209,13 @@ func (w *whoCan) resolveArgs(args []string) error {
197209
w.verb = args[0]
198210
if strings.HasPrefix(args[1], "/") {
199211
w.nonResourceURL = args[1]
212+
glog.V(3).Infof("Resolved nonResourceURL `%s`", w.nonResourceURL)
200213
} else {
201214
resourceTokens := strings.SplitN(args[1], "/", 2)
202215
w.resource = resourceTokens[0]
203216
if len(resourceTokens) > 1 {
204217
w.resourceName = resourceTokens[1]
218+
glog.V(3).Infof("Resolved resourceName `%s`", w.resourceName)
205219
}
206220
}
207221
return nil
@@ -250,28 +264,26 @@ func (w *whoCan) Check() error {
250264
return fmt.Errorf("checking API access: %v", err)
251265
}
252266

253-
w.r = make(map[role]struct{}, 10)
254-
255267
// Get the Roles that relate to the Verbs and Resources we are interested in
256-
err = w.getRoles()
268+
roleNames, err := w.GetRolesFor(w.Action)
257269
if err != nil {
258270
return fmt.Errorf("getting Roles: %v", err)
259271
}
260272

261-
// Get the RoleBindings that relate to this set of Roles
262-
roleBindings, err := w.getRoleBindings()
273+
// Get the ClusterRoles that relate to the verbs and resources we are interested in
274+
clusterRoleNames, err := w.GetClusterRolesFor(w.Action)
263275
if err != nil {
264-
return fmt.Errorf("getting RoleBindings: %v", err)
276+
return fmt.Errorf("getting ClusterRoles: %v", err)
265277
}
266278

267-
// Get the ClusterRoles that relate to the verbs and resources we are interested in
268-
err = w.getClusterRoles()
279+
// Get the RoleBindings that relate to this set of Roles or ClusterRoles
280+
roleBindings, err := w.GetRoleBindings(roleNames, clusterRoleNames)
269281
if err != nil {
270-
return fmt.Errorf("getting ClusterRoles: %v", err)
282+
return fmt.Errorf("getting RoleBindings: %v", err)
271283
}
272284

273285
// Get the ClusterRoleBindings that relate to this set of ClusterRoles
274-
clusterRoleBindings, err := w.getClusterRoleBindings()
286+
clusterRoleBindings, err := w.GetClusterRoleBindings(clusterRoleNames)
275287
if err != nil {
276288
return fmt.Errorf("getting ClusterRoleBindings: %v", err)
277289
}
@@ -344,167 +356,95 @@ func (w *whoCan) printAPIAccessWarnings(warnings []string) {
344356
}
345357
}
346358

347-
func (w *whoCan) getRoles() error {
359+
// GetRolesFor returns a set of names of Roles matching the specified Action.
360+
func (w *whoCan) GetRolesFor(action Action) (roles, error) {
348361
rl, err := w.clientRBAC.Roles(w.namespace).List(meta.ListOptions{})
349362
if err != nil {
350-
return err
363+
return nil, err
351364
}
352365

353-
w.filterRoles(rl)
354-
return nil
355-
}
356-
357-
func (w *whoCan) filterRoles(roles *rbac.RoleList) {
358-
for _, item := range roles.Items {
359-
for _, rule := range item.Rules {
360-
if !w.policyRuleMatches(rule) {
361-
glog.V(3).Infof("Role [%s] doesn't match policy filter", item.Name)
362-
continue
363-
}
366+
roleNames := make(map[string]struct{}, 10)
364367

365-
newRole := role{
366-
name: item.Name,
367-
isClusterRole: false,
368-
}
369-
if _, ok := w.r[newRole]; !ok {
370-
w.r[newRole] = struct{}{}
368+
for _, item := range rl.Items {
369+
if w.policyRuleMatcher.MatchesRole(item, action) {
370+
if _, ok := roleNames[item.Name]; !ok {
371+
roleNames[item.Name] = struct{}{}
371372
}
372-
373373
}
374374
}
375+
376+
return roleNames, nil
375377
}
376378

377-
func (w *whoCan) getClusterRoles() error {
379+
// GetClusterRolesFor returns a set of names of ClusterRoles matching the specified Action.
380+
func (w *whoCan) GetClusterRolesFor(action Action) (clusterRoles, error) {
378381
crl, err := w.clientRBAC.ClusterRoles().List(meta.ListOptions{})
379382
if err != nil {
380-
return err
383+
return nil, err
381384
}
382385

383-
w.filterClusterRoles(crl)
384-
return nil
385-
}
386-
387-
func (w *whoCan) filterClusterRoles(roles *rbac.ClusterRoleList) {
388-
for _, item := range roles.Items {
389-
for _, rule := range item.Rules {
390-
if !w.policyRuleMatches(rule) {
391-
glog.V(3).Infof("ClusterRole [%s] doesn't match policy filter", item.Name)
392-
continue
393-
}
386+
cr := make(map[string]struct{}, 10)
394387

395-
newRole := role{
396-
name: item.Name,
397-
isClusterRole: true,
388+
for _, item := range crl.Items {
389+
if w.policyRuleMatcher.MatchesClusterRole(item, action) {
390+
if _, ok := cr[item.Name]; !ok {
391+
cr[item.Name] = struct{}{}
398392
}
399-
if _, ok := w.r[newRole]; !ok {
400-
w.r[newRole] = struct{}{}
401-
}
402-
}
403-
}
404-
}
405-
406-
func (w *whoCan) policyRuleMatches(rule rbac.PolicyRule) bool {
407-
if w.nonResourceURL != "" {
408-
return w.matchesVerb(rule) &&
409-
w.matchesNonResourceURL(rule)
410-
}
411-
412-
return w.matchesVerb(rule) &&
413-
w.matchesResource(rule) &&
414-
w.matchesResourceName(rule)
415-
}
416-
417-
func (w *whoCan) matchesVerb(rule rbac.PolicyRule) bool {
418-
for _, verb := range rule.Verbs {
419-
if verb == rbac.VerbAll || verb == w.verb {
420-
return true
421-
}
422-
}
423-
return false
424-
}
425-
426-
func (w *whoCan) matchesResource(rule rbac.PolicyRule) bool {
427-
for _, resource := range rule.Resources {
428-
if resource == rbac.ResourceAll || resource == w.resource {
429-
return true
430393
}
431394
}
432-
return false
395+
return cr, nil
433396
}
434397

435-
func (w *whoCan) matchesResourceName(rule rbac.PolicyRule) bool {
436-
if w.resourceName == "" && len(rule.ResourceNames) == 0 {
437-
return true
438-
}
439-
if len(rule.ResourceNames) == 0 {
440-
return true
441-
}
442-
for _, name := range rule.ResourceNames {
443-
if name == w.resourceName {
444-
return true
445-
}
446-
}
447-
return false
448-
}
449-
450-
func (w *whoCan) matchesNonResourceURL(rule rbac.PolicyRule) bool {
451-
for _, URL := range rule.NonResourceURLs {
452-
if URL == w.nonResourceURL {
453-
return true
454-
}
398+
// GetRoleBindings returns the RoleBindings that refer to the given set of Role names or ClusterRole names.
399+
func (w *whoCan) GetRoleBindings(roleNames roles, clusterRoleNames clusterRoles) (roleBindings []rbac.RoleBinding, err error) {
400+
// TODO I'm wondering if GetRoleBindings should be invoked at all when the --all-namespaces flag is specified?
401+
if w.namespace == core.NamespaceAll {
402+
return
455403
}
456-
return false
457-
}
458404

459-
func (w *whoCan) getRoleBindings() (roleBindings []rbac.RoleBinding, err error) {
460-
rbl, err := w.clientRBAC.RoleBindings(w.namespace).List(meta.ListOptions{})
405+
list, err := w.clientRBAC.RoleBindings(w.namespace).List(meta.ListOptions{})
461406
if err != nil {
462407
return
463408
}
464409

465-
for _, roleBinding := range rbl.Items {
466-
if w.r.match(&roleBinding.RoleRef) {
467-
glog.V(1).Info(fmt.Sprintf("Match found: roleRef: %v", roleBinding.RoleRef))
468-
roleBindings = append(roleBindings, roleBinding)
410+
for _, roleBinding := range list.Items {
411+
if roleBinding.RoleRef.Kind == RoleKind {
412+
if _, ok := roleNames[roleBinding.RoleRef.Name]; ok {
413+
roleBindings = append(roleBindings, roleBinding)
414+
}
415+
} else if roleBinding.RoleRef.Kind == ClusterRoleKind {
416+
if _, ok := clusterRoleNames[roleBinding.RoleRef.Name]; ok {
417+
roleBindings = append(roleBindings, roleBinding)
418+
}
419+
} else {
420+
_, _ = fmt.Fprintf(w.Out, "Warning: Unrecognized RoleRef kind `%s` found in `%s` RoleBinding\n", roleBinding.RoleRef.Kind, roleBinding.Name)
469421
}
470422
}
471423

472424
return
473425
}
474426

475-
func (w *whoCan) getClusterRoleBindings() (clusterRoleBindings []rbac.ClusterRoleBinding, err error) {
476-
rbl, err := w.clientRBAC.ClusterRoleBindings().List(meta.ListOptions{})
427+
// GetClusterRoleBindings returns the ClusterRoleBindings that refer to the given sef of ClusterRole names.
428+
func (w *whoCan) GetClusterRoleBindings(clusterRoleNames clusterRoles) (clusterRoleBindings []rbac.ClusterRoleBinding, err error) {
429+
list, err := w.clientRBAC.ClusterRoleBindings().List(meta.ListOptions{})
477430
if err != nil {
478431
return
479432
}
480433

481-
for _, roleBinding := range rbl.Items {
482-
if w.r.match(&roleBinding.RoleRef) {
483-
glog.V(1).Info(fmt.Sprintf("Match found: roleRef: %v", roleBinding.RoleRef))
434+
for _, roleBinding := range list.Items {
435+
if _, ok := clusterRoleNames[roleBinding.RoleRef.Name]; ok {
484436
clusterRoleBindings = append(clusterRoleBindings, roleBinding)
485437
}
486438
}
487439

488440
return
489441
}
490442

491-
func (r roles) match(roleRef *rbac.RoleRef) bool {
492-
tempRole := role{
493-
name: roleRef.Name,
494-
isClusterRole: (roleRef.Kind == "ClusterRole"),
495-
}
496-
497-
glog.V(3).Info(fmt.Sprintf("Testing against roleRef: %v", tempRole))
498-
499-
_, ok := r[tempRole]
500-
return ok
501-
}
502-
503443
func (w *whoCan) output(roleBindings []rbac.RoleBinding, clusterRoleBindings []rbac.ClusterRoleBinding) {
504444
wr := new(tabwriter.Writer)
505445
wr.Init(w.Out, 0, 8, 2, ' ', 0)
506446

507-
action := w.prettyPrintAction()
447+
action := w.Action.PrettyPrint()
508448

509449
if w.resource != "" {
510450
// NonResourceURL permissions can only be granted through ClusterRoles. Hence no point in printing RoleBindings section.
@@ -535,7 +475,7 @@ func (w *whoCan) output(roleBindings []rbac.RoleBinding, clusterRoleBindings []r
535475
wr.Flush()
536476
}
537477

538-
func (w *whoCan) prettyPrintAction() string {
478+
func (w Action) PrettyPrint() string {
539479
if w.nonResourceURL != "" {
540480
return fmt.Sprintf("%s %s", w.verb, w.nonResourceURL)
541481
}

0 commit comments

Comments
 (0)