Skip to content

Commit 5ab9e06

Browse files
danielpacaklizrice
authored andcommitted
feat: Resolving API groups (#40)
* test: Add integration test scenario for resolving API group * feat: Resolve resource API groups Kubernetes assigns resources to API grups. So you can have pods in the core group as well as pods in the metrics.k8s.io groups. With the changes introduced the plugin allows you to specify the fully qualified resoruce name at the command line, for example: $ kubect who-can get pods $ kubect who-can get pods.metrics.k8s.io Resolves: #35, #38
1 parent 01427bc commit 5ab9e06

13 files changed

+590
-180
lines changed

.github/main.workflow

-18
This file was deleted.

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
# kubectl-who-can
88

9-
Shows who has permissions to VERB [TYPE | TYPE/NAME | NONRESOURCEURL] in Kubernetes.
9+
Shows which subjects have RBAC permissions to VERB [TYPE | TYPE/NAME | NONRESOURCEURL] in Kubernetes.
1010

11-
[![asciicast](https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j.svg)](https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j)
11+
[![asciicast][asciicast-img]][asciicast]
1212

1313
## Installation
1414

@@ -31,7 +31,7 @@ Download a release distribution archive for your operating system, extract it, a
3131
executable to your `$PATH`. For example, to manually install `kubectl-who-can` on macOS run the following command:
3232

3333
```
34-
VERSION="v0.1.0-alpha.1"
34+
VERSION=`git describe --abbrev=0`
3535
3636
mkdir -p /tmp/who-can/$VERSION && \
3737
curl -L https://github.com/aquasecurity/kubectl-who-can/releases/download/$VERSION/kubectl-who-can_darwin_x86_64.tar.gz \
@@ -83,3 +83,6 @@ The `kubectl-who-can` binary will be in `/usr/local/bin`.
8383

8484
[license-img]: https://img.shields.io/github/license/aquasecurity/kubectl-who-can.svg
8585
[license]: https://github.com/aquasecurity/kubectl-who-can/blob/master/LICENSE
86+
87+
[asciicast-img]: https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j.svg
88+
[asciicast]: https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j

go.mod

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ go 1.12
44

55
require (
66
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
7-
github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937
8-
github.com/stretchr/objx v0.2.0 // indirect
7+
github.com/spf13/cobra v0.0.4
98
github.com/stretchr/testify v1.3.0
10-
k8s.io/api v0.0.0-20190612125737-db0771252981
11-
k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad
9+
k8s.io/api v0.0.0-20190703205437-39734b2a72fe
10+
k8s.io/apiextensions-apiserver v0.0.0-20190704050600-357b4270afe4
11+
k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76
1212
k8s.io/cli-runtime v0.0.0-20190612131021-ced92c4c4749
13-
k8s.io/client-go v0.0.0-20190612125919-5c45477a8ae7
13+
k8s.io/client-go v0.0.0-20190704045512-07281898b0f0
1414
)

go.sum

+181
Large diffs are not rendered by default.

pkg/cmd/access_checker.go

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type accessChecker struct {
1717
client clientauthz.SelfSubjectAccessReviewInterface
1818
}
1919

20+
// NewAccessChecker constructs the default AccessChecker.
2021
func NewAccessChecker(client clientauthz.SelfSubjectAccessReviewInterface) AccessChecker {
2122
return &accessChecker{
2223
client: client,

pkg/cmd/list.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"github.com/spf13/cobra"
88
core "k8s.io/api/core/v1"
9+
"k8s.io/apimachinery/pkg/runtime/schema"
910
clioptions "k8s.io/cli-runtime/pkg/genericclioptions"
1011
"k8s.io/client-go/kubernetes"
1112
clientcore "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -24,14 +25,18 @@ const (
2425
whoCanLong = `Shows which users, groups and service accounts can perform a given verb on a given resource type.
2526
2627
VERB is a logical Kubernetes API verb like 'get', 'list', 'watch', 'delete', etc.
27-
TYPE is a Kubernetes resource. Shortcuts, such as 'pod' or 'po' will be resolved. NAME is the name of a particular Kubernetes resource.
28+
TYPE is a Kubernetes resource. Shortcuts and API groups will be resolved, e.g. 'po' or 'pods.metrics.k8s.io'.
29+
NAME is the name of a particular Kubernetes resource.
2830
NONRESOURCEURL is a partial URL that starts with "/".`
2931
whoCanExample = ` # List who can get pods in any namespace
3032
kubectl who-can get pods --all-namespaces
3133
3234
# List who can create pods in the current namespace
3335
kubectl who-can create pods
3436
37+
# List who can get pods specifying the API group
38+
kubectl who-can get pods.metrics.k8s.io
39+
3540
# List who can create services in namespace "foo"
3641
kubectl who-can create services -n foo
3742
@@ -63,6 +68,7 @@ type Action struct {
6368
nonResourceURL string
6469
subResource string
6570
resourceName string
71+
gr schema.GroupResource
6672

6773
namespace string
6874
allNamespaces bool
@@ -186,7 +192,7 @@ func (w *whoCan) Complete(args []string) error {
186192
}
187193

188194
if w.resource != "" {
189-
w.resource, err = w.resourceResolver.Resolve(w.verb, w.resource, w.subResource)
195+
w.gr, err = w.resourceResolver.Resolve(w.verb, w.resource, w.subResource)
190196
if err != nil {
191197
return fmt.Errorf("resolving resource: %v", err)
192198
}

pkg/cmd/list_test.go

+16-15
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
core "k8s.io/api/core/v1"
1010
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
1111
"k8s.io/apimachinery/pkg/runtime"
12+
"k8s.io/apimachinery/pkg/runtime/schema"
1213
clioptions "k8s.io/cli-runtime/pkg/genericclioptions"
1314
"k8s.io/client-go/kubernetes/fake"
1415
clientTesting "k8s.io/client-go/testing"
@@ -40,9 +41,9 @@ type resourceResolverMock struct {
4041
mock.Mock
4142
}
4243

43-
func (r *resourceResolverMock) Resolve(verb, resource, subResource string) (string, error) {
44+
func (r *resourceResolverMock) Resolve(verb, resource, subResource string) (schema.GroupResource, error) {
4445
args := r.Called(verb, resource, subResource)
45-
return args.String(0), args.Error(1)
46+
return args.Get(0).(schema.GroupResource), args.Error(1)
4647
}
4748

4849
type clientConfigMock struct {
@@ -91,8 +92,8 @@ func TestComplete(t *testing.T) {
9192
resource string
9293
subResource string
9394

94-
result string
95-
err error
95+
gr schema.GroupResource
96+
err error
9697
}
9798

9899
type expected struct {
@@ -106,20 +107,20 @@ func TestComplete(t *testing.T) {
106107
data := []struct {
107108
scenario string
108109

109-
*currentContext
110+
currentContext *currentContext
110111

111-
flags flags
112-
args []string
113-
*resolution
112+
flags flags
113+
args []string
114+
resolution *resolution
114115

115-
expected
116+
expected expected
116117
}{
117118
{
118119
scenario: "A",
119120
currentContext: &currentContext{namespace: "foo"},
120121
flags: flags{namespace: "", allNamespaces: false},
121122
args: []string{"list", "pods"},
122-
resolution: &resolution{verb: "list", resource: "pods", result: "pods"},
123+
resolution: &resolution{verb: "list", resource: "pods", gr: schema.GroupResource{Resource: "pods"}},
123124
expected: expected{
124125
namespace: "foo",
125126
verb: "list",
@@ -132,7 +133,7 @@ func TestComplete(t *testing.T) {
132133
currentContext: &currentContext{err: errors.New("cannot open context")},
133134
flags: flags{namespace: "", allNamespaces: false},
134135
args: []string{"list", "pods"},
135-
resolution: &resolution{verb: "list", resource: "pods", result: "pods"},
136+
resolution: &resolution{verb: "list", resource: "pods", gr: schema.GroupResource{Resource: "pods"}},
136137
expected: expected{
137138
namespace: "",
138139
verb: "list",
@@ -145,7 +146,7 @@ func TestComplete(t *testing.T) {
145146
scenario: "C",
146147
flags: flags{namespace: "", allNamespaces: true},
147148
args: []string{"get", "service/mongodb"},
148-
resolution: &resolution{verb: "get", resource: "service", result: "services"},
149+
resolution: &resolution{verb: "get", resource: "service", gr: schema.GroupResource{Resource: "services"}},
149150
expected: expected{
150151
namespace: core.NamespaceAll,
151152
verb: "get",
@@ -157,7 +158,7 @@ func TestComplete(t *testing.T) {
157158
scenario: "D",
158159
flags: flags{namespace: "bar", allNamespaces: false},
159160
args: []string{"delete", "pv"},
160-
resolution: &resolution{verb: "delete", resource: "pv", result: "persistentvolumes"},
161+
resolution: &resolution{verb: "delete", resource: "pv", gr: schema.GroupResource{Resource: "persistentvolumes"}},
161162
expected: expected{
162163
namespace: "bar",
163164
verb: "delete",
@@ -211,7 +212,7 @@ func TestComplete(t *testing.T) {
211212

212213
if tt.resolution != nil {
213214
resourceResolver.On("Resolve", tt.resolution.verb, tt.resolution.resource, tt.resolution.subResource).
214-
Return(tt.resolution.result, tt.resolution.err)
215+
Return(tt.resolution.gr, tt.resolution.err)
215216
}
216217
if tt.currentContext != nil {
217218
clientConfig.On("Namespace").Return(tt.currentContext.namespace, false, tt.currentContext.err)
@@ -239,7 +240,7 @@ func TestComplete(t *testing.T) {
239240
assert.Equal(t, tt.expected.err, err)
240241
assert.Equal(t, tt.expected.namespace, o.namespace)
241242
assert.Equal(t, tt.expected.verb, o.verb)
242-
assert.Equal(t, tt.expected.resource, o.resource)
243+
assert.Equal(t, tt.expected.resource, o.gr.Resource)
243244
assert.Equal(t, tt.expected.resourceName, o.resourceName)
244245

245246
clientConfig.AssertExpectations(t)

pkg/cmd/namespace_validator.go

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import (
88
clientcore "k8s.io/client-go/kubernetes/typed/core/v1"
99
)
1010

11+
// NamespaceValidator wraps the Validate method.
12+
//
13+
// Validate checks whether the given namespace exists or not.
14+
// Returns nil if it exists, an error otherwise.
1115
type NamespaceValidator interface {
1216
Validate(name string) error
1317
}
@@ -16,6 +20,7 @@ type namespaceValidator struct {
1620
client clientcore.NamespaceInterface
1721
}
1822

23+
// NewNamespaceValidator constructs the default NamespaceValidator.
1924
func NewNamespaceValidator(client clientcore.NamespaceInterface) NamespaceValidator {
2025
return &namespaceValidator{
2126
client: client,

pkg/cmd/policy_rule_matcher.go

+19-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import (
88
// PolicyRuleMatcher wraps the Matches* methods.
99
//
1010
// MatchesRole returns `true` if any PolicyRule defined by the given Role matches the specified Action, `false` otherwise.
11-
// MatchesClusterRole returns `true` if any PolicyRule defined by the given ClusterRole matches the specified Action, `false` otherwise.
11+
//
12+
// MatchesClusterRole returns `true` if any PolicyRule defined by the given ClusterRole matches the specified Action, `false` otherwise.
1213
type PolicyRuleMatcher interface {
1314
MatchesRole(role rbac.Role, action Action) bool
1415
MatchesClusterRole(role rbac.ClusterRole, action Action) bool
@@ -17,7 +18,7 @@ type PolicyRuleMatcher interface {
1718
type matcher struct {
1819
}
1920

20-
// NewPolicyRuleMatcher constructs a PolicyRuleMatcher.
21+
// NewPolicyRuleMatcher constructs the default PolicyRuleMatcher.
2122
func NewPolicyRuleMatcher() PolicyRuleMatcher {
2223
return &matcher{}
2324
}
@@ -54,11 +55,26 @@ func (m *matcher) matches(rule rbac.PolicyRule, action Action) bool {
5455
m.matchesNonResourceURL(rule, action.nonResourceURL)
5556
}
5657

58+
resource := action.gr.Resource
59+
if action.subResource != "" {
60+
resource += "/" + action.subResource
61+
}
62+
5763
return m.matchesVerb(rule, action.verb) &&
58-
m.matchesResource(rule, action.resource) &&
64+
m.matchesResource(rule, resource) &&
65+
m.matchesAPIGroup(rule, action.gr.Group) &&
5966
m.matchesResourceName(rule, action.resourceName)
6067
}
6168

69+
func (m *matcher) matchesAPIGroup(rule rbac.PolicyRule, actionGroup string) bool {
70+
for _, group := range rule.APIGroups {
71+
if group == rbac.APIGroupAll || group == actionGroup {
72+
return true
73+
}
74+
}
75+
return false
76+
}
77+
6278
func (m *matcher) matchesVerb(rule rbac.PolicyRule, actionVerb string) bool {
6379
for _, verb := range rule.Verbs {
6480
if verb == rbac.VerbAll || verb == actionVerb {

0 commit comments

Comments
 (0)