RBAC: Split non-empty scopes into kind, attribute and identifier fields for better search performance (#71933)

* add a feature toggle

* add the fields for attribute, kind and identifier to permission

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* set the new fields when new permissions are stored

* add migrations

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* remove comments

* Update pkg/services/accesscontrol/migrator/migrator.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* feedback: put column migrations behind the feature toggle, added an index, changed how wildcard scopes are split

* PR feedback: add a comment and revert an accidentally changed file

* PR feedback: handle the case with : in resource identifier

* switch from checking feature toggle through cfg to checking it through featuremgmt

* don't put the column migrations behind a feature toggle after all - this breaks permission queries from db

---------

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Ieva 2023-07-21 15:23:01 +01:00 committed by GitHub
parent 59eb2f68b7
commit cfa1a2c55f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 179 additions and 37 deletions

View File

@ -67,6 +67,7 @@ Some features are enabled by default. You can disable these feature by setting t
| `enableDatagridEditing` | Enables the edit functionality in the datagrid panel | | `enableDatagridEditing` | Enables the edit functionality in the datagrid panel |
| `dataSourcePageHeader` | Apply new pageHeader UI in data source edit page | | `dataSourcePageHeader` | Apply new pageHeader UI in data source edit page |
| `sqlDatasourceDatabaseSelection` | Enables previous SQL data source dataset dropdown behavior | | `sqlDatasourceDatabaseSelection` | Enables previous SQL data source dataset dropdown behavior |
| `splitScopes` | Support faster dashboard and folder search by splitting permission scopes into parts |
## Experimental feature toggles ## Experimental feature toggles

View File

@ -114,4 +114,5 @@ export interface FeatureToggles {
disableTraceQLStreaming?: boolean; disableTraceQLStreaming?: boolean;
grafanaAPIServer?: boolean; grafanaAPIServer?: boolean;
featureToggleAdminPage?: boolean; featureToggleAdminPage?: boolean;
splitScopes?: boolean;
} }

View File

@ -425,10 +425,10 @@ func setupServer(b testing.TB, sc benchScenario, features *featuremgmt.FeatureMa
folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sc.cfg, dashStore, folderStore, sc.db, features) folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sc.cfg, dashStore, folderStore, sc.db, features)
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
sc.cfg, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc) features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc)
require.NoError(b, err) require.NoError(b, err)
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
sc.cfg, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc) features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc)
require.NoError(b, err) require.NoError(b, err)
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(

View File

@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"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/pluginutils" "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
@ -31,15 +32,25 @@ const (
cacheTTL = 10 * time.Second cacheTTL = 10 * time.Second
) )
func ProvideService(cfg *setting.Cfg, store db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService, func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
accessControl accesscontrol.AccessControl, features *featuremgmt.FeatureManager) (*Service, error) { accessControl accesscontrol.AccessControl, features *featuremgmt.FeatureManager) (*Service, error) {
service := ProvideOSSService(cfg, database.ProvideService(store), cache, features) service := ProvideOSSService(cfg, database.ProvideService(db), cache, features)
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 {
return nil, err return nil, err
} }
if cfg.IsFeatureToggleEnabled(featuremgmt.FlagSplitScopes) {
// Migrating scopes that haven't been split yet to have kind, attribute and identifier in the DB
// This will be removed once we've:
// 1) removed the feature toggle and
// 2) have released enough versions not to support a version without split scopes
if err := migrator.MigrateScopeSplit(db, service.log); err != nil {
return nil, err
}
}
return service, nil return service, nil
} }

View File

@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
@ -57,15 +56,12 @@ func TestUsageMetrics(t *testing.T) {
cfg := setting.NewCfg() cfg := setting.NewCfg()
cfg.RBACEnabled = tt.enabled cfg.RBACEnabled = tt.enabled
s, errInitAc := ProvideService( s := ProvideOSSService(
cfg, cfg,
db.InitTestDB(t), database.ProvideService(db.InitTestDB(t)),
routing.NewRouteRegister(),
localcache.ProvideService(), localcache.ProvideService(),
actest.FakeAccessControl{},
featuremgmt.WithFeatures(), featuremgmt.WithFeatures(),
) )
require.NoError(t, errInitAc)
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"])
}) })
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/quota/quotatest"
@ -314,7 +315,7 @@ func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, user.Service, te
cfg.AutoAssignOrgRole = "Viewer" cfg.AutoAssignOrgRole = "Viewer"
cfg.AutoAssignOrgId = 1 cfg.AutoAssignOrgId = 1
acstore := ProvideService(sql) acstore := ProvideService(sql)
permissionStore := rs.NewStore(sql) permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures())
teamService := teamimpl.ProvideService(sql, cfg) teamService := teamimpl.ProvideService(sql, cfg)
orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil)) orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil))
require.NoError(t, err) require.NoError(t, err)

