Fix issue #211 + ns access msg

mine
derailed 2019-06-04 09:47:17 -06:00
parent 574c58e329
commit b66a25e5f8
11 changed files with 83 additions and 44 deletions

View File

@ -24,19 +24,23 @@ func NewMockConnection() *MockConnection {
return &MockConnection{fail: pegomock.GlobalFailHandler} return &MockConnection{fail: pegomock.GlobalFailHandler}
} }
func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) bool { func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) (bool, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().") panic("mock must not be nil. Use myMock := NewMockConnection().")
} }
params := []pegomock.Param{_param0, _param1, _param2, _param3} params := []pegomock.Param{_param0, _param1, _param2, _param3}
result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 bool var ret0 bool
var ret1 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(bool) ret0 = result[0].(bool)
} }
if result[1] != nil {
ret1 = result[1].(error)
}
} }
return ret0 return ret0, ret1
} }
func (mock *MockConnection) Config() *k8s.Config { func (mock *MockConnection) Config() *k8s.Config {

View File

@ -64,7 +64,7 @@ type (
ServerVersion() (*version.Info, error) ServerVersion() (*version.Info, error)
FetchNodes() (*v1.NodeList, error) FetchNodes() (*v1.NodeList, error)
CurrentNamespaceName() (string, error) CurrentNamespaceName() (string, error)
CanIAccess(ns, name, resURL string, verbs []string) bool CanIAccess(ns, name, resURL string, verbs []string) (bool, error)
} }
// APIClient represents a Kubernetes api client. // APIClient represents a Kubernetes api client.
@ -90,7 +90,7 @@ func InitConnectionOrDie(config *Config, logger zerolog.Logger) *APIClient {
} }
// CanIAccess checks if user has access to a certain resource. // CanIAccess checks if user has access to a certain resource.
func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) bool { func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) (bool, error) {
_, gr := schema.ParseResourceArg(strings.ToLower(resURL)) _, gr := schema.ParseResourceArg(strings.ToLower(resURL))
sar := &authorizationv1.SelfSubjectAccessReview{ sar := &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
@ -104,24 +104,31 @@ func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) bool {
}, },
} }
var resp *authorizationv1.SelfSubjectAccessReview user, _ := a.Config().CurrentUserName()
var err error groups, _ := a.Config().CurrentGroupNames()
var allow bool log.Debug().Msgf("AuthInfo user/groups: %s:%v", user, groups)
var (
resp *authorizationv1.SelfSubjectAccessReview
err error
allow bool
)
for _, v := range verbs { for _, v := range verbs {
sar.Spec.ResourceAttributes.Verb = v sar.Spec.ResourceAttributes.Verb = v
resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar) resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar)
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("CanIAccess") log.Warn().Err(err).Msgf("CanIAccess")
return false return false, err
} }
log.Debug().Msgf("CHECKING ACCESS for %s/%s/ in NS %q verb: %s -> %t, %s", resURL, name, ns, v, resp.Status.Allowed, resp.Status.Reason) log.Debug().Msgf("CHECKING ACCESS res:%s-%q for NS: %q Verb: %s -> %t, %s", resURL, name, ns, v, resp.Status.Allowed, resp.Status.Reason)
if !resp.Status.Allowed { if !resp.Status.Allowed {
return false return false, err
} }
allow = true allow = true
} }
log.Debug().Msgf("GRANT ACCESS:%t", allow) log.Debug().Msgf("GRANT ACCESS? >>> %t", allow)
return allow
return allow, nil
} }
// CurrentNamespaceName return namespace name set via either cli arg or cluster config. // CurrentNamespaceName return namespace name set via either cli arg or cluster config.

View File

