Skip to content

Commit

Permalink
Merge pull request #29 from danielpacak/issue_24_wildcards
Browse files Browse the repository at this point in the history
feat: Support verb and resource wildcards (`*`)
  • Loading branch information
lizrice authored Jun 26, 2019
2 parents b9b4181 + 57e4e39 commit 5d0c6ed
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 46 deletions.
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
[![Go Report Card][report-card-img]][report-card]

# kubectl-who-can
[WIP] show who has permissions to <verb> <resources> in kubernetes

Shows who has permissions to VERB [TYPE | TYPE/NAME | NONRESOURCEURL] in Kubernetes.

[![asciicast](https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j.svg)](https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j)

Expand Down Expand Up @@ -38,10 +39,6 @@ docker run --rm -v /usr/local/bin:/go/bin golang go get -v github.com/aquasecuri
```
The `kubectl-who-can` binary will be in `/usr/local/bin`.

## TODO

* Make it a kubectl plugin (for now it's a standalone executable)

[release-img]: https://img.shields.io/github/release/aquasecurity/kubectl-who-can.svg
[release]: https://github.com/aquasecurity/kubectl-who-can/releases

Expand Down
1 change: 1 addition & 0 deletions cmd/kubectl-who-can.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"github.com/aquasecurity/kubectl-who-can/pkg/cmd"
clioptions "k8s.io/cli-runtime/pkg/genericclioptions"
// Load all known auth plugins
_ "k8s.io/client-go/plugin/pkg/client/auth"
"os"
)
Expand Down
18 changes: 9 additions & 9 deletions pkg/cmd/access_checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ package cmd
import (
"errors"
"github.com/stretchr/testify/assert"
authzv1 "k8s.io/api/authorization/v1"
authz "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
k8stesting "k8s.io/client-go/testing"
clientauthz "k8s.io/client-go/kubernetes/typed/authorization/v1"
clienttesting "k8s.io/client-go/testing"
"testing"
)

func TestIsAllowed(t *testing.T) {

data := []struct {
scenario string
reactionFunc k8stesting.ReactionFunc
reactionFunc clienttesting.ReactionFunc

allowed bool
err error
Expand Down Expand Up @@ -53,16 +53,16 @@ func TestIsAllowed(t *testing.T) {

}

func newClient(reaction k8stesting.ReactionFunc) v1.SelfSubjectAccessReviewInterface {
func newClient(reaction clienttesting.ReactionFunc) clientauthz.SelfSubjectAccessReviewInterface {
client := fake.NewSimpleClientset()
client.Fake.PrependReactor("create", "selfsubjectaccessreviews", reaction)
return client.AuthorizationV1().SelfSubjectAccessReviews()
}

func newSelfSubjectAccessReviewsReactionFunc(allowed bool, err error) k8stesting.ReactionFunc {
return func(action k8stesting.Action) (bool, runtime.Object, error) {
sar := &authzv1.SelfSubjectAccessReview{
Status: authzv1.SubjectAccessReviewStatus{
func newSelfSubjectAccessReviewsReactionFunc(allowed bool, err error) clienttesting.ReactionFunc {
return func(action clienttesting.Action) (bool, runtime.Object, error) {
sar := &authz.SelfSubjectAccessReview{
Status: authz.SubjectAccessReviewStatus{
Allowed: allowed,
},
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ NONRESOURCEURL is a partial URL that starts with "/".`
# List who can get the service named "mongodb" in namespace "bar"
kubectl who-can get svc/mongodb --namespace bar
# List who can do everything with pods in the current namespace
kubectl who-can '*' pods
# List who can list every resource in the namespace "baz"
kubectl who-can list '*' -n baz
# List who can read pod logs
kubectl who-can get pods --subresource=log
Expand Down Expand Up @@ -148,7 +154,8 @@ func NewCmdWhoCan(streams clioptions.IOStreams) (*cobra.Command, error) {
},
}