View File

@ -0,0 +1,44 @@
package migrator
import (
"context"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
func MigrateScopeSplit(db db.DB, log log.Logger) error {
t := time.Now()
var count = 0
err := db.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
var permissions []accesscontrol.Permission
err := sess.SQL("SELECT * FROM permission WHERE NOT scope = '' AND identifier = ''").Find(&permissions)
if err != nil {
return err
}
for i, p := range permissions {
count++
kind, attribute, identifier := p.SplitScope()
permissions[i].Kind = kind
permissions[i].Attribute = attribute
permissions[i].Identifier = identifier
_, err := sess.Exec("UPDATE permission SET kind = ?, attribute = ?, identifier = ? WHERE id = ?", permissions[i].Kind, permissions[i].Attribute, permissions[i].Identifier, permissions[i].ID)
if err != nil {
return err
}
}
return nil
})
log.Debug("Migrated permissions ", "count", count, "in", time.Since(t))
return err
}

View File

@ -182,6 +182,10 @@ type Permission struct {
Action string `json:"action"` Action string `json:"action"`
Scope string `json:"scope"` Scope string `json:"scope"`
Kind string `json:"-"`
Attribute string `json:"-"`
Identifier string `json:"-"`
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
} }
@ -193,6 +197,23 @@ func (p Permission) OSSPermission() Permission {
} }
} }
// SplitScope returns kind, attribute and Identifier
func (p Permission) SplitScope() (string, string, string) {
if p.Scope == "" {
return "", "", ""
}
fragments := strings.Split(p.Scope, ":")
switch l := len(fragments); l {
case 1: // Splitting a wildcard scope "*" -> kind: "*"; attribute: "*"; identifier: "*"
return fragments[0], fragments[0], fragments[0]
case 2: // Splitting a wildcard scope with specified kind "dashboards:*" -> kind: "dashboards"; attribute: "*"; identifier: "*"
return fragments[0], fragments[1], fragments[1]
default: // Splitting a scope with all fields specified "dashboards:uid:my_dash" -> kind: "dashboards"; attribute: "uid"; identifier: "my_dash"
return fragments[0], fragments[1], strings.Join(fragments[2:], ":")
}
}
type GetUserPermissionsQuery struct { type GetUserPermissionsQuery struct {
OrgID int64 OrgID int64
UserID int64 UserID int64

View File

@ -3,6 +3,7 @@ package accesscontrol
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -108,3 +109,32 @@ func TestSaveExternalServiceRoleCommand_Validate(t *testing.T) {
}) })
} }
} }
func TestPermission_ScopeSplit(t *testing.T) {
type testCase struct {
desc string
scope string
kind string
attribute string
identifier string
}
tests := []testCase{
{desc: "all fields should be empty for empty scope", scope: "", kind: "", attribute: "", identifier: ""},
{desc: "all fields should be set to * for wildcard", scope: "*", kind: "*", attribute: "*", identifier: "*"},
{desc: "kind should be specified and attribute and identifier should be * for a wildcard with kind prefix", scope: "dashboards:*", kind: "dashboards", attribute: "*", identifier: "*"},
{desc: "all fields should be set correctly", scope: "dashboards:uid:123", kind: "dashboards", attribute: "uid", identifier: "123"},
{desc: "can handle a case with : in the uid", scope: "datasources:uid:weird:name", kind: "datasources", attribute: "uid", identifier: "weird:name"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
p := Permission{Scope: tt.scope}
kind, attribute, identifier := p.SplitScope()
assert.Equal(t, tt.kind, kind)
assert.Equal(t, tt.attribute, attribute)
assert.Equal(t, tt.identifier, identifier)
})
}
}

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts"
@ -18,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/team/teamimpl"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
) )
type TeamPermissionsService struct { type TeamPermissionsService struct {
@ -40,7 +40,7 @@ var (
) )
func ProvideTeamPermissions( func ProvideTeamPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB,
ac accesscontrol.AccessControl, license licensing.Licensing, service accesscontrol.Service, ac accesscontrol.AccessControl, license licensing.Licensing, service accesscontrol.Service,
teamService team.Service, userService user.Service, teamService team.Service, userService user.Service,
) (*TeamPermissionsService, error) { ) (*TeamPermissionsService, error) {
@ -98,7 +98,7 @@ func ProvideTeamPermissions(
}, },
} }
srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql, teamService, userService) srv, err := resourcepermissions.New(options, features, router, license, ac, service, sql, teamService, userService)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -114,7 +114,7 @@ var DashboardEditActions = append(DashboardViewActions, []string{dashboards.Acti
var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...) var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...)
func ProvideDashboardPermissions( func ProvideDashboardPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl,
license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service,
teamService team.Service, userService user.Service, teamService team.Service, userService user.Service,
) (*DashboardPermissionsService, error) { ) (*DashboardPermissionsService, error) {
@ -178,7 +178,7 @@ func ProvideDashboardPermissions(
RoleGroup: "Dashboards", RoleGroup: "Dashboards",
} }
srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql, teamService, userService) srv, err := resourcepermissions.New(options, features, router, license, ac, service, sql, teamService, userService)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -201,7 +201,7 @@ var FolderEditActions = append(FolderViewActions, []string{
var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...) var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...)
func ProvideFolderPermissions( func ProvideFolderPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl,
license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service,
teamService team.Service, userService user.Service, teamService team.Service, userService user.Service,
) (*FolderPermissionsService, error) { ) (*FolderPermissionsService, error) {
@ -238,7 +238,7 @@ func ProvideFolderPermissions(
WriterRoleName: "Folder permission writer", WriterRoleName: "Folder permission writer",
RoleGroup: "Folders", RoleGroup: "Folders",
} }
srv, err := resourcepermissions.New(options, cfg, router, license, accesscontrol, service, sql, teamService, userService) srv, err := resourcepermissions.New(options, features, router, license, accesscontrol, service, sql, teamService, userService)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -301,7 +301,7 @@ type ServiceAccountPermissionsService struct {
} }
func ProvideServiceAccountPermissions( func ProvideServiceAccountPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl,
license licensing.Licensing, serviceAccountRetrieverService *retriever.Service, service accesscontrol.Service, license licensing.Licensing, serviceAccountRetrieverService *retriever.Service, service accesscontrol.Service,
teamService team.Service, userService user.Service, teamService team.Service, userService user.Service,
) (*ServiceAccountPermissionsService, error) { ) (*ServiceAccountPermissionsService, error) {
@ -330,7 +330,7 @@ func ProvideServiceAccountPermissions(
RoleGroup: "Service accounts", RoleGroup: "Service accounts",
} }
srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql, teamService, userService) srv, err := resourcepermissions.New(options, features, router, license, ac, service, sql, teamService, userService)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,11 +8,11 @@ import (
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
) )
type Store interface { type Store interface {
@ -52,7 +52,7 @@ type Store interface {
} }
func New( func New(
options Options, cfg *setting.Cfg, router routing.RouteRegister, license licensing.Licensing, options Options, features featuremgmt.FeatureToggles, router routing.RouteRegister, license licensing.Licensing,
ac accesscontrol.AccessControl, service accesscontrol.Service, sqlStore db.DB, ac accesscontrol.AccessControl, service accesscontrol.Service, sqlStore db.DB,
teamService team.Service, userService user.Service, teamService team.Service, userService user.Service,
) (*Service, error) { ) (*Service, error) {
@ -77,8 +77,7 @@ func New(
s := &Service{ s := &Service{
ac: ac, ac: ac,
cfg: cfg, store: NewStore(sqlStore, features),
store: NewStore(sqlStore),
options: options, options: options,
license: license, license: license,
permissions: permissions, permissions: permissions,
@ -102,7 +101,6 @@ func New(
// Service is used to create access control sub system including api / and service for managed resource permission // Service is used to create access control sub system including api / and service for managed resource permission
type Service struct { type Service struct {
cfg *setting.Cfg
ac accesscontrol.AccessControl ac accesscontrol.AccessControl
service accesscontrol.Service service accesscontrol.Service
store Store store Store

View File

@ -12,6 +12,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/actest" "github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/licensing/licensingtest"
"github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/quota/quotatest"
@ -244,7 +245,7 @@ func setupTestEnvironment(t *testing.T, ops Options) (*Service, *sqlstore.SQLSto
ac := acimpl.ProvideAccessControl(cfg) ac := acimpl.ProvideAccessControl(cfg)
acService := &actest.FakeService{} acService := &actest.FakeService{}
service, err := New( service, err := New(
ops, cfg, routing.NewRouteRegister(), license, ops, featuremgmt.WithFeatures(), routing.NewRouteRegister(), license,
ac, acService, sql, teamSvc, userSvc, ac, acService, sql, teamSvc, userSvc,
) )
require.NoError(t, err) require.NoError(t, err)

View File

@ -8,18 +8,20 @@ import (
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func NewStore(sql db.DB) *store { func NewStore(sql db.DB, features featuremgmt.FeatureToggles) *store {
return &store{sql} return &store{sql, features}
} }
type store struct { type store struct {
sql db.DB sql db.DB
features featuremgmt.FeatureToggles
} }
type flatResourcePermission struct { type flatResourcePermission struct {
@ -648,6 +650,9 @@ func (s *store) createPermissions(sess *db.Session, roleID int64, resource, reso
p.RoleID = roleID p.RoleID = roleID
p.Created = time.Now() p.Created = time.Now()
p.Updated = time.Now() p.Updated = time.Now()
if s.features.IsEnabled(featuremgmt.FlagSplitScopes) {
p.Kind, p.Attribute, p.Identifier = p.SplitScope()
}
permissions = append(permissions, p) permissions = append(permissions, p)
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/quota/quotatest"
@ -520,7 +521,7 @@ func seedResourcePermissions(t *testing.T, store *store, sql *sqlstore.SQLStore,
func setupTestEnv(t testing.TB) (*store, *sqlstore.SQLStore) { func setupTestEnv(t testing.TB) (*store, *sqlstore.SQLStore) {
sql := db.InitTestDB(t) sql := db.InitTestDB(t)
return NewStore(sql), sql return NewStore(sql, featuremgmt.WithFeatures()), sql
} }
func TestStore_IsInherited(t *testing.T) { func TestStore_IsInherited(t *testing.T) {

View File

@ -657,5 +657,13 @@ var (
Owner: grafanaOperatorExperienceSquad, Owner: grafanaOperatorExperienceSquad,
RequiresRestart: true, RequiresRestart: true,
}, },
{
Name: "splitScopes",
Description: "Support faster dashboard and folder search by splitting permission scopes into parts",
Stage: FeatureStagePublicPreview,
FrontendOnly: false,
Owner: grafanaAuthnzSquad,
RequiresRestart: true,
},
} }
) )

View File

@ -95,3 +95,4 @@ mlExpressions,experimental,@grafana/alerting-squad,false,false,false,false
disableTraceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true disableTraceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true
grafanaAPIServer,experimental,@grafana/grafana-app-platform-squad,false,false,false,false grafanaAPIServer,experimental,@grafana/grafana-app-platform-squad,false,false,false,false
featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,false,true,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,false,true,false
splitScopes,preview,@grafana/grafana-authnz-team,false,false,true,false

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
95 disableTraceQLStreaming experimental @grafana/observability-traces-and-profiling false false false true
96 grafanaAPIServer experimental @grafana/grafana-app-platform-squad false false false false
97 featureToggleAdminPage experimental @grafana/grafana-operator-experience-squad false false true false
98 splitScopes preview @grafana/grafana-authnz-team false false true false

View File

@ -390,4 +390,8 @@ const (
// FlagFeatureToggleAdminPage // FlagFeatureToggleAdminPage
// Enable admin page for managing feature toggles from the Grafana front-end // Enable admin page for managing feature toggles from the Grafana front-end
FlagFeatureToggleAdminPage = "featureToggleAdminPage" FlagFeatureToggleAdminPage = "featureToggleAdminPage"
// FlagSplitScopes
// Support faster dashboard and folder search by splitting permission scopes into parts
FlagSplitScopes = "splitScopes"
) )

View File

@ -629,10 +629,10 @@ func setupAccessControlGuardianTest(t *testing.T, uid string,
require.NoError(t, err) require.NoError(t, err)
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
cfg, routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, foldertest.NewFakeService(), ac, teamSvc, userSvc) featuremgmt.WithFeatures(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, foldertest.NewFakeService(), ac, teamSvc, userSvc)
require.NoError(t, err) require.NoError(t, err)
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
cfg, routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, foldertest.NewFakeService(), ac, teamSvc, userSvc) featuremgmt.WithFeatures(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, foldertest.NewFakeService(), ac, teamSvc, userSvc)
require.NoError(t, err) require.NoError(t, err)
g, err := NewAccessControlDashboardGuardian(context.Background(), cfg, dash.ID, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc) g, err := NewAccessControlDashboardGuardian(context.Background(), cfg, dash.ID, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc)

View File

@ -170,4 +170,20 @@ func AddMigration(mg *migrator.Migrator) {
mg.AddMigration("add column hidden to role table", migrator.NewAddColumnMigration(roleV1, &migrator.Column{ mg.AddMigration("add column hidden to role table", migrator.NewAddColumnMigration(roleV1, &migrator.Column{
Name: "hidden", Type: migrator.DB_Bool, Nullable: false, Default: "0", Name: "hidden", Type: migrator.DB_Bool, Nullable: false, Default: "0",
})) }))
mg.AddMigration("permission kind migration", migrator.NewAddColumnMigration(permissionV1, &migrator.Column{
Name: "kind", Type: migrator.DB_NVarchar, Length: 40, Default: "''",
}))
mg.AddMigration("permission attribute migration", migrator.NewAddColumnMigration(permissionV1, &migrator.Column{
Name: "attribute", Type: migrator.DB_NVarchar, Length: 40, Default: "''",
}))
mg.AddMigration("permission identifier migration", migrator.NewAddColumnMigration(permissionV1, &migrator.Column{
Name: "identifier", Type: migrator.DB_NVarchar, Length: 40, Default: "''",
}))
mg.AddMigration("add permission identifier index", migrator.NewAddIndexMigration(permissionV1, &migrator.Index{
Cols: []string{"identifier"},
}))
} }

View File

@ -111,7 +111,7 @@ func TestBacktesting(t *testing.T) {
}) })
// access control permissions store // access control permissions store
permissionsStore := resourcepermissions.NewStore(env.SQLStore) permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
_, err := permissionsStore.SetUserResourcePermission(context.Background(), _, err := permissionsStore.SetUserResourcePermission(context.Background(),
accesscontrol.GlobalOrgID, accesscontrol.GlobalOrgID,
accesscontrol.User{ID: testUserId}, accesscontrol.User{ID: testUserId},

View File

@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@ -663,7 +664,7 @@ func TestIntegrationPrometheusRulesPermissions(t *testing.T) {
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password") apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// access control permissions store // access control permissions store
permissionsStore := resourcepermissions.NewStore(store) permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
// Create the namespace we'll save our alerts to. // Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1") apiClient.CreateFolder(t, "folder1", "folder1")

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" datasourceService "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@ -41,7 +42,7 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
}) })
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
permissionsStore := resourcepermissions.NewStore(store) permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
// Create a user to make authenticated requests // Create a user to make authenticated requests
userID := createUser(t, store, user.CreateUserCommand{ userID := createUser(t, store, user.CreateUserCommand{
@ -868,7 +869,7 @@ func TestIntegrationRuleUpdate(t *testing.T) {
AppModeProduction: true, AppModeProduction: true,
}) })
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
permissionsStore := resourcepermissions.NewStore(store) permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
// Create a user to make authenticated requests // Create a user to make authenticated requests
userID := createUser(t, store, user.CreateUserCommand{ userID := createUser(t, store, user.CreateUserCommand{

View File

@ -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/resourcepermissions" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@ -267,7 +268,7 @@ func TestGrafanaRuleConfig(t *testing.T) {
}) })
// access control permissions store // access control permissions store
permissionsStore := resourcepermissions.NewStore(env.SQLStore) permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
_, err := permissionsStore.SetUserResourcePermission(context.Background(), _, err := permissionsStore.SetUserResourcePermission(context.Background(),
accesscontrol.GlobalOrgID, accesscontrol.GlobalOrgID,
accesscontrol.User{ID: testUserId}, accesscontrol.User{ID: testUserId},