RBAC: Add permission registry (#91247)

* RBAC: Permission registry

* Populate permission registry

* Wire

* conflic_user_cmd

* Update pkg/services/accesscontrol/permreg/permreg_test.go

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* PR feedback

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Remove ToDo, tackle in subsequent PR

---------

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
pull/91466/head
Gabriel MABILLE 11 months ago committed by GitHub
parent 391284bb33
commit 8988e04044
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      pkg/api/folder_bench_test.go
  2. 3
      pkg/cmd/grafana-cli/commands/conflict_user_command.go
  3. 2
      pkg/server/wire.go
  4. 19
      pkg/services/accesscontrol/acimpl/service.go
  5. 3
      pkg/services/accesscontrol/acimpl/service_test.go
  6. 183
      pkg/services/accesscontrol/permreg/permreg.go
  7. 246
      pkg/services/accesscontrol/permreg/permreg_test.go
  8. 22
      pkg/services/accesscontrol/permreg/test/testreg.go
  9. 2
      pkg/services/serviceaccounts/extsvcaccounts/service_test.go

@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
@ -463,7 +464,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
actionSets := resourcepermissions.NewActionSetService(features)
acSvc := acimpl.ProvideOSSService(
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db.DB(),
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db.DB(), permreg.ProvidePermissionRegistry(),
)
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(

@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
@ -90,7 +91,7 @@ func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx
if err != nil {
return nil, fmt.Errorf("%v: %w", "failed to initialize tracer service", err)
}
acService, err := acimpl.ProvideService(cfg, replstore, routing, nil, nil, nil, features, tracer, zanzana.NewNoopClient())
acService, err := acimpl.ProvideService(cfg, replstore, routing, nil, nil, nil, features, tracer, zanzana.NewNoopClient(), permreg.ProvidePermissionRegistry())
if err != nil {
return nil, fmt.Errorf("%v: %w", "failed to get access control", err)
}

@ -39,6 +39,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
@ -345,6 +346,7 @@ var wireBasicSet = wire.NewSet(
resourcepermissions.NewActionSetService,
wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)),
wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)),
permreg.ProvidePermissionRegistry,
acimpl.ProvideAccessControl,
navtreeimpl.ProvideService,
wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)),

@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -50,9 +51,9 @@ var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.E
func ProvideService(
cfg *setting.Cfg, db db.ReplDB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
accessControl accesscontrol.AccessControl, actionResolver accesscontrol.ActionResolver,
features featuremgmt.FeatureToggles, tracer tracing.Tracer, zclient zanzana.Client,
features featuremgmt.FeatureToggles, tracer tracing.Tracer, zclient zanzana.Client, permRegistry permreg.PermissionRegistry,
) (*Service, error) {
service := ProvideOSSService(cfg, database.ProvideService(db), actionResolver, cache, features, tracer, zclient, db.DB())
service := ProvideOSSService(cfg, database.ProvideService(db), actionResolver, cache, features, tracer, zclient, db.DB(), permRegistry)
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
@ -73,7 +74,7 @@ func ProvideService(
func ProvideOSSService(
cfg *setting.Cfg, store accesscontrol.Store, actionResolver accesscontrol.ActionResolver,
cache *localcache.CacheService, features featuremgmt.FeatureToggles, tracer tracing.Tracer,
zclient zanzana.Client, db db.DB,
zclient zanzana.Client, db db.DB, permRegistry permreg.PermissionRegistry,
) *Service {
s := &Service{
actionResolver: actionResolver,
@ -85,6 +86,7 @@ func ProvideOSSService(
store: store,
tracer: tracer,
sync: migrator.NewZanzanaSynchroniser(zclient, db),
permRegistry: permRegistry,
}
return s
@ -102,6 +104,7 @@ type Service struct {
store accesscontrol.Store
tracer tracing.Tracer
sync *migrator.ZanzanaSynchroniser
permRegistry permreg.PermissionRegistry
}
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
@ -406,6 +409,10 @@ func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistrat
return err
}
for i := range r.Role.Permissions {
s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope)
}
s.registrations.Append(r)
}
@ -458,6 +465,12 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
return err
}
for i := range r.Role.Permissions {
// Register plugin actions and their possible scopes for permission validation
s.permRegistry.RegisterPluginScope(r.Role.Permissions[i].Scope)
s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope)
}
s.log.Debug("Registering plugin role", "role", r.Role.Name)
s.registrations.Append(r)
}

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
@ -42,6 +43,7 @@ func setupTestEnv(t testing.TB) *Service {
roles: accesscontrol.BuildBasicRoleDefinitions(),
tracer: tracing.InitializeTracerForTest(),
store: database.ProvideService(db.InitTestReplDB(t)),
permRegistry: permreg.ProvidePermissionRegistry(),
}
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
return ac
@ -71,6 +73,7 @@ func TestUsageMetrics(t *testing.T) {
tracing.InitializeTracerForTest(),
nil,
nil,
permreg.ProvidePermissionRegistry(),
)
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
})

