Skip to content

Commit

Permalink
feat: Add -o wide flag to print the ROLE column (#79)
Browse files Browse the repository at this point in the history
Co-authored-by: Liz Rice <[email protected]>
Signed-off-by: Daniel Pacak <[email protected]>
  • Loading branch information
danielpacak and lizrice authored Aug 21, 2020
1 parent 5d0f3d1 commit bb71880
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 182 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ unit-tests: $(SOURCES)

integration-tests: $(SOURCES)
GO111MODULE=on go test -v test/integration_test.go

.PHONY: clean
clean:
rm $(BINARY)
rm coverage.txt
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![Coverage Status][cov-img]][cov]
[![Go Report Card][report-card-img]][report-card]
[![License][license-img]][license]
[![GitHub All Releases][github-all-releases-img]][release]

# kubectl-who-can

Expand Down Expand Up @@ -87,7 +88,7 @@ For additional details on flags and usage, run `kubectl who-can --help`.

This repository is available under the [Apache License 2.0](https://github.com/aquasecurity/kubectl-who-can/blob/master/LICENSE).

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

[build-action-img]: https://github.com/aquasecurity/kubectl-who-can/workflows/build/badge.svg
Expand All @@ -101,7 +102,7 @@ This repository is available under the [Apache License 2.0](https://github.com/a

[license-img]: https://img.shields.io/github/license/aquasecurity/kubectl-who-can.svg
[license]: https://github.com/aquasecurity/kubectl-who-can/blob/master/LICENSE
[github-all-releases-img]: https://img.shields.io/github/downloads/aquasecurity/kubectl-who-can/total?logo=github

[asciicast-img]: https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j.svg
[asciicast]: https://asciinema.org/a/ccqqYwA5L5rMV9kd1tgzyZJ2j

9 changes: 6 additions & 3 deletions cmd/kubectl-who-can/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package main

import (
"fmt"

"github.com/aquasecurity/kubectl-who-can/pkg/cmd"
clioptions "k8s.io/cli-runtime/pkg/genericclioptions"
// Load all known auth plugins

"flag"
"os"

"github.com/spf13/pflag"
// Load all known auth plugins
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/klog"
"os"
)

func initFlags() {
Expand All @@ -30,7 +33,7 @@ func main() {
initFlags()
root, err := cmd.NewWhoCanCommand(clioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr})
if err != nil {
fmt.Printf("Error: %v\n", err)
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := root.Execute(); err != nil {
Expand Down
68 changes: 18 additions & 50 deletions pkg/cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"errors"
"flag"
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
"io"
core "k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
Expand All @@ -19,8 +20,6 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog"
"strings"
"text/tabwriter"
)

const (
Expand Down Expand Up @@ -57,15 +56,21 @@ NONRESOURCEURL is a partial URL that starts with "/".`
# List who can access the URL /logs/
kubectl who-can get /logs`
)

const (
// RoleKind is the RoleRef's Kind referencing a Role.
RoleKind = "Role"
// ClusterRoleKind is the RoleRef's Kind referencing a ClusterRole.
ClusterRoleKind = "ClusterRole"
)

const (
subResourceFlag = "subresource"
allNamespacesFlag = "all-namespaces"
namespaceFlag = "namespace"
outputFlag = "output"
outputWide = "wide"
)

// Action represents an action a subject can be given permission to.
Expand Down Expand Up @@ -158,23 +163,31 @@ func NewWhoCanCommand(streams clioptions.IOStreams) (*cobra.Command, error) {
return err
}

output, err := cmd.Flags().GetString(outputFlag)
if err != nil {
return err
}

printer := NewPrinter(streams.Out, output == outputWide)

// Output warnings
PrintWarnings(streams.Out, warnings)
printer.PrintWarnings(warnings)

roleBindings, clusterRoleBindings, err := o.Check(action)
if err != nil {
return err
}

// Output check results
PrintChecks(streams.Out, action, roleBindings, clusterRoleBindings)
printer.PrintChecks(action, roleBindings, clusterRoleBindings)

return nil
},
}

cmd.Flags().String(subResourceFlag, "", "SubResource such as pod/log or deployment/scale")
cmd.Flags().BoolP(allNamespacesFlag, "A", false, "If true, check for users that can do the specified action in any of the available namespaces")
cmd.Flags().StringP(outputFlag, "o", "", "Output format. Currently the only supported output format is wide.")

flag.CommandLine.VisitAll(func(gf *flag.Flag) {
cmd.Flags().AddGoFlag(gf)
Expand Down Expand Up @@ -436,51 +449,6 @@ func (w *WhoCan) getClusterRoleBindings(clusterRoleNames clusterRoles) (clusterR
return
}

// PrintWarnings prints warnings, if any, returned by CheckAPIAccess.
func PrintWarnings(out io.Writer, warnings []string) {
if len(warnings) > 0 {
_, _ = fmt.Fprintln(out, "Warning: The list might not be complete due to missing permission(s):")
for _, warning := range warnings {
_, _ = fmt.Fprintf(out, "\t%s\n", warning)
}
_, _ = fmt.Fprintln(out)
}
}

// PrintChecks prints permission checks returned by Check()
func PrintChecks(out io.Writer, action Action, roleBindings []rbac.RoleBinding, clusterRoleBindings []rbac.ClusterRoleBinding) {
wr := new(tabwriter.Writer)
wr.Init(out, 0, 8, 2, ' ', 0)

if action.Resource != "" {
// NonResourceURL permissions can only be granted through ClusterRoles. Hence no point in printing RoleBindings section.
if len(roleBindings) == 0 {
_, _ = fmt.Fprintf(out, "No subjects found with permissions to %s assigned through RoleBindings\n", action)
} else {
_, _ = fmt.Fprintln(wr, "ROLEBINDING\tNAMESPACE\tSUBJECT\tTYPE\tSA-NAMESPACE")
for _, rb := range roleBindings {
for _, s := range rb.Subjects {
_, _ = fmt.Fprintf(wr, "%s\t%s\t%s\t%s\t%s\n", rb.Name, rb.GetNamespace(), s.Name, s.Kind, s.Namespace)
}
}
}

_, _ = fmt.Fprintln(wr)
}

if len(clusterRoleBindings) == 0 {
_, _ = fmt.Fprintf(out, "No subjects found with permissions to %s assigned through ClusterRoleBindings\n", action)
} else {
_, _ = fmt.Fprintln(wr, "CLUSTERROLEBINDING\tSUBJECT\tTYPE\tSA-NAMESPACE")
for _, rb := range clusterRoleBindings {
for _, s := range rb.Subjects {
_, _ = fmt.Fprintf(wr, "%s\t%s\t%s\t%s\n", rb.Name, s.Name, s.Kind, s.Namespace)
}
}
}
_ = wr.Flush()
}

func (w Action) String() string {
if w.NonResourceURL != "" {
return fmt.Sprintf("%s %s", w.Verb, w.NonResourceURL)
Expand Down
129 changes: 2 additions & 127 deletions pkg/cmd/list_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package cmd

import (
"bytes"
"errors"
"testing"

"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand All @@ -14,7 +15,6 @@ import (
"k8s.io/client-go/kubernetes/fake"
clientTesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/clientcmd"
"testing"

rbac "k8s.io/api/rbac/v1"
)
Expand Down Expand Up @@ -578,128 +578,3 @@ func TestWhoCan_GetClusterRoleBindings(t *testing.T) {
assert.Equal(t, 1, len(bindings))
assert.Contains(t, bindings, getHealthzBnd)
}

func TestPrintWarnings(t *testing.T) {

data := []struct {
scenario string
warnings []string
expectedOutput string
}{
{
scenario: "A",
warnings: []string{"w1", "w2"},
expectedOutput: "Warning: The list might not be complete due to missing permission(s):\n\tw1\n\tw2\n\n",
},
{
scenario: "B",
warnings: []string{},
expectedOutput: "",
},
{
scenario: "C",
warnings: nil,
expectedOutput: "",
},
}

for _, tt := range data {
t.Run(tt.scenario, func(t *testing.T) {
var buf bytes.Buffer
PrintWarnings(&buf, tt.warnings)
assert.Equal(t, tt.expectedOutput, buf.String())
})
}
}

func TestPrintChecks(t *testing.T) {
data := []struct {
scenario string

verb string
resource string
nonResourceURL string
resourceName string

roleBindings []rbac.RoleBinding
clusterRoleBindings []rbac.ClusterRoleBinding

output string
}{
{
scenario: "A",
verb: "get", resource: "pods", resourceName: "",
output: `No subjects found with permissions to get pods assigned through RoleBindings
No subjects found with permissions to get pods assigned through ClusterRoleBindings
`,
},
{
scenario: "B",
verb: "get", resource: "pods", resourceName: "my-pod",
output: `No subjects found with permissions to get pods/my-pod assigned through RoleBindings
No subjects found with permissions to get pods/my-pod assigned through ClusterRoleBindings
`,
},
{
scenario: "C",
verb: "get", nonResourceURL: "/healthz",
output: "No subjects found with permissions to get /healthz assigned through ClusterRoleBindings\n",
},
{
scenario: "D",
verb: "get", resource: "pods",
roleBindings: []rbac.RoleBinding{
{
ObjectMeta: meta.ObjectMeta{Name: "Alice-can-view-pods", Namespace: "default"},
Subjects: []rbac.Subject{
{Name: "Alice", Kind: "User"},
}},
{
ObjectMeta: meta.ObjectMeta{Name: "Admins-can-view-pods", Namespace: "bar"},
Subjects: []rbac.Subject{
{Name: "Admins", Kind: "Group"},
}},
},
clusterRoleBindings: []rbac.ClusterRoleBinding{
{
ObjectMeta: meta.ObjectMeta{Name: "Bob-and-Eve-can-view-pods", Namespace: "default"},
Subjects: []rbac.Subject{
{Name: "Bob", Kind: "ServiceAccount", Namespace: "foo"},
{Name: "Eve", Kind: "User"},
},
},
},
output: `ROLEBINDING NAMESPACE SUBJECT TYPE SA-NAMESPACE
Alice-can-view-pods default Alice User
Admins-can-view-pods bar Admins Group
CLUSTERROLEBINDING SUBJECT TYPE SA-NAMESPACE
Bob-and-Eve-can-view-pods Bob ServiceAccount foo
Bob-and-Eve-can-view-pods Eve User
`,
},
}

for _, tt := range data {
t.Run(tt.scenario, func(t *testing.T) {
// given
var buf bytes.Buffer
action := Action{
Verb: tt.verb,
Resource: tt.resource,
NonResourceURL: tt.nonResourceURL,
ResourceName: tt.resourceName,
}

// when
PrintChecks(&buf, action, tt.roleBindings, tt.clusterRoleBindings)

// then
assert.Equal(t, tt.output, buf.String())
})

}

}
Loading

0 comments on commit bb71880

Please sign in to comment.