k9s/internal/view/policy.go

346 lines
8.1 KiB
Go

package view
import (
"context"
"fmt"
"time"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
const (
policyTitle = "Policy"
group = "Group"
user = "User"
sa = "ServiceAccount"
)
type (
namespacedRole struct {
ns, role string
}
// Policy presents a RBAC policy viewer.
Policy struct {
*Table
cancel context.CancelFunc
subjectKind string
subjectName string
cache render.RowEvents
}
)
// NewPolicy returns a new viewer.
func NewPolicy(app *App, subject, name string) *Policy {
return &Policy{
Table: NewTable(policyTitle),
subjectKind: mapSubject(subject),
subjectName: name,
}
}
// Init the view.
func (p *Policy) Init(ctx context.Context) error {
p.Table.Path = p.subjectKind + ":" + p.subjectName
if err := p.Table.Init(ctx); err != nil {
return err
}
p.SetColorerFn(render.Policy{}.ColorerFunc())
p.bindKeys()
p.SetSortCol(1, len(render.Policy{}.Header(render.AllNamespaces)), false)
p.refresh()
p.SelectRow(1, true)
return nil
}
func (p *Policy) Name() string {
return "policy"
}
func (p *Policy) Start() {
p.Stop()
ctx, cancel := context.WithCancel(context.Background())
p.cancel = cancel
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-time.After(time.Duration(p.app.Config.K9s.GetRefreshRate()) * time.Second):
p.refresh()
}
}
}(ctx)
}
func (p *Policy) bindKeys() {
p.Actions().Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace)
p.Actions().Add(ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Back", p.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", p.activateCmd, false),
ui.KeyShiftP: ui.NewKeyAction("Sort Namespace", p.SortColCmd(0, true), false),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.SortColCmd(1, true), false),
ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.SortColCmd(2, true), false),
ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.SortColCmd(3, true), false),
})
}
func (p *Policy) getTitle() string {
return fmt.Sprintf(rbacTitleFmt, policyTitle, p.subjectKind+":"+p.subjectName, p.GetRowCount())
}
func (p *Policy) refresh() {
log.Debug().Msgf(">>>>>>>>>>>>>>> Refreshing Policies")
// BOZO!!
defer func(t time.Time) {
log.Debug().Msgf("Policy Refresh elapsed %v", time.Since(t))
}(time.Now())
data, err := p.reconcile()
if err != nil {
log.Error().Err(err).Msgf("Refresh for %s:%s", p.subjectKind, p.subjectName)
p.app.Flash().Err(err)
}
p.app.QueueUpdateDraw(func() {
p.Update(data)
})
}
func (p *Policy) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !p.SearchBuff().Empty() {
p.SearchBuff().Reset()
return nil
}
return p.backCmd(evt)
}
func (p *Policy) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if p.cancel != nil {
p.cancel()
}
if p.SearchBuff().IsActive() {
p.SearchBuff().Reset()
return nil
}
return p.app.PrevCmd(evt)
}
func (p *Policy) reconcile() (render.TableData, error) {
// BOZO!!
defer func(t time.Time) {
log.Debug().Msgf("Policy Reconcile elapsed %v", time.Since(t))
}(time.Now())
var table render.TableData
evts, errs := p.fetchClusterRoleBindings()
if len(errs) > 0 {
for _, err := range errs {
log.Error().Err(err).Msg("Unable to find cluster policies")
}
return table, errs[0]
}
nevts, errs := p.namespacedPolicies()
if len(errs) > 0 {
for _, err := range errs {
log.Error().Err(err).Msg("Unable to find cluster policies")
}
return table, errs[0]
}
for _, v := range nevts {
evts = append(evts, v)
}
return buildTable(p, evts), nil
}
// Protocol...
func (p *Policy) Header() render.HeaderRow {
return render.Policy{}.Header(render.AllNamespaces)
}
func (p *Policy) GetCache() render.RowEvents {
return p.cache
}
func (p *Policy) SetCache(evts render.RowEvents) {
p.cache = evts
}
func (p *Policy) fetchClusterRoleBindings() (render.Rows, []error) {
var errs []error
oo, err := p.app.factory.List(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterrolebindings", labels.Everything())
if err != nil {
return nil, append(errs, err)
}
roles := make([]string, 0, len(oo))
for _, o := range oo {
var crb rbacv1.ClusterRoleBinding
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb)
if err != nil {
errs = append(errs, err)
continue
}
for _, s := range crb.Subjects {
if s.Kind == p.subjectKind && s.Name == p.subjectName {
roles = append(roles, crb.RoleRef.Name)
}
}
}
rows := make(render.Rows, 0, len(oo))
for _, role := range roles {
o, err := p.app.factory.Get(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterroles", role, labels.Everything())
if err != nil {
return nil, append(errs, err)
}
var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr)
if err != nil {
errs = append(errs, err)
continue
}
for _, v := range p.parseRules("*", "CR:"+role, cr.Rules) {
rows = append(rows, v)
}
}
return rows, errs
}
func (p *Policy) fetchRoleBindings() ([]namespacedRole, error) {
oo, err := p.app.factory.List(render.AllNamespaces, "rbac.authorization.k8s.io/v1/rolebindings", labels.Everything())
if err != nil {
return nil, err
}
rr := make([]namespacedRole, 0, len(oo))
for _, o := range oo {
var rb rbacv1.RoleBinding
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb)
if err != nil {
return nil, err
}
for _, s := range rb.Subjects {
if s.Kind == p.subjectKind && s.Name == p.subjectName {
rr = append(rr, namespacedRole{rb.Namespace, rb.RoleRef.Name})
}
}
}
return rr, nil
}
func (p *Policy) fetchClusterRoles(errs []error, rr []namespacedRole) (render.Rows, []error) {
rows := make(render.Rows, 0, len(rr))
for _, r := range rr {
o, err := p.app.factory.Get(r.ns, "rbac.authorization.k8s.io/v1/clusterroles", r.role, labels.Everything())
if err != nil {
return nil, append(errs, err)
}
var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr)
if err != nil {
errs = append(errs, err)
continue
}
rows = append(rows, p.parseRules(r.ns, "RO:"+r.role, cr.Rules)...)
}
return rows, errs
}
func (p *Policy) namespacedPolicies() (render.Rows, []error) {
var errs []error
roles, err := p.fetchRoleBindings()
if err != nil {
errs = append(errs, err)
}
return p.fetchClusterRoles(errs, roles)
}
func (p *Policy) parseRules(ns, binding string, rules []rbacv1.PolicyRule) render.Rows {
m := make(render.Rows, 0, len(rules))
for _, r := range rules {
for _, grp := range r.APIGroups {
for _, res := range r.Resources {
k := res
if grp != "" {
k = res + "." + grp
}
for _, na := range r.ResourceNames {
n := fqn(k, na)
m = append(m, render.Row{
ID: fqn(ns, n),
Fields: append(policyRow(ns, n, grp, binding), asVerbs(r.Verbs...)...),
})
}
m = append(m, render.Row{
ID: fqn(ns, k),
Fields: append(policyRow(ns, k, grp, binding), asVerbs(r.Verbs...)...),
})
}
}
for _, nres := range r.NonResourceURLs {
if nres[0] != '/' {
nres = "/" + nres
}
m = append(m, render.Row{
ID: fqn(ns, nres),
Fields: append(policyRow(ns, nres, "", binding), asVerbs(r.Verbs...)...),
})
}
}
return m
}
func policyRow(ns, res, grp, binding string) render.Fields {
if grp != "" {
grp = toGroup(grp)
}
r := make(render.Fields, 0, len(render.Policy{}.Header(render.AllNamespaces)))
return append(r, ns, res, grp, binding)
}
func mapSubject(subject string) string {
switch subject {
case "g":
return group
case "s":
return sa
default:
return user
}
}
func showSAPolicy(app *App, _, _, selection string) {
_, n := namespaced(selection)
subject, err := mapFuSubject("ServiceAccount")
if err != nil {
app.Flash().Err(err)
return
}
app.inject(NewPolicy(app, subject, n))
}