@ -0,0 +1,183 @@
package permreg
import (
"strings"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/infra/log"
)
var (
// ErrInvalidScope is returned when the scope is not valid for the action
ErrInvalidScopeTplt = "invalid scope: {{.Public.Scope}}, for action: {{.Public.Action}}, expected prefixes are {{.Public.ValidScopesFormat}}"
ErrBaseInvalidScope = errutil.BadRequest("permreg.invalid-scope").MustTemplate(ErrInvalidScopeTplt, errutil.WithPublic(ErrInvalidScopeTplt))
ErrUnknownActionTplt = "unknown action: {{.Public.Action}}, was not found in the list of valid actions"
ErrBaseUnknownAction = errutil.BadRequest("permreg.unknown-action").MustTemplate(ErrUnknownActionTplt, errutil.WithPublic(ErrUnknownActionTplt))
)
func ErrInvalidScope(scope string, action string, validScopePrefixes PrefixSet) error {
if len(validScopePrefixes) == 0 {
return ErrBaseInvalidScope.Build(errutil.TemplateData{Public: map[string]any{"Scope": scope, "Action": action, "ValidScopesFormat": "[none]"}})
}
formats := generateValidScopeFormats(validScopePrefixes)
return ErrBaseInvalidScope.Build(errutil.TemplateData{Public: map[string]any{"Scope": scope, "Action": action, "ValidScopesFormat": formats}})
}
func ErrUnknownAction(action string) error {
return ErrBaseUnknownAction.Build(errutil.TemplateData{Public: map[string]any{"Action": action}})
}
func generateValidScopeFormats(acceptedScopePrefixes PrefixSet) []string {
if len(acceptedScopePrefixes) == 0 {
return []string{}
}
acceptedPrefixesList := make([]string, 0, 10)
acceptedPrefixesList = append(acceptedPrefixesList, "*")
for prefix := range acceptedScopePrefixes {
parts := strings.Split(prefix, ":")
// If the prefix has an attribute part add the intermediate format kind:*
if len(parts) > 2 {
acceptedPrefixesList = append(acceptedPrefixesList, parts[0]+":*")
}
// Add the most specific format kind:attribute:*
acceptedPrefixesList = append(acceptedPrefixesList, prefix+"*")
}
return acceptedPrefixesList
}
type PermissionRegistry interface {
RegisterPluginScope(scope string)
RegisterPermission(action, scope string)
IsPermissionValid(action, scope string) error
GetScopePrefixes(action string) (PrefixSet, bool)
}
type PrefixSet map[string]bool
var _ PermissionRegistry = &permissionRegistry{}
type permissionRegistry struct {
actionScopePrefixes map[string]PrefixSet // TODO use thread safe map
kindScopePrefix map[string]string
logger log.Logger
}
func ProvidePermissionRegistry() PermissionRegistry {
return newPermissionRegistry()
}
func newPermissionRegistry() *permissionRegistry {
// defaultKindScopes maps the most specific accepted scope prefix for a given kind (folders, dashboards, etc)
defaultKindScopes := map[string]string{
"teams": "teams:id:",
"users": "users:id:",
"datasources": "datasources:uid:",
"dashboards": "dashboards:uid:",
"folders": "folders:uid:",
"annotations": "annotations:type:",
"apikeys": "apikeys:id:",
"orgs": "orgs:id:",
"plugins": "plugins:id:",
"provisioners": "provisioners:",
"reports": "reports:id:",
"permissions": "permissions:type:",
"serviceaccounts": "serviceaccounts:id:",
"settings": "settings:",
"global.users": "global.users:id:",
"roles": "roles:uid:",
"services": "services:",
}
return &permissionRegistry{
actionScopePrefixes: make(map[string]PrefixSet, 200),
kindScopePrefix: defaultKindScopes,
logger: log.New("accesscontrol.permreg"),
}
}
func (pr *permissionRegistry) RegisterPluginScope(scope string) {
if scope == "" {
return
}
scopeParts := strings.Split(scope, ":")
// If the scope is already registered, return
if _, found := pr.kindScopePrefix[scopeParts[0]]; found {
return
}
// If the scope contains an attribute part, register the kind and attribute
if len(scopeParts) > 2 {
kind, attr := scopeParts[0], scopeParts[1]
pr.kindScopePrefix[kind] = kind + ":" + attr + ":"
pr.logger.Debug("registered scope prefix", "kind", kind, "scope_prefix", kind+":"+attr+":")
return
}
pr.logger.Debug("registered scope prefix", "kind", scopeParts[0], "scope_prefix", scopeParts[0]+":")
pr.kindScopePrefix[scopeParts[0]] = scopeParts[0] + ":"
}
func (pr *permissionRegistry) RegisterPermission(action, scope string) {
if _, ok := pr.actionScopePrefixes[action]; !ok {
pr.actionScopePrefixes[action] = PrefixSet{}
}
if scope == "" {
// scopeless action
return
}
kind := strings.Split(scope, ":")[0]
scopePrefix, ok := pr.kindScopePrefix[kind]
if !ok {
pr.logger.Warn("unknown scope prefix", "scope", scope)
return
}
// Add a new entry in case the scope is not empty
pr.actionScopePrefixes[action][scopePrefix] = true
}
func (pr *permissionRegistry) IsPermissionValid(action, scope string) error {
validScopePrefixes, ok := pr.actionScopePrefixes[action]
if !ok {
return ErrUnknownAction(action)
}
if ok && len(validScopePrefixes) == 0 {
// Expecting action without any scope
if scope != "" {
return ErrInvalidScope(scope, action, nil)
}
return nil
}
if !isScopeValid(scope, validScopePrefixes) {
return ErrInvalidScope(scope, action, validScopePrefixes)
}
return nil
}
func isScopeValid(scope string, validScopePrefixes PrefixSet) bool {
// Super wildcard scope
if scope == "*" {
return true
}
for scopePrefix := range validScopePrefixes {
// Correct scope prefix
if strings.HasPrefix(scope, scopePrefix) {
return true
}
// Scope is wildcard of the correct prefix
if strings.HasSuffix(scope, ":*") && strings.HasPrefix(scopePrefix, scope[:len(scope)-2]) {
return true
}
}
return false
}
func (pr *permissionRegistry) GetScopePrefixes(action string) (PrefixSet, bool) {
set, ok := pr.actionScopePrefixes[action]
return set, ok
}

