mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
391284bb33
commit
8988e04044
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||||
acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
"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/accesscontrol/resourcepermissions"
|
||||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
"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)
|
actionSets := resourcepermissions.NewActionSetService(features)
|
||||||
acSvc := acimpl.ProvideOSSService(
|
acSvc := acimpl.ProvideOSSService(
|
||||||
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
|
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(
|
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
"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/authz/zanzana"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||||
@ -90,7 +91,7 @@ func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to initialize tracer service", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to get access control", err)
|
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"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
"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/accesscontrol/resourcepermissions"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
|
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
|
||||||
@ -345,6 +346,7 @@ var wireBasicSet = wire.NewSet(
|
|||||||
resourcepermissions.NewActionSetService,
|
resourcepermissions.NewActionSetService,
|
||||||
wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)),
|
wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)),
|
||||||
wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)),
|
wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)),
|
||||||
|
permreg.ProvidePermissionRegistry,
|
||||||
acimpl.ProvideAccessControl,
|
acimpl.ProvideAccessControl,
|
||||||
navtreeimpl.ProvideService,
|
navtreeimpl.ProvideService,
|
||||||
wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)),
|
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/api"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
|
"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/accesscontrol/pluginutils"
|
||||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
@ -50,9 +51,9 @@ var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.E
|
|||||||
func ProvideService(
|
func ProvideService(
|
||||||
cfg *setting.Cfg, db db.ReplDB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
|
cfg *setting.Cfg, db db.ReplDB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
|
||||||
accessControl accesscontrol.AccessControl, actionResolver accesscontrol.ActionResolver,
|
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, 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()
|
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
|
||||||
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
|
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
|
||||||
@ -73,7 +74,7 @@ func ProvideService(
|
|||||||
func ProvideOSSService(
|
func ProvideOSSService(
|
||||||
cfg *setting.Cfg, store accesscontrol.Store, actionResolver accesscontrol.ActionResolver,
|
cfg *setting.Cfg, store accesscontrol.Store, actionResolver accesscontrol.ActionResolver,
|
||||||
cache *localcache.CacheService, features featuremgmt.FeatureToggles, tracer tracing.Tracer,
|
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 {
|
) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
actionResolver: actionResolver,
|
actionResolver: actionResolver,
|
||||||
@ -85,6 +86,7 @@ func ProvideOSSService(
|
|||||||
store: store,
|
store: store,
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
sync: migrator.NewZanzanaSynchroniser(zclient, db),
|
sync: migrator.NewZanzanaSynchroniser(zclient, db),
|
||||||
|
permRegistry: permRegistry,
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
@ -102,6 +104,7 @@ type Service struct {
|
|||||||
store accesscontrol.Store
|
store accesscontrol.Store
|
||||||
tracer tracing.Tracer
|
tracer tracing.Tracer
|
||||||
sync *migrator.ZanzanaSynchroniser
|
sync *migrator.ZanzanaSynchroniser
|
||||||
|
permRegistry permreg.PermissionRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
||||||
@ -406,6 +409,10 @@ func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistrat
|
|||||||
return err
|
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)
|
s.registrations.Append(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,6 +465,12 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
|
|||||||
return err
|
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.log.Debug("Registering plugin role", "role", r.Role.Name)
|
||||||
s.registrations.Append(r)
|
s.registrations.Append(r)
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
"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/accesscontrol/resourcepermissions"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
@ -42,6 +43,7 @@ func setupTestEnv(t testing.TB) *Service {
|
|||||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||||
tracer: tracing.InitializeTracerForTest(),
|
tracer: tracing.InitializeTracerForTest(),
|
||||||
store: database.ProvideService(db.InitTestReplDB(t)),
|
store: database.ProvideService(db.InitTestReplDB(t)),
|
||||||
|
permRegistry: permreg.ProvidePermissionRegistry(),
|
||||||
}
|
}
|
||||||
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
|
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
|
||||||
return ac
|
return ac
|
||||||
@ -71,6 +73,7 @@ func TestUsageMetrics(t *testing.T) {
|
|||||||
tracing.InitializeTracerForTest(),
|
tracing.InitializeTracerForTest(),
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
permreg.ProvidePermissionRegistry(),
|
||||||
)
|
)
|
||||||
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
||||||
})
|
})
|
||||||
|
183
pkg/services/accesscontrol/permreg/permreg.go
Normal file
183
pkg/services/accesscontrol/permreg/permreg.go
Normal file
@ -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
|
||||||
|
}
|
246
pkg/services/accesscontrol/permreg/permreg_test.go
Normal file
246
pkg/services/accesscontrol/permreg/permreg_test.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
22
pkg/services/accesscontrol/permreg/test/testreg.go
Normal file
22
pkg/services/accesscontrol/permreg/test/testreg.go
Normal file
@ -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"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
"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/accesscontrol/resourcepermissions"
|
||||||
"github.com/grafana/grafana/pkg/services/apikey"
|
"github.com/grafana/grafana/pkg/services/apikey"
|
||||||
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
||||||
@ -48,6 +49,7 @@ func setupTestEnv(t *testing.T) *TestEnv {
|
|||||||
acSvc: acimpl.ProvideOSSService(
|
acSvc: acimpl.ProvideOSSService(
|
||||||
cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{},
|
cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{},
|
||||||
localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, nil,
|
localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, nil,
|
||||||
|
permreg.ProvidePermissionRegistry(),
|
||||||
),
|
),
|
||||||
features: fmgt,
|
features: fmgt,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
Loading…
Reference in New Issue
Block a user