ext jwt client: map k8s-style to rbac permissions (#106279)

* initial commit

* Proposal
Co-Authored-By: mohammad-hamid <mohammad.hamid@grafana.com>

* extend k8s-style mapper
- add tests

* address comments

* cleanup

* address comments

---------

Co-authored-by: Gabriel Mabille <gabriel.mabille@grafana.com>
This commit is contained in:
mohammad-hamid
2025-06-18 11:51:35 -04:00
committed by GitHub
parent 67f50478d9
commit 936dd05eac
8 changed files with 319 additions and 38 deletions
+69 -12
View File
@@ -6,6 +6,21 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
// Mapping maps a verb to a RBAC action and a resource name to a RBAC scope.
type Mapping interface {
// action returns the action for the given verb.
// If no action is found, it returns false.
Action(verb string) (string, bool)
// scope returns the scope for the given resource name.
Scope(name string) string
// prefix returns the scope prefix for the translation.
Prefix() string
// AllActions returns all the actions for the translation.
AllActions() []string
// HasFolderSupport returns true if the translation supports folders.
HasFolderSupport() bool
}
type translation struct {
resource string
attribute string
@@ -13,19 +28,47 @@ type translation struct {
folderSupport bool
}
func (t translation) action(verb string) (string, bool) {
func (t translation) Action(verb string) (string, bool) {
action, ok := t.verbMapping[verb]
return action, ok
}
func (t translation) scope(name string) string {
func (t translation) Scope(name string) string {
return t.resource + ":" + t.attribute + ":" + name
}
func (t translation) prefix() string {
func (t translation) Prefix() string {
return t.resource + ":" + t.attribute + ":"
}
func (t translation) AllActions() []string {
actions := make([]string, 0, len(t.verbMapping))
actionsMap := make(map[string]bool)
for _, action := range t.verbMapping {
if actionsMap[action] {
continue
}
actionsMap[action] = true
actions = append(actions, action)
}
return actions
}
func (t translation) HasFolderSupport() bool {
return t.folderSupport
}
// MapperRegistry is a registry of mappers that maps a group and resource to a translation.
type MapperRegistry interface {
// Get returns the permission mapper for the given group and resource.
// If no translation is found, it returns false.
Get(group, resource string) (Mapping, bool)
// GetAll returns all the translations for the given group
GetAll(group string) []Mapping
}
type mapper map[string]map[string]translation
func newResourceTranslation(resource string, attribute string, folderSupport bool) translation {
defaultMapping := func(r string) map[string]string {
return map[string]string{
@@ -50,10 +93,8 @@ func newResourceTranslation(resource string, attribute string, folderSupport boo
}
}
type mapper map[string]map[string]translation
func newMapper() mapper {
return map[string]map[string]translation{
func NewMapperRegistry() MapperRegistry {
mapper := mapper(map[string]map[string]translation{
"dashboard.grafana.app": {
"dashboards": newResourceTranslation("dashboards", "uid", true),
},
@@ -77,19 +118,35 @@ func newMapper() mapper {
folderSupport: false,
},
},
}
})
return mapper
}
func (m mapper) translation(group, resource string) (translation, bool) {
func (m mapper) Get(group, resource string) (Mapping, bool) {
resources, ok := m[group]
if !ok {
return translation{}, false
return nil, false
}
t, ok := resources[resource]
if !ok {
return translation{}, false
return nil, false
}
return t, true
return &t, true
}
func (m mapper) GetAll(group string) []Mapping {
resources, ok := m[group]
if !ok {
return nil
}
translations := make([]Mapping, 0, len(resources))
for _, t := range resources {
translations = append(translations, &t)
}
return translations
}
+10 -10
View File
@@ -45,7 +45,7 @@ type Service struct {
identityStore legacy.LegacyIdentityStore
settings Settings
mapper mapper
mapper MapperRegistry
logger log.Logger
tracer tracing.Tracer
@@ -93,7 +93,7 @@ func NewService(
logger: logger,
tracer: tracer,
metrics: newMetrics(reg),
mapper: newMapper(),
mapper: NewMapperRegistry(),
idCache: newCacheWrap[store.UserIdentifiers](cache, logger, tracer, longCacheTTL),
permCache: newCacheWrap[map[string]bool](cache, logger, tracer, settings.CacheTTL),
permDenialCache: newCacheWrap[bool](cache, logger, tracer, settings.CacheTTL),
@@ -334,13 +334,13 @@ func (s *Service) validateSubject(ctx context.Context, subject string) (string,
func (s *Service) validateAction(ctx context.Context, group, resource, verb string) (string, error) {
ctxLogger := s.logger.FromContext(ctx)
t, ok := s.mapper.translation(group, resource)
t, ok := s.mapper.Get(group, resource)
if !ok {
ctxLogger.Error("unsupport resource", "group", group, "resource", resource)
return "", status.Error(codes.NotFound, "unsupported resource")
}
action, ok := t.action(verb)
action, ok := t.Action(verb)
if !ok {
ctxLogger.Error("unsupport verb", "group", group, "resource", resource, "verb", verb)
return "", status.Error(codes.NotFound, "unsupported verb")
@@ -585,17 +585,17 @@ func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool,
return true, nil
}
t, ok := s.mapper.translation(req.Group, req.Resource)
t, ok := s.mapper.Get(req.Group, req.Resource)
if !ok {
ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource)
return false, status.Error(codes.NotFound, "unsupported resource")
}
if req.Name != "" && scopeMap[t.scope(req.Name)] {
if req.Name != "" && scopeMap[t.Scope(req.Name)] {
return true, nil
}
if !t.folderSupport {
if !t.HasFolderSupport() {
return false, nil
}
@@ -679,14 +679,14 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool,
defer span.End()
ctxLogger := s.logger.FromContext(ctx)
t, ok := s.mapper.translation(req.Group, req.Resource)
t, ok := s.mapper.Get(req.Group, req.Resource)
if !ok {
ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource)
return nil, status.Error(codes.NotFound, "unsupported resource")
}
var tree folderTree
if t.folderSupport {
if t.HasFolderSupport() {
var err error
tree, err = s.buildFolderTree(ctx, req.Namespace)
if err != nil {
@@ -699,7 +699,7 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool,
if strings.HasPrefix(req.Action, "folders:") {
res = buildFolderList(scopeMap, tree)
} else {
res = buildItemList(scopeMap, tree, t.prefix())
res = buildItemList(scopeMap, tree, t.Prefix())
}
span.SetAttributes(attribute.Int("num_folders", len(res.Folders)), attribute.Int("num_items", len(res.Items)))
+1 -1
View File
@@ -1519,7 +1519,7 @@ func setupService() *Service {
tracer := tracing.NewNoopTracerService()
return &Service{
logger: logger,
mapper: newMapper(),
mapper: NewMapperRegistry(),
tracer: tracer,
metrics: newMetrics(nil),
idCache: newCacheWrap[store.UserIdentifiers](cache, logger, tracer, longCacheTTL),