From bd71ae50ce22d6843daed2d81671470229e3ef54 Mon Sep 17 00:00:00 2001 From: Gadi Naor <7112088+gadinaor@users.noreply.github.com> Date: Mon, 25 Oct 2021 10:40:30 +0300 Subject: [PATCH] Add to policy-rules information about the origin of a rule. (#43) Co-authored-by: Gadi Naor --- cmd/policyrules_cmd.go | 40 +++++++++- pkg/rbac/subject_permissions.go | 28 +++++-- .../policyrules/multiple-role-bindings.yaml | 79 +++++++++++++++++++ 3 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 testdata/policyrules/multiple-role-bindings.yaml diff --git a/cmd/policyrules_cmd.go b/cmd/policyrules_cmd.go index ff7279d..ae8b4c0 100644 --- a/cmd/policyrules_cmd.go +++ b/cmd/policyrules_cmd.go @@ -1,6 +1,7 @@ package cmd import ( + "bytes" "encoding/json" "fmt" "os" @@ -13,6 +14,8 @@ import ( "github.com/alcideio/rbac-tool/pkg/rbac" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/util/sets" ) func NewCommandPolicyRules() *cobra.Command { @@ -117,21 +120,26 @@ rbac-tool policy-rules -o json | jp "[? @.allowedTo[? (verb=='get' || verb=='*' allowedTo.Resource, strings.Join(allowedTo.ResourceNames, ","), strings.Join(allowedTo.NonResourceURLs, ","), + renderOriginatedFromColumn(allowedTo.Namespace, allowedTo.OriginatedFrom), } rows = append(rows, row) } } sort.Slice(rows, func(i, j int) bool { - if strings.Compare(rows[i][0], rows[j][0]) == 0 { - return (strings.Compare(rows[i][1], rows[j][1]) < 0) + + for c := range [6]int{} { + if strings.Compare(rows[i][c], rows[j][c]) == 0 { + continue + } + return (strings.Compare(rows[i][c], rows[j][c]) < 0) } - return (strings.Compare(rows[i][0], rows[j][0]) < 0) + return true }) table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"TYPE", "SUBJECT", "VERBS", "NAMESPACE", "API GROUP", "KIND", "NAMES", "NonResourceURI"}) + table.SetHeader([]string{"TYPE", "SUBJECT", "VERBS", "NAMESPACE", "API GROUP", "KIND", "NAMES", "NonResourceURI", "ORIGINATED FROM"}) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetBorder(false) table.SetAlignment(tablewriter.ALIGN_LEFT) @@ -175,3 +183,27 @@ rbac-tool policy-rules -o json | jp "[? @.allowedTo[? (verb=='get' || verb=='*' flags.BoolVarP(&inverse, "not", "n", false, "Inverse the regex matching. Use to search for users that do not match '^system:.*'") return cmd } + +func renderOriginatedFromColumn(ns string, list []v1.RoleRef) string { + roles := sets.NewString() + clusterRoles := sets.NewString() + s := bytes.NewBufferString("") + + for _, ref := range list { + if ref.Kind == "ClusterRole" { + clusterRoles.Insert(ref.Name) + } else { + roles.Insert(fmt.Sprintf("%v/%v ", ns, ref.Name)) + } + } + + if clusterRoles.Len() > 0 { + s.WriteString(fmt.Sprintf("ClusterRoles>>%v", strings.Join(clusterRoles.List(), ","))) + } + + if roles.Len() > 0 { + s.WriteString(fmt.Sprintf("Roles>>%v", strings.Join(roles.List(), ","))) + } + + return s.String() +} diff --git a/pkg/rbac/subject_permissions.go b/pkg/rbac/subject_permissions.go index a46bfda..480f48a 100644 --- a/pkg/rbac/subject_permissions.go +++ b/pkg/rbac/subject_permissions.go @@ -8,11 +8,18 @@ import ( "k8s.io/klog" ) +type PolicyRule struct { + v1.PolicyRule + + //Specify the Roles or ClusterRoles this rule originated from + OriginatedFrom []v1.RoleRef +} + type SubjectPermissions struct { Subject v1.Subject //Rules Per Namespace ... "" means cluster-wide - Rules map[string][]v1.PolicyRule + Rules map[string][]PolicyRule } func NewSubjectPermissions(perms *Permissions) []SubjectPermissions { @@ -47,18 +54,24 @@ func NewSubjectPermissions(perms *Permissions) []SubjectPermissions { if !exist { subPerms = &SubjectPermissions{ Subject: subject, - Rules: map[string][]v1.PolicyRule{}, + Rules: map[string][]PolicyRule{}, } } rules, exist := subPerms.Rules[binding.Namespace] if !exist { - rules = []v1.PolicyRule{} + rules = []PolicyRule{} } - //klog.V(6).Infof("%+v --add-- %v %v", subject, len(rules), len(role.Rules)) + roleRules := make([]PolicyRule, len(role.Rules)) + for i, _ := range role.Rules { + roleRules[i].PolicyRule = role.Rules[i] + roleRules[i].OriginatedFrom = []v1.RoleRef{binding.RoleRef} + } + + klog.V(6).Infof("%+v --add-- %v %v %+v", subject, len(rules), len(role.Rules), roleRules) - rules = append(rules, role.Rules...) + rules = append(rules, roleRules...) subPerms.Rules[binding.Namespace] = rules subjects[sub] = subPerms } @@ -107,6 +120,9 @@ type NamespacedPolicyRule struct { // NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path // Since non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding. NonResourceURLs []string `json:"nonResourceURLs,omitempty"` + + //The Role/ClusterRole rule references + OriginatedFrom []v1.RoleRef `json:"originatedFrom,omitempty"` } type SubjectPolicyList struct { @@ -152,6 +168,7 @@ func NewSubjectPermissionsList(policies []SubjectPermissions) []SubjectPolicyLis Resource: resource, ResourceNames: rule.ResourceNames, NonResourceURLs: rule.NonResourceURLs, + OriginatedFrom: rule.OriginatedFrom, } nsrules = append(nsrules, subjectPolicy) @@ -164,6 +181,7 @@ func NewSubjectPermissionsList(policies []SubjectPermissions) []SubjectPolicyLis Namespace: namespace, Verb: verb, NonResourceURLs: rule.NonResourceURLs, + OriginatedFrom: rule.OriginatedFrom, } nsrules = append(nsrules, subjectPolicy) diff --git a/testdata/policyrules/multiple-role-bindings.yaml b/testdata/policyrules/multiple-role-bindings.yaml new file mode 100644 index 0000000..10b0522 --- /dev/null +++ b/testdata/policyrules/multiple-role-bindings.yaml @@ -0,0 +1,79 @@ +# +# Install: +# kubectl apply -f testdata/policyrules/multiple-role-bindings.yaml +# +# Run: +# bin/rbac-tool policy-rules -e the-test-user | grep the-test-user +# +# Expect: +# +# ServiceAccount | the-test-user | get | policyrules | core | * | | | Roles>>policyrules/some-rules +# ServiceAccount | the-test-user | get | policyrules | core | * | | | Roles>>policyrules/more-rules +# ServiceAccount | the-test-user | get | policyrules | core | secrets | some-secret | | Roles>>policyrules/some-rules +# ServiceAccount | the-test-user | get | policyrules | core | secrets | | | Roles>>policyrules/more-rules +# ServiceAccount | the-test-user | list | policyrules | core | secrets | some-secret | | Roles>>policyrules/some-rules +# ServiceAccount | the-test-user | watch | policyrules | core | secrets | some-secret | | Roles>>policyrules/some-rules +# +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: policyrules + name: some-rules +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["secrets"] + resourceNames: ["some-secret"] + verbs: ["get", "watch", "list"] + - apiGroups: [""] # "" indicates the core API group + resources: ["*"] + verbs: ["get"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: policyrules + name: more-rules +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["secrets"] + verbs: ["get"] + - apiGroups: [""] # "" indicates the core API group + resources: ["*"] + verbs: ["get"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +# This role binding allows "jane" to read pods in the "default" namespace. +# You need to already have a Role named "pod-reader" in that namespace. +kind: RoleBinding +metadata: + name: some-rules-binding + namespace: policyrules +subjects: + - kind: ServiceAccount + name: the-test-user # "name" is case sensitive + namespace: policyrules +roleRef: + # "roleRef" specifies the binding to a Role / ClusterRole + kind: Role #this must be Role or ClusterRole + name: some-rules # this must match the name of the Role or ClusterRole you wish to bind to + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: rbac.authorization.k8s.io/v1 +# This role binding allows "jane" to read pods in the "default" namespace. +# You need to already have a Role named "pod-reader" in that namespace. +kind: RoleBinding +metadata: + name: more-rules-binding + namespace: policyrules +subjects: + - kind: ServiceAccount + name: the-test-user # "name" is case sensitive + namespace: policyrules +roleRef: + # "roleRef" specifies the binding to a Role / ClusterRole + kind: Role #this must be Role or ClusterRole + name: more-rules # this must match the name of the Role or ClusterRole you wish to bind to + apiGroup: rbac.authorization.k8s.io \ No newline at end of file