@ -163,6 +163,15 @@ func (c *Config) ClusterNames() ([]string, error) {
return cc, nil return cc, nil
} }
// CurrentGroupNames retrieves the active group names.
func (c *Config) CurrentGroupNames() ([]string, error) {
if c.flags.ImpersonateGroup != nil && len(*c.flags.ImpersonateGroup) != 0 {
return *c.flags.ImpersonateGroup, nil
}
return []string{}, errors.New("unable to locate current group")
}
// CurrentUserName retrieves the active user name. // CurrentUserName retrieves the active user name.
func (c *Config) CurrentUserName() (string, error) { func (c *Config) CurrentUserName() (string, error) {
if isSet(c.flags.Impersonate) { if isSet(c.flags.Impersonate) {
@ -186,7 +195,7 @@ func (c *Config) CurrentUserName() (string, error) {
return ctx.AuthInfo, nil return ctx.AuthInfo, nil
} }
return "", errors.New("unable to locate current cluster") return "", errors.New("unable to locate current user")
} }
// CurrentNamespaceName retrieves the active namespace. // CurrentNamespaceName retrieves the active namespace.
@ -199,12 +208,10 @@ func (c *Config) CurrentNamespaceName() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
ctx, err := c.CurrentContextName() ctx, err := c.CurrentContextName()
if err != nil { if err != nil {
return "", err return "", err
} }
if ctx, ok := cfg.Contexts[ctx]; ok { if ctx, ok := cfg.Contexts[ctx]; ok {
if isSet(&ctx.Namespace) { if isSet(&ctx.Namespace) {
return ctx.Namespace, nil return ctx.Namespace, nil

View File

@ -24,19 +24,23 @@ func NewMockClusterMeta() *MockClusterMeta {
return &MockClusterMeta{fail: pegomock.GlobalFailHandler} return &MockClusterMeta{fail: pegomock.GlobalFailHandler}
} }
func (mock *MockClusterMeta) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) bool { func (mock *MockClusterMeta) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) (bool, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().") panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
} }
params := []pegomock.Param{_param0, _param1, _param2, _param3} params := []pegomock.Param{_param0, _param1, _param2, _param3}
result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 bool var ret0 bool
var ret1 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(bool) ret0 = result[0].(bool)
} }
if result[1] != nil {
ret1 = result[1].(error)
}
} }
return ret0 return ret0, ret1
} }
func (mock *MockClusterMeta) ClusterName() string { func (mock *MockClusterMeta) ClusterName() string {

View File

@ -24,19 +24,23 @@ func NewMockConnection() *MockConnection {
return &MockConnection{fail: pegomock.GlobalFailHandler} return &MockConnection{fail: pegomock.GlobalFailHandler}
} }
func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) bool { func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) (bool, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().") panic("mock must not be nil. Use myMock := NewMockConnection().")
} }
params := []pegomock.Param{_param0, _param1, _param2, _param3} params := []pegomock.Param{_param0, _param1, _param2, _param3}
result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 bool var ret0 bool
var ret1 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(bool) ret0 = result[0].(bool)
} }
if result[1] != nil {
ret1 = result[1].(error)
}
} }
return ret0 return ret0, ret1
} }
func (mock *MockConnection) Config() *k8s.Config { func (mock *MockConnection) Config() *k8s.Config {

View File

@ -24,19 +24,23 @@ func NewMockClusterMeta() *MockClusterMeta {
return &MockClusterMeta{fail: pegomock.GlobalFailHandler} return &MockClusterMeta{fail: pegomock.GlobalFailHandler}
} }
func (mock *MockClusterMeta) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) bool { func (mock *MockClusterMeta) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) (bool, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().") panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
} }
params := []pegomock.Param{_param0, _param1, _param2, _param3} params := []pegomock.Param{_param0, _param1, _param2, _param3}
result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 bool var ret0 bool
var ret1 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(bool) ret0 = result[0].(bool)
} }
if result[1] != nil {
ret1 = result[1].(error)
}
} }
return ret0 return ret0, ret1
} }
func (mock *MockClusterMeta) ClusterName() string { func (mock *MockClusterMeta) ClusterName() string {

View File

@ -6,6 +6,7 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
) )
const ( const (
@ -36,7 +37,7 @@ func (v *namespaceView) extraActions(aa keyActions) {
} }
func (v *namespaceView) switchNs(app *appView, _, res, sel string) { func (v *namespaceView) switchNs(app *appView, _, res, sel string) {
v.useNamespace(sel) v.useNamespace(v.cleanser(sel))
app.gotoResource("po", true) app.gotoResource("po", true)
} }
@ -63,12 +64,13 @@ func (v *namespaceView) getSelectedItem() string {
} }
func (*namespaceView) cleanser(s string) string { func (*namespaceView) cleanser(s string) string {
log.Debug().Msgf("SWITCHING: %s-%s", s, nsCleanser.ReplaceAllString(s, `$1`))
return nsCleanser.ReplaceAllString(s, `$1`) return nsCleanser.ReplaceAllString(s, `$1`)
} }
func (v *namespaceView) decorate(data resource.TableData) resource.TableData { func (v *namespaceView) decorate(data resource.TableData) resource.TableData {
if _, ok := data.Rows[resource.AllNamespaces]; !ok { if _, ok := data.Rows[resource.AllNamespaces]; !ok {
if v.app.conn().CanIAccess("", "namespaces", "namespace.v1", []string{"list"}) { if acc, err := v.app.conn().CanIAccess("", "namespaces", "namespace.v1", []string{"list"}); acc && err != nil {
data.Rows[resource.AllNamespace] = &resource.RowEvent{ data.Rows[resource.AllNamespace] = &resource.RowEvent{
Action: resource.Unchanged, Action: resource.Unchanged,
Fields: resource.Row{resource.AllNamespace, "Active", "0"}, Fields: resource.Row{resource.AllNamespace, "Active", "0"},

View File

@ -97,7 +97,7 @@ func (v *resourceView) init(ctx context.Context, ns string) {
} }
v.getTV().setColorer(colorer) v.getTV().setColorer(colorer)
v.nsListAccess = v.app.conn().CanIAccess("", "namespaces", "namespace.v1", []string{"list"}) v.nsListAccess, _ = v.app.conn().CanIAccess("", "namespaces", "namespace.v1", []string{"list"})
if v.nsListAccess { if v.nsListAccess {
nn, err := k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces) nn, err := k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces)
if err != nil { if err != nil {

View File

@ -62,17 +62,20 @@ func NewInformer(client k8s.Connection, ns string) *Informer {
log.Debug().Msgf(">> Starting Informer") log.Debug().Msgf(">> Starting Informer")
i := Informer{client: client, informers: map[string]StoreInformer{}} i := Informer{client: client, informers: map[string]StoreInformer{}}
nsAccess := i.client.CanIAccess("", "", "namespaces", []string{"list", "watch"}) _, err := client.CanIAccess("", "", "namespaces", []string{"list", "watch"})
ns, err := client.Config().CurrentNamespaceName() if err != nil && ns == AllNamespaces {
// User did not lock NS. Check all ns access if not bail log.Panic().Msg("Unauthorized: All namespaces. Missing verbs ['list', 'watch']. Please specify a namespace or correct RBAC")
if err != nil && !nsAccess {
log.Panic().Msg("Unauthorized: Unable to list ALL namespaces. Missing verbs ['list', 'watch']. Please specify a namespace or correct RBAC")
} }
// Namespace is locked in. check if user has auth for this ns access. // Namespace is locked in. check if user has auth for this ns access.
if ns != AllNamespaces && !nsAccess { if ns != AllNamespaces {
if !i.client.CanIAccess("", ns, "namespaces", []string{"get", "watch"}) { acc, err := client.CanIAccess("", ns, "namespaces", []string{"get", "watch"})
log.Panic().Msgf("Unauthorized: Access to namespace %q is missing verbs ['get', 'watch']", ns) if err != nil {
log.Panic().Err(err).Msgf("Failed access %s", ns)
}
if !acc {
user, _ := client.Config().CurrentUserName()
log.Panic().Msgf("Unauthorized: %s access to namespace %q is missing verbs ['get', 'watch']", user, ns)
} }
i.init(ns) i.init(ns)
} else { } else {
@ -90,7 +93,7 @@ func (i *Informer) init(ns string) {
ContainerIndex: NewContainer(po), ContainerIndex: NewContainer(po),
} }
if i.client.CanIAccess("", "", "nodes", []string{"list", "watch"}) { if acc, err := i.client.CanIAccess("", "", "nodes", []string{"list", "watch"}); acc && err != nil {
i.informers[NodeIndex] = NewNode(i.client) i.informers[NodeIndex] = NewNode(i.client)
} }
@ -98,7 +101,7 @@ func (i *Informer) init(ns string) {
return return
} }
if i.client.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"}) { if acc, err := i.client.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"}); acc && err != nil {
i.informers[NodeMXIndex] = NewNodeMetrics(i.client) i.informers[NodeMXIndex] = NewNodeMetrics(i.client)
i.informers[PodMXIndex] = NewPodMetrics(i.client, ns) i.informers[PodMXIndex] = NewPodMetrics(i.client, ns)
} }

View File

@ -19,9 +19,9 @@ func TestInformerInitWithNS(t *testing.T) {
cmo := NewMockConnection() cmo := NewMockConnection()
m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
m.When(cmo.HasMetrics()).ThenReturn(true) m.When(cmo.HasMetrics()).ThenReturn(true)
m.When(cmo.CanIAccess("", "", "namespaces", []string{"list", "watch"})).ThenReturn(false) m.When(cmo.CanIAccess("", "", "namespaces", []string{"list", "watch"})).ThenReturn(false, nil)
m.When(cmo.CanIAccess("", ns, "namespaces", []string{"get", "watch"})).ThenReturn(true) m.When(cmo.CanIAccess("", ns, "namespaces", []string{"get", "watch"})).ThenReturn(true, nil)
m.When(cmo.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"})).ThenReturn(true) m.When(cmo.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"})).ThenReturn(true, nil)
i := NewInformer(cmo, ns) i := NewInformer(cmo, ns)
o, err := i.List(PodIndex, "fred", metav1.ListOptions{}) o, err := i.List(PodIndex, "fred", metav1.ListOptions{})

View File

@ -24,19 +24,23 @@ func NewMockConnection() *MockConnection {
return &MockConnection{fail: pegomock.GlobalFailHandler} return &MockConnection{fail: pegomock.GlobalFailHandler}
} }
func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) bool { func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) (bool, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().") panic("mock must not be nil. Use myMock := NewMockConnection().")
} }
params := []pegomock.Param{_param0, _param1, _param2, _param3} params := []pegomock.Param{_param0, _param1, _param2, _param3}
result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 bool var ret0 bool
var ret1 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(bool) ret0 = result[0].(bool)
} }
if result[1] != nil {
ret1 = result[1].(error)
}
} }
return ret0 return ret0, ret1
} }
func (mock *MockConnection) Config() *k8s.Config { func (mock *MockConnection) Config() *k8s.Config {