cmd.PersistentFlags().StringVar(&o.subResource, "subresource", o.subResource, "SubResource such as pod/log or deployment/scale")
cmd.PersistentFlags().StringVar(&o.subResource, "subresource", o.subResource,
"SubResource such as pod/log or deployment/scale")
cmd.PersistentFlags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", false,
"If true, check the specified action in all namespaces.")

Expand Down
18 changes: 9 additions & 9 deletions pkg/cmd/namespace_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,37 @@ package cmd

import (
"fmt"
apicorev1 "k8s.io/api/core/v1"
core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
clientcore "k8s.io/client-go/kubernetes/typed/core/v1"
)

type NamespaceValidator interface {
Validate(name string) error
}

type namespaceValidator struct {
client typedcorev1.NamespaceInterface
client clientcore.NamespaceInterface
}

func NewNamespaceValidator(client typedcorev1.NamespaceInterface) NamespaceValidator {
func NewNamespaceValidator(client clientcore.NamespaceInterface) NamespaceValidator {
return &namespaceValidator{
client: client,
}
}

func (w *namespaceValidator) Validate(name string) error {
if name != apicorev1.NamespaceAll {
ns, err := w.client.Get(name, metav1.GetOptions{})
if name != core.NamespaceAll {
ns, err := w.client.Get(name, meta.GetOptions{})
if err != nil {
if statusErr, ok := err.(*errors.StatusError); ok &&
statusErr.Status().Reason == metav1.StatusReasonNotFound {
statusErr.Status().Reason == meta.StatusReasonNotFound {
return fmt.Errorf("\"%s\" not found", name)
}
return fmt.Errorf("getting namespace: %v", err)
}
if ns.Status.Phase != apicorev1.NamespaceActive {
if ns.Status.Phase != core.NamespaceActive {
return fmt.Errorf("invalid status: %v", ns.Status.Phase)
}
}
Expand Down
46 changes: 27 additions & 19 deletions pkg/cmd/resource_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package cmd

import (
"fmt"
rbac "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apismeta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
)
Expand All @@ -29,72 +30,75 @@ func NewResourceResolver(client discovery.DiscoveryInterface, mapper meta.RESTMa
}
}

func (rv *resourceResolver) Resolve(verbArg, resourceArg, subResource string) (string, error) {
resource, err := rv.resourceFor(resourceArg, subResource)
func (rv *resourceResolver) Resolve(verb, resource, subResource string) (string, error) {
if resource == rbac.ResourceAll {
return resource, nil
}
apiResource, err := rv.resourceFor(resource, subResource)
if err != nil {
name := resourceArg
name := resource
if subResource != "" {
name = name + "/" + subResource
}
return "", fmt.Errorf("the server doesn't have a resource type \"%s\"", name)
}

if !rv.isVerbSupportedBy(verbArg, resource) {
return "", fmt.Errorf("the \"%s\" resource does not support the \"%s\" verb, only %v", resource.Name, verbArg, resource.Verbs)
if !rv.isVerbSupportedBy(verb, apiResource) {
return "", fmt.Errorf("the \"%s\" resource does not support the \"%s\" verb, only %v", apiResource.Name, verb, apiResource.Verbs)
}

return resource.Name, nil
return apiResource.Name, nil
}

func (rv *resourceResolver) resourceFor(resourceArg, subResource string) (v1.APIResource, error) {
func (rv *resourceResolver) resourceFor(resourceArg, subResource string) (apismeta.APIResource, error) {
index, err := rv.indexResources()
if err != nil {
return v1.APIResource{}, err
return apismeta.APIResource{}, err
}

apiResource, err := rv.lookupResource(index, resourceArg)
if err != nil {
return v1.APIResource{}, err
return apismeta.APIResource{}, err
}

if subResource != "" {
apiResource, err = rv.lookupSubResource(index, apiResource.Name+"/"+subResource)
if err != nil {
return v1.APIResource{}, err
return apismeta.APIResource{}, err
}
return apiResource, nil
}
return apiResource, nil
}

func (rv *resourceResolver) lookupResource(index map[string]v1.APIResource, resourceArg string) (v1.APIResource, error) {
func (rv *resourceResolver) lookupResource(index map[string]apismeta.APIResource, resourceArg string) (apismeta.APIResource, error) {
resource, ok := index[resourceArg]
if ok {
return resource, nil
}

gvr, err := rv.mapper.ResourceFor(schema.GroupVersionResource{Resource: resourceArg})
if err != nil {
return v1.APIResource{}, err
return apismeta.APIResource{}, err
}
resource, ok = index[gvr.Resource]
if ok {
return resource, nil
}
return v1.APIResource{}, fmt.Errorf("not found \"%s\"", resourceArg)
return apismeta.APIResource{}, fmt.Errorf("not found \"%s\"", resourceArg)
}

func (rv *resourceResolver) lookupSubResource(index map[string]v1.APIResource, subResource string) (v1.APIResource, error) {
func (rv *resourceResolver) lookupSubResource(index map[string]apismeta.APIResource, subResource string) (apismeta.APIResource, error) {
apiResource, ok := index[subResource]
if !ok {
return v1.APIResource{}, fmt.Errorf("not found \"%s\"", subResource)
return apismeta.APIResource{}, fmt.Errorf("not found \"%s\"", subResource)
}
return apiResource, nil
}

// indexResources builds a lookup index for APIResources where the keys are resources names (both plural and short names).
func (rv *resourceResolver) indexResources() (map[string]v1.APIResource, error) {
serverResources := make(map[string]v1.APIResource)
func (rv *resourceResolver) indexResources() (map[string]apismeta.APIResource, error) {
serverResources := make(map[string]apismeta.APIResource)

serverGroups, err := rv.client.ServerGroups()
if err != nil {
Expand Down Expand Up @@ -125,7 +129,11 @@ func (rv *resourceResolver) indexResources() (map[string]v1.APIResource, error)
}

// isVerbSupportedBy returns `true` if the given verb is supported by the given resource, `false` otherwise.
func (rv *resourceResolver) isVerbSupportedBy(verb string, resource v1.APIResource) bool {
// Returns `true` if the given verb equals VerbAll.
func (rv *resourceResolver) isVerbSupportedBy(verb string, resource apismeta.APIResource) bool {
if verb == rbac.VerbAll {
return true
}
supported := false
for _, v := range resource.Verbs {
if v == verb {
Expand Down
16 changes: 13 additions & 3 deletions pkg/cmd/resource_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apismeta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/fake"
"testing"
Expand All @@ -25,10 +25,10 @@ func TestResourceResolver_Resolve(t *testing.T) {

client := fake.NewSimpleClientset()

client.Resources = []*v1.APIResourceList{
client.Resources = []*apismeta.APIResourceList{
{
GroupVersion: "v1",
APIResources: []v1.APIResource{
APIResources: []apismeta.APIResource{
{Version: "v1", Name: "pods", ShortNames: []string{"po"}, Verbs: []string{"list", "create", "delete"}},
{Version: "v1", Name: "pods/log", ShortNames: []string{}, Verbs: []string{"get"}},
{Version: "v1", Name: "services", ShortNames: []string{"svc"}, Verbs: []string{"list", "delete"}},
Expand Down Expand Up @@ -116,6 +116,16 @@ func TestResourceResolver_Resolve(t *testing.T) {
mappingResult: &mappingResult{err: errors.New("mapping failed")},
expected: expected{err: errors.New("the server doesn't have a resource type \"pod\"")},
},
{
scenario: "L",
given: given{verb: "*", resource: "pods"},
expected: expected{resource: "pods"},
},
{
scenario: "M",
given: given{verb: "list", resource: "*"},
expected: expected{resource: "*"},
},
}

for _, tt := range data {
Expand Down

0 comments on commit 5d0c6ed

Please sign in to comment.