@ -0,0 +1,246 @@
package permreg
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_permissionRegistry_RegisterPluginScope(t *testing.T) {
tests := []struct {
scope string
wantKind string
wantScope string
}{
{
scope: "folders:uid:AABBCC",
wantKind: "folders",
wantScope: "folders:uid:",
},
{
scope: "plugins:id:test-app",
wantKind: "plugins",
wantScope: "plugins:id:",
},
{
scope: "resource:uid:res",
wantKind: "resource",
wantScope: "resource:uid:",
},
{
scope: "resource:*",
wantKind: "resource",
wantScope: "resource:",
},
}
for _, tt := range tests {
t.Run(tt.scope, func(t *testing.T) {
pr := newPermissionRegistry()
pr.RegisterPluginScope(tt.scope)
got, ok := pr.kindScopePrefix[tt.wantKind]
require.True(t, ok)
require.Equal(t, tt.wantScope, got)
})
}
}
func Test_permissionRegistry_RegisterPermission(t *testing.T) {
tests := []struct {
name string
action string
scope string
wantKind string
wantPrefixSet PrefixSet
wantSkip bool
}{
{
name: "register folders read",
action: "folders:read",
scope: "folders:*",
wantKind: "folders",
wantPrefixSet: PrefixSet{"folders:uid:": true},
},
{
name: "register app plugin settings read",
action: "test-app.settings:read",
wantKind: "settings",
wantPrefixSet: PrefixSet{},
},
{
name: "register an action on an unknown kind",
action: "unknown:action",
scope: "unknown:uid:*",
wantPrefixSet: PrefixSet{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pr := newPermissionRegistry()
pr.RegisterPermission(tt.action, tt.scope)
got, ok := pr.actionScopePrefixes[tt.action]
require.True(t, ok)
for k, v := range got {
require.Equal(t, v, tt.wantPrefixSet[k])
}
})
}
}
func Test_permissionRegistry_IsPermissionValid(t *testing.T) {
pr := newPermissionRegistry()
pr.RegisterPermission("folders:read", "folders:uid:")
pr.RegisterPermission("test-app.settings:read", "")
tests := []struct {
name string
action string
scope string
wantErr bool
}{
{
name: "valid folders read",
action: "folders:read",
scope: "folders:uid:AABBCC",
wantErr: false,
},
{
name: "valid folders read with wildcard",
action: "folders:read",
scope: "folders:uid:*",
wantErr: false,
},
{
name: "valid folders read with kind level wildcard",
action: "folders:read",
scope: "folders:*",
wantErr: false,
},
{
name: "valid folders read with super wildcard",
action: "folders:read",
scope: "*",
wantErr: false,
},
{
name: "invalid folders read with wrong kind",
action: "folders:read",
scope: "unknown:uid:AABBCC",
wantErr: true,
},
{
name: "invalid folders read with wrong attribute",
action: "folders:read",
scope: "folders:id:3",
wantErr: true,
},
{
name: "valid app plugin settings read",
action: "test-app.settings:read",
scope: "",
wantErr: false,
},
{
name: "app plugin settings read with a scope",
action: "test-app.settings:read",
scope: "folders:uid:*",
wantErr: true,
},
{
name: "unknown action",
action: "unknown:write",
scope: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := pr.IsPermissionValid(tt.action, tt.scope)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func Test_permissionRegistry_GetScopePrefixes(t *testing.T) {
pr := newPermissionRegistry()
pr.RegisterPermission("folders:read", "folders:uid:")
pr.RegisterPermission("test-app.settings:read", "")
tests := []struct {
name string
action string
want PrefixSet
shouldExist bool
}{
{
name: "get folders read scope prefixes",
action: "folders:read",
want: PrefixSet{"folders:uid:": true},
shouldExist: true,
},
{
name: "get app plugin settings read scope prefixes",
action: "test-app.settings:read",
want: PrefixSet{},
shouldExist: true,
},
{
name: "get unknown action scope prefixes",
action: "unknown:write",
want: PrefixSet{},
shouldExist: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := pr.GetScopePrefixes(tt.action)
if !tt.shouldExist {
require.False(t, got1)
return
}
require.True(t, got1)
require.Len(t, tt.want, len(got))
for k, v := range got {
require.Equal(t, v, tt.want[k])
}
})
}
}
func Test_generateValidScopeFormats(t *testing.T) {
tests := []struct {
name string
prefixSet PrefixSet
want []string
}{
{
name: "empty prefix set",
prefixSet: PrefixSet{},
want: []string{},
},
{
name: "short prefix",
prefixSet: PrefixSet{"folders:": true},
want: []string{"*", "folders:*"},
},
{
name: "single prefix",
prefixSet: PrefixSet{"folders:uid:": true},
want: []string{"*", "folders:*", "folders:uid:*"},
},
{
name: "multiple prefixes",
prefixSet: PrefixSet{"folders:uid:": true, "dashboards:uid:": true},
want: []string{"*", "folders:*", "folders:uid:*", "dashboards:*", "dashboards:uid:*"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generateValidScopeFormats(tt.prefixSet)
require.ElementsMatch(t, tt.want, got)
})
}
}

@ -0,0 +1,22 @@
package test
import "github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
func ProvidePermissionRegistry() permreg.PermissionRegistry {
permReg := permreg.ProvidePermissionRegistry()
// Test core permissions
permReg.RegisterPermission("datasources:read", "datasources:uid:")
permReg.RegisterPermission("dashboards:read", "dashboards:uid:")
permReg.RegisterPermission("dashboards:read", "folders:uid:")
permReg.RegisterPermission("folders:read", "folders:uid:")
// Test plugins permissions
permReg.RegisterPermission("plugins.app:access", "plugins:id:")
// App
permReg.RegisterPermission("test-app:read", "")
permReg.RegisterPermission("test-app.settings:read", "")
permReg.RegisterPermission("test-app.projects:read", "")
// App 1
permReg.RegisterPermission("test-app1.catalog:read", "")
permReg.RegisterPermission("test-app1.announcements:read", "")
return permReg
}

@ -15,6 +15,7 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/extsvcauth"
@ -48,6 +49,7 @@ func setupTestEnv(t *testing.T) *TestEnv {
acSvc: acimpl.ProvideOSSService(
cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{},
localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, nil,
permreg.ProvidePermissionRegistry(),
),
features: fmgt,
logger: logger,

Loading…
Cancel
Save