From e812bfbd5942ab5ab897ea7de289885ce05e53c1 Mon Sep 17 00:00:00 2001 From: Aya Igarashi Date: Wed, 15 May 2019 14:38:37 +0900 Subject: [PATCH] Make output more pretty --- README.md | 25 +++ cmd/cmd.go | 32 +++- go.mod | 3 +- go.sum | 4 + pkg/explorer/apipolicy.go | 58 +++++++ pkg/explorer/explorer.go | 317 +++++++++------------------------- pkg/explorer/resource.go | 50 ++++++ pkg/explorer/stringutil.go | 13 ++ pkg/explorer/subjutil.go | 61 +++++++ pkg/util/printer/converter.go | 97 +++++++++++ pkg/util/printer/printer.go | 133 +++++++++++++- test.sh | 3 + 12 files changed, 553 insertions(+), 243 deletions(-) create mode 100644 pkg/explorer/apipolicy.go create mode 100644 pkg/explorer/resource.go create mode 100644 pkg/explorer/stringutil.go create mode 100644 pkg/explorer/subjutil.go create mode 100644 pkg/util/printer/converter.go diff --git a/README.md b/README.md index 22972ea..8fa232e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,28 @@ # kubectl-bindrole Finding Kubernetes Roles bound to a specified ServiceAccount, Group or User. + + +## Design + +```bash +$ kubectl bindrole test-user + +[ServiceAccount] default/test-user +Secrets: +* default/test-user-token +BindedRoles: +* */edit +* default/test-role + +Policies: +- Name: default/test-role + APIPolicies: |- + + PodSecurityPolicies: |- + +- Name: edit + APIPolicies: |- + PodSecurityPolicies: |- + +``` diff --git a/cmd/cmd.go b/cmd/cmd.go index bcae3ae..eca746b 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -9,6 +9,7 @@ import ( "github.com/Ladicle/kubectl-bindrole/pkg/util/subject" "github.com/spf13/pflag" rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" ) @@ -62,17 +63,32 @@ func Execute() error { return err } - exp := explorer.NewPolicyExplorer(sub, client) - - pp := printer.DefaultPrettyPrinter() - pp.PrintSubject(exp.Subject) - pp.BlankLine() - if _, err := exp.NamespacedPolicy(); err != nil { + exp := explorer.NewPolicyExplorer(client) + nsp, err := exp.NamespacedSbjRoles(sub) + if err != nil { return err } - pp.BlankLine() - if _, err := exp.ClusterPolicy(); err != nil { + clusterp, err := exp.ClusterSbjRoles(sub) + if err != nil { return err } + + pp := printer.DefaultPrettyPrinter() + pp.PrintSubject(sub) + if sub.Kind == subject.KindSA { + sa, err := client.CoreV1().ServiceAccounts(sub.Namespace). + Get(sub.Name, metav1.GetOptions{}) + if err != nil { + return err + } + pp.PrintSA(sa) + } + + pp.BlankLine() + pp.PrintHeader("Policies") + pp.PrintPolicies(nsp) + pp.BlankLine() + pp.PrintPolicies(clusterp) + return nil } diff --git a/go.mod b/go.mod index 9306c8e..2439103 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.12 require ( github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946 - github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937 + github.com/mattn/go-runewidth v0.0.4 // indirect + github.com/olekukonko/tablewriter v0.0.1 github.com/spf13/pflag v1.0.3 k8s.io/api v0.0.0-20190511023547-e63b5755afac k8s.io/apimachinery v0.0.0-20190511023455-ad85901afca0 diff --git a/go.sum b/go.sum index a97254b..7ea1dd7 100644 --- a/go.sum +++ b/go.sum @@ -54,11 +54,15 @@ github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946 h1:z+WaKrgu3kCp github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= diff --git a/pkg/explorer/apipolicy.go b/pkg/explorer/apipolicy.go new file mode 100644 index 0000000..c370996 --- /dev/null +++ b/pkg/explorer/apipolicy.go @@ -0,0 +1,58 @@ +package explorer + +import rbacv1 "k8s.io/api/rbac/v1" + +const ( + VerbGet uint = 1 << iota + VerbList + VerbWatch + VerbCreate + VerbUpdate + VerbPatch + VerbDelete + VerbDeletionC +) + +type ResourceAPIPolicy struct { + Resource Resource + APIVerbFlag uint + OtherVerbs []string + ResourceName []string + NonResourceURL []string +} + +func NewResourceAPIPolicy(res Resource, rule rbacv1.PolicyRule) *ResourceAPIPolicy { + rapip := &ResourceAPIPolicy{ + Resource: res, + OtherVerbs: []string{}, + ResourceName: rule.ResourceNames, + NonResourceURL: rule.NonResourceURLs, + } + rapip.SetVerbs(rule.Verbs) + return rapip +} + +func (r *ResourceAPIPolicy) SetVerbs(verbs []string) { + for _, v := range verbs { + switch v { + case "get": + r.APIVerbFlag |= VerbGet + case "list": + r.APIVerbFlag |= VerbList + case "update": + r.APIVerbFlag |= VerbUpdate + case "delete": + r.APIVerbFlag |= VerbDelete + case "deletecollection": + r.APIVerbFlag |= VerbDeletionC + case "patch": + r.APIVerbFlag |= VerbPatch + case "create": + r.APIVerbFlag |= VerbCreate + case "watch": + r.APIVerbFlag |= VerbWatch + default: + r.OtherVerbs = append(r.OtherVerbs, v) + } + } +} diff --git a/pkg/explorer/explorer.go b/pkg/explorer/explorer.go index a6e7664..a95b99b 100644 --- a/pkg/explorer/explorer.go +++ b/pkg/explorer/explorer.go @@ -1,11 +1,8 @@ package explorer import ( - "fmt" - "strings" + "sort" - "github.com/Ladicle/kubectl-bindrole/pkg/util/subject" - "github.com/logrusorgru/aurora" policyv1beta1 "k8s.io/api/policy/v1beta1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -15,264 +12,122 @@ import ( const psp = "podsecuritypolicies" type PolicyExplorer struct { - Subject *rbacv1.Subject - - client *kubernetes.Clientset - targetNS string -} - -func NewPolicyExplorer(sub *rbacv1.Subject, client *kubernetes.Clientset) *PolicyExplorer { - var ns string - if sub.Kind == subject.KindSA { - ns = sub.Namespace - } else { - ns = metav1.NamespaceAll - } - - return &PolicyExplorer{ - Subject: sub, - - client: client, - targetNS: ns, - } + client *kubernetes.Clientset } -type Rule struct { - Resource Resource - APIPolicy APIPolicy - OtherVerbs []string -} - -type Resource struct { - Name string - Group string - Subresource string -} - -func (r Resource) String() string { - res := r.Name - if r.Group != "" { - res += "." + r.Group - } - if r.Subresource != "" { - res += "/" + r.Subresource - } - return res +func NewPolicyExplorer(client *kubernetes.Clientset) *PolicyExplorer { + return &PolicyExplorer{client: client} } -type APIPolicy struct { - Get bool - List bool - Watch bool - Create bool - Update bool - Patch bool - Delete bool - DeleteCollection bool +type SubjectRole struct { + Name string + Namespace string + PolicyList *SubjectPolicyList } -type NamespacedPolicy struct { - Name string - Namespace string - Rules []rbacv1.PolicyRule - PodSecurityPolicies []policyv1beta1.PodSecurityPolicy +type SubjectPolicyList struct { + APIPolicies []*ResourceAPIPolicy + PSPs []*policyv1beta1.PodSecurityPolicy } -func (e *PolicyExplorer) NamespacedPolicy() ([]NamespacedPolicy, error) { - list, err := e.client.RbacV1().RoleBindings(e.targetNS).List(metav1.ListOptions{}) +// NamespacedSbjRoles explores bound namespaced roles to the specified subject. +func (e *PolicyExplorer) NamespacedSbjRoles(sbj *rbacv1.Subject) ([]*SubjectRole, error) { + sbjrbs, err := subjectRoleBindings(e.client, sbj) if err != nil { return nil, err } - - var machiedBindings []rbacv1.RoleBinding - for _, b := range list.Items { - if e.isBind(b.Subjects) { - machiedBindings = append(machiedBindings, b) - } - } - - var policies []NamespacedPolicy - for _, b := range machiedBindings { - role, err := e.client.RbacV1().Roles(e.targetNS).Get(b.RoleRef.Name, metav1.GetOptions{}) + var sbjrs []*SubjectRole + for _, b := range sbjrbs { + role, err := e.client.RbacV1().Roles(sbj.Namespace). + Get(b.RoleRef.Name, metav1.GetOptions{}) if err != nil { return nil, err } - - policy := NamespacedPolicy{Name: role.Name, Namespace: role.Namespace} - - fmt.Printf("Role: %v/%v\n", role.Namespace, aurora.Green(role.Name).Bold()) - - fmt.Printf(" %25v\t", "Resource") - fmt.Printf("[%v]\t", "Verbs") - fmt.Printf("[%v]\t", "ResourceName") - fmt.Printf("[%v]\t", "NonResourceURL") - fmt.Println() - for _, rule := range role.Rules { - ress := rule2resources(&rule) - for _, res := range ress { - fmt.Printf(" %25v\t", res) - fmt.Printf("[%v]\t", strings.Join(rule.Verbs, ",")) - fmt.Printf("[%v]\t", strings.Join(rule.ResourceNames, ",")) - fmt.Printf("[%v]\t", strings.Join(rule.NonResourceURLs, ",")) - fmt.Println() - } - } - policies = append(policies, policy) - } - return policies, nil -} - -func rule2resources(rule *rbacv1.PolicyRule) []Resource { - var resources []Resource - for _, res := range rule.Resources { - ss := strings.Split(res, "/") - name := ss[0] - - var sub string - if len(ss) == 2 { - sub = ss[1] - } - - for _, group := range rule.APIGroups { - resources = append(resources, Resource{ - Name: name, - Group: group, - Subresource: sub, - }) + sbjpl, err := rule2sbjpl(e.client, role.Rules) + if err != nil { + return nil, err } + sbjrs = append(sbjrs, &SubjectRole{ + Name: role.Name, + Namespace: role.Namespace, + PolicyList: sbjpl, + }) } - return resources -} - -type ClusterPolicy struct { - Name string - Rules []rbacv1.PolicyRule - PodSecurityPolicies []policyv1beta1.PodSecurityPolicy + return sbjrs, nil } -func (e *PolicyExplorer) ClusterPolicy() ([]ClusterPolicy, error) { - list, err := e.client.RbacV1().ClusterRoleBindings().List(metav1.ListOptions{}) +// ClusterSbjRoles explores bound cluster roles to the specified subject. +func (e *PolicyExplorer) ClusterSbjRoles(sbj *rbacv1.Subject) ([]*SubjectRole, error) { + sbjcrbs, err := subjectClusterRoleBindings(e.client, sbj) if err != nil { return nil, err } - - var machiedBindings []rbacv1.ClusterRoleBinding - for _, b := range list.Items { - if e.isBind(b.Subjects) { - machiedBindings = append(machiedBindings, b) + var sbjrs []*SubjectRole + for _, b := range sbjcrbs { + role, err := e.client.RbacV1().ClusterRoles(). + Get(b.RoleRef.Name, metav1.GetOptions{}) + if err != nil { + return nil, err } - } - - var policies []ClusterPolicy - for _, b := range machiedBindings { - role, err := e.client.RbacV1().ClusterRoles().Get(b.RoleRef.Name, metav1.GetOptions{}) + sbjpl, err := rule2sbjpl(e.client, role.Rules) if err != nil { return nil, err } + sbjrs = append(sbjrs, &SubjectRole{ + Name: role.Name, + Namespace: role.Namespace, + PolicyList: sbjpl, + }) + } + return sbjrs, nil +} - policy := ClusterPolicy{Name: role.Name} - - fmt.Printf("ClusterRole: %v\n", aurora.Green(role.Name).Bold()) - fmt.Printf(" %25v\t", "Resource") - fmt.Printf("[%v]\t", "Verbs") - fmt.Printf("[%v]\t", "ResourceName") - fmt.Printf("[%v]\t", "NonResourceURL") - fmt.Println() - for _, rule := range role.Rules { - ress := rule2resources(&rule) - for _, res := range ress { - fmt.Printf(" %25v\t", res) - fmt.Printf("[%v]\t", strings.Join(rule.Verbs, ",")) - fmt.Printf("[%v]\t", strings.Join(rule.ResourceNames, ",")) - fmt.Printf("[%v]\t", strings.Join(rule.NonResourceURLs, ",")) - fmt.Println() +func rule2sbjpl(client *kubernetes.Clientset, rules []rbacv1.PolicyRule) (*SubjectPolicyList, error) { + sbjpl := &SubjectPolicyList{ + APIPolicies: []*ResourceAPIPolicy{}, + PSPs: []*policyv1beta1.PodSecurityPolicy{}, + } + rapipMap := make(map[string]*ResourceAPIPolicy) + + for _, rule := range rules { + // Set Pod-Security-Policy + if len(rule.Resources) == 1 && rule.Resources[0] == psp { + for _, name := range rule.ResourceNames { + psp, err := client.PolicyV1beta1().PodSecurityPolicies(). + Get(name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + sbjpl.PSPs = append(sbjpl.PSPs, psp) } + continue } - policies = append(policies, policy) - } - return policies, nil -} - -func (e *PolicyExplorer) isBind(subjects []rbacv1.Subject) bool { - for _, sub := range subjects { - if sub.Kind == e.Subject.Kind && sub.Name == e.Subject.Name { - if sub.Kind == subject.KindSA && - sub.Namespace != e.Subject.Namespace { - continue + // Set API policies + ress := rule2res(&rule) + for _, res := range ress { + respath := res.String() + v, ok := rapipMap[respath] + if ok { + if equalStrings(v.ResourceName, rule.ResourceNames) && + equalStrings(v.NonResourceURL, rule.NonResourceURLs) { + v.SetVerbs(rule.Verbs) + rapipMap[respath] = v + continue + } } - return true + rapipMap[respath] = NewResourceAPIPolicy(res, rule) } } - return false -} - -// func (e *PolicyExplorer) ClusterRoles() { -// fmt.Println("# Cluster Role") -// crbList, err := client.RbacV1().ClusterRoleBindings().List(metav1.ListOptions{}) -// if err != nil { -// log.Fatal(err) -// } - -// // TODO: Kind=Group,User -// var cBindedList []rbacv1.ClusterRoleBinding -// for _, crb := range crbList.Items { -// for _, sub := range crb.Subjects { -// if sub.Kind != "ServiceAccount" { -// continue -// } -// if sub.Namespace == ns && sub.Name == subName { -// cBindedList = append(cBindedList, crb) -// } -// } -// } -// pspList = []string{} -// for _, crb := range cBindedList { -// crole, err := client.RbacV1().ClusterRoles(). -// Get(crb.RoleRef.Name, metav1.GetOptions{}) -// if err != nil { -// log.Println(crb) -// log.Println(err) -// continue -// } -// fmt.Printf("Name: %v\n", crole.Name) -// fmt.Println("Rules:") -// for _, rule := range crole.Rules { -// for _, r := range rule.Resources { -// if r == psp { -// pspList = append(pspList, rule.ResourceNames...) -// continue -// } -// fmt.Printf("%v\t", r) -// fmt.Printf("%v\t", rule.Verbs) -// fmt.Printf("%v\t", strings.Join(rule.APIGroups, ",")) -// fmt.Printf("%v\t", rule.ResourceNames) -// fmt.Printf("%v\t", rule.NonResourceURLs) -// fmt.Println() -// } -// } -// // TODO -// fmt.Printf("AggregationRules: %v\n\n", crole.AggregationRule) -// } + var keyarr []string + for k := range rapipMap { + keyarr = append(keyarr, k) -// fmt.Println("## Pod Security Policy") -// for _, name := range pspList { -// psp, err := client.PolicyV1beta1().PodSecurityPolicies().Get(name, metav1.GetOptions{}) -// if err != nil { -// log.Println(err) -// continue -// } -// fmt.Printf("%v\t", psp.Name) -// fmt.Printf("%v\t", psp.Spec.AllowPrivilegeEscalation) -// fmt.Printf("%v\t", psp.Spec.AllowedCapabilities) -// fmt.Printf("%v\t", psp.Spec.SELinux) -// fmt.Printf("%v\t", psp.Spec.RunAsUser) -// fmt.Printf("%v\t", psp.Spec.FSGroup) -// fmt.Printf("%v\t", psp.Spec.SupplementalGroups) -// fmt.Printf("%v\t", psp.Spec.ReadOnlyRootFilesystem) -// fmt.Printf("%v\t", psp.Spec.SupplementalGroups) -// fmt.Printf("%v\t", psp.Spec.ReadOnlyRootFilesystem) -// fmt.Printf("%v\t", psp.Spec.Volumes) -// } -// } + } + sort.Strings(keyarr) + for _, key := range keyarr { + sbjpl.APIPolicies = append(sbjpl.APIPolicies, rapipMap[key]) + } + return sbjpl, nil +} diff --git a/pkg/explorer/resource.go b/pkg/explorer/resource.go new file mode 100644 index 0000000..7fa0990 --- /dev/null +++ b/pkg/explorer/resource.go @@ -0,0 +1,50 @@ +package explorer + +import ( + "strings" + + rbacv1 "k8s.io/api/rbac/v1" +) + +type Resource struct { + Name string + Groups []string + Subresource string +} + +func (r Resource) String() string { + res := r.Name + if r.Groups != nil { + if len(r.Groups) == 1 { + if r.Groups[0] != "" { + res += "." + r.Groups[0] + } + } else { + res += ".[" + strings.Join(r.Groups, ",") + "]" + } + } + if r.Subresource != "" { + res += "/" + r.Subresource + } + return res +} + +// rule2res converts Rule into the Resource list. +func rule2res(rule *rbacv1.PolicyRule) []Resource { + var resources []Resource + for _, res := range rule.Resources { + ss := strings.Split(res, "/") + name := ss[0] + + var sub string + if len(ss) == 2 { + sub = ss[1] + } + resources = append(resources, Resource{ + Name: name, + Groups: rule.APIGroups, + Subresource: sub, + }) + } + return resources +} diff --git a/pkg/explorer/stringutil.go b/pkg/explorer/stringutil.go new file mode 100644 index 0000000..f2c2dcd --- /dev/null +++ b/pkg/explorer/stringutil.go @@ -0,0 +1,13 @@ +package explorer + +func equalStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} diff --git a/pkg/explorer/subjutil.go b/pkg/explorer/subjutil.go new file mode 100644 index 0000000..a6135c7 --- /dev/null +++ b/pkg/explorer/subjutil.go @@ -0,0 +1,61 @@ +package explorer + +import ( + "github.com/Ladicle/kubectl-bindrole/pkg/util/subject" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// subjectRoles retrieve RoleBindings for the specified subject. +func subjectRoleBindings(client *kubernetes.Clientset, sbj *rbacv1.Subject) ([]rbacv1.RoleBinding, error) { + list, err := client.RbacV1().RoleBindings(sbj.Namespace). + List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + var sbjrs []rbacv1.RoleBinding + for _, b := range list.Items { + if containSubject(sbj, b.Subjects) { + sbjrs = append(sbjrs, b) + } + } + return sbjrs, nil +} + +// subjectClusterRoles retrieve ClusterRoleBindings for the specified subject. +func subjectClusterRoleBindings(client *kubernetes.Clientset, sbj *rbacv1.Subject) ([]rbacv1.ClusterRoleBinding, error) { + list, err := client.RbacV1().ClusterRoleBindings(). + List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + var sbjcrs []rbacv1.ClusterRoleBinding + for _, b := range list.Items { + if containSubject(sbj, b.Subjects) { + sbjcrs = append(sbjcrs, b) + } + } + return sbjcrs, nil +} + +// containSubject returns true if the specified subject is on the list. +func containSubject(s *rbacv1.Subject, list []rbacv1.Subject) bool { + for _, sub := range list { + if sameSubject(&sub, s) { + return true + } + } + return false +} + +// sameSubject returns true if s1 equals s2. +func sameSubject(s1, s2 *rbacv1.Subject) bool { + if s1.Kind != s2.Kind || s1.Name != s2.Name { + return false + } + if s1.Kind == subject.KindSA && s1.Namespace != s2.Namespace { + return false + } + return true +} diff --git a/pkg/util/printer/converter.go b/pkg/util/printer/converter.go new file mode 100644 index 0000000..a4a0b9e --- /dev/null +++ b/pkg/util/printer/converter.go @@ -0,0 +1,97 @@ +package printer + +import ( + "fmt" + "strings" + + "github.com/Ladicle/kubectl-bindrole/pkg/explorer" + "github.com/logrusorgru/aurora" + "k8s.io/api/core/v1" + "k8s.io/api/policy/v1beta1" +) + +const ( + allNamespace = "*" + + uBullet = "•" + uCheckBoxBlank = "☐" + uCheckBoxNG = "☒" + uCheckBoxOK = "☑" + uNG = "✗" + uNG2 = "✖" + uCheck = "✓" + uCheck2 = "✔" +) + +var bullet = aurora.Magenta(uBullet) + +func blank2Asterisk(s string) string { + if strings.TrimSpace(s) == "" { + return allNamespace + } + return s +} + +func joinOrAsterisk(list []string) string { + if len(list) == 0 { + return "[*]" + } + return join(list) +} + +func joinOrDash(list []string) string { + if len(list) == 0 { + return "[-]" + } + return join(list) +} + +func join(list []string) string { + return "[" + strings.Join(list, ", ") + "]" +} + +func apiVerb2CheckTable(flag uint) string { + return fmt.Sprintf("%v %v %v %v %v %v %v %v", + mark(flag&explorer.VerbGet == explorer.VerbGet), + mark(flag&explorer.VerbList == explorer.VerbList), + mark(flag&explorer.VerbWatch == explorer.VerbWatch), + mark(flag&explorer.VerbCreate == explorer.VerbCreate), + mark(flag&explorer.VerbUpdate == explorer.VerbUpdate), + mark(flag&explorer.VerbPatch == explorer.VerbPatch), + mark(flag&explorer.VerbDelete == explorer.VerbDelete), + mark(flag&explorer.VerbDeletionC == explorer.VerbDeletionC)) +} + +func colorBool(flag bool) string { + if flag { + return aurora.Green("True").String() + } + return aurora.Red("False").String() +} + +func mark(flag bool) aurora.Value { + if flag { + return aurora.Green(uCheck2) + } + return aurora.Red(uNG2) +} + +func joinCap(caps []v1.Capability) string { + var scaps []string + for _, cap := range caps { + scaps = append(scaps, string(cap)) + } + return join(scaps) +} + +func joinFsType(fsts []v1beta1.FSType) string { + var sfsts []string + for _, fstype := range fsts { + sfsts = append(sfsts, string(fstype)) + } + return join(sfsts) +} + +func tabHead(header string) string { + return aurora.Yellow(header).String() +} diff --git a/pkg/util/printer/printer.go b/pkg/util/printer/printer.go index cd92467..047366b 100644 --- a/pkg/util/printer/printer.go +++ b/pkg/util/printer/printer.go @@ -5,9 +5,13 @@ import ( "io" "os" + "github.com/Ladicle/kubectl-bindrole/pkg/explorer" "github.com/Ladicle/kubectl-bindrole/pkg/util/subject" "github.com/logrusorgru/aurora" - rbacv1 "k8s.io/api/rbac/v1" + "github.com/olekukonko/tablewriter" + core "k8s.io/api/core/v1" + policy "k8s.io/api/policy/v1beta1" + rbac "k8s.io/api/rbac/v1" ) type PrettyPrinter struct { @@ -20,16 +24,139 @@ func DefaultPrettyPrinter() *PrettyPrinter { } } -func (p *PrettyPrinter) PrintSubject(sub *rbacv1.Subject) { +func (p *PrettyPrinter) PrintSubject(sub *rbac.Subject) { var name string if sub.Kind == subject.KindSA { name = sub.Namespace + "/" + sub.Name } else { name = sub.Name } - fmt.Fprintf(p.out, "%v %v\n", aurora.Green(sub.Kind).Bold(), name) + fmt.Fprintf(p.out, "%v: %v\n", aurora.Yellow(sub.Kind), name) +} + +func (p *PrettyPrinter) PrintSA(sa *core.ServiceAccount) { + p.PrintHeader("Secrets") + for _, s := range sa.Secrets { + fmt.Fprintf(p.out, "%v %v/%v\n", bullet, blank2Asterisk(s.Namespace), s.Name) + } +} + +func (p *PrettyPrinter) PrintBindRoles(sbjrs []*explorer.SubjectRole) { + for _, r := range sbjrs { + fmt.Fprintf(p.out, "%v %v/%v\n", bullet, blank2Asterisk(r.Namespace), r.Name) + } +} + +func (p *PrettyPrinter) PrintPolicies(sbjrs []*explorer.SubjectRole) { + for i, r := range sbjrs { + if i != 0 { + p.BlankLine() + } + fmt.Fprintf(p.out, "%v %v: %v/%v\n", + bullet, aurora.BrightCyan("Name"), + blank2Asterisk(r.Namespace), r.Name) + + if len(r.PolicyList.APIPolicies) != 0 { + p.printAPIPolicy(r.PolicyList.APIPolicies) + } + + p.BlankLine() + if len(r.PolicyList.PSPs) != 0 { + p.printPSP(r.PolicyList.PSPs) + } + } +} + +func (p *PrettyPrinter) printAPIPolicy(apips []*explorer.ResourceAPIPolicy) { + tw := p.newTabwriter() + defer tw.Render() + + tw.Append([]string{ + tabHead("Resource"), + tabHead("Name"), + tabHead("Exclude"), + tabHead("Verbs"), + tabHead("G L W C U P D DC")}) + + tw.SetColumnAlignment([]int{ + tablewriter.ALIGN_LEFT, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_LEFT}) + + for _, policy := range apips { + tw.Append([]string{ + policy.Resource.String(), + joinOrAsterisk(policy.ResourceName), + joinOrDash(policy.NonResourceURL), + joinOrDash(policy.OtherVerbs), + apiVerb2CheckTable(policy.APIVerbFlag), + }) + } +} + +func (p *PrettyPrinter) printPSP(psps []*policy.PodSecurityPolicy) { + tw := p.newTabwriter() + defer tw.Render() + + tw.Append([]string{ + tabHead("Name"), + tabHead("PRIV"), + tabHead("RO-RootFS"), + tabHead("Volumes"), + tabHead("Caps"), + tabHead("SELinux"), + tabHead("RunAsUser"), + tabHead("FSgroup"), + tabHead("SUPgroup")}) + + tw.SetColumnAlignment([]int{ + tablewriter.ALIGN_LEFT, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER}) + + for _, policy := range psps { + tw.Append([]string{ + policy.Name, + colorBool(*policy.Spec.AllowPrivilegeEscalation), + colorBool(policy.Spec.ReadOnlyRootFilesystem), + joinFsType(policy.Spec.Volumes), + joinCap(policy.Spec.AllowedCapabilities), + string(policy.Spec.SELinux.Rule), + string(policy.Spec.RunAsUser.Rule), + string(policy.Spec.FSGroup.Rule), + string(policy.Spec.SupplementalGroups.Rule), + }) + } } func (p *PrettyPrinter) BlankLine() { fmt.Fprintln(p.out) } + +func (p *PrettyPrinter) newTabwriter() *tablewriter.Table { + tw := tablewriter.NewWriter(p.out) + tw.SetRowSeparator("") + tw.SetCenterSeparator("") + tw.SetColumnSeparator("") + tw.SetBorder(false) + tw.SetRowLine(false) + tw.SetHeaderLine(false) + tw.SetAutoWrapText(false) + return tw +} + +func (p *PrettyPrinter) PrintHeader(header string) { + fmt.Fprintln(p.out, aurora.BrightCyan(header+":")) +} + +func (p *PrettyPrinter) printHeaderL2(header string) { + fmt.Fprintln(p.out, aurora.BrightCyan(" "+header+":")) +} diff --git a/test.sh b/test.sh index 45229b8..4958819 100755 --- a/test.sh +++ b/test.sh @@ -80,3 +80,6 @@ kubectl create clusterrolebinding test --clusterrole edit --serviceaccount defau echo; echo "Test..." ./kubectl-bindrole test-user + +echo; echo "Roles via kubectl..." +kubectl describe clusterrole edit|grep '\[\]'|sort