diff --git a/pkg/services/accesscontrol/database/database.go b/pkg/services/accesscontrol/database/database.go index 6766fa91ec5..a53674c9bd5 100644 --- a/pkg/services/accesscontrol/database/database.go +++ b/pkg/services/accesscontrol/database/database.go @@ -9,10 +9,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" ) -const ( - globalOrgID = 0 -) - func ProvideService(sql db.DB) *AccessControlStore { return &AccessControlStore{sql} } @@ -29,7 +25,7 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces return nil } - filter, params := userRolesFilter(query.OrgID, query.UserID, query.TeamIDs, query.Roles) + filter, params := accesscontrol.UserRolesFilter(query.OrgID, query.UserID, query.TeamIDs, query.Roles) q := ` SELECT @@ -59,57 +55,6 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces return result, err } -func userRolesFilter(orgID, userID int64, teamIDs []int64, roles []string) (string, []interface{}) { - var params []interface{} - builder := strings.Builder{} - - // This is an additional security. We should never have permissions granted to userID 0. - // Only allow real users to get user/team permissions (anonymous/apikeys) - if userID > 0 { - builder.WriteString(` - SELECT ur.role_id - FROM user_role AS ur - WHERE ur.user_id = ? - AND (ur.org_id = ? OR ur.org_id = ?) - `) - params = []interface{}{userID, orgID, globalOrgID} - } - - if len(teamIDs) > 0 { - if builder.Len() > 0 { - builder.WriteString("UNION") - } - builder.WriteString(` - SELECT tr.role_id FROM team_role as tr - WHERE tr.team_id IN(?` + strings.Repeat(", ?", len(teamIDs)-1) + `) - AND tr.org_id = ? - `) - for _, id := range teamIDs { - params = append(params, id) - } - params = append(params, orgID) - } - - if len(roles) != 0 { - if builder.Len() > 0 { - builder.WriteString("UNION") - } - - builder.WriteString(` - SELECT br.role_id FROM builtin_role AS br - WHERE br.role IN (?` + strings.Repeat(", ?", len(roles)-1) + `) - AND (br.org_id = ? OR br.org_id = ?) - `) - for _, role := range roles { - params = append(params, role) - } - - params = append(params, orgID, globalOrgID) - } - - return "INNER JOIN (" + builder.String() + ") as all_role ON role.id = all_role.role_id", params -} - func (s *AccessControlStore) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error { err := s.sql.WithDbSession(ctx, func(sess *db.Session) error { roleDeleteQuery := "DELETE FROM user_role WHERE user_id = ?" diff --git a/pkg/services/accesscontrol/filter.go b/pkg/services/accesscontrol/filter.go index 67ea04a03c2..2df980d2f96 100644 --- a/pkg/services/accesscontrol/filter.go +++ b/pkg/services/accesscontrol/filter.go @@ -125,3 +125,54 @@ func SetAcceptListForTest(list map[string]struct{}) func() { sqlIDAcceptList = original } } + +func UserRolesFilter(orgID, userID int64, teamIDs []int64, roles []string) (string, []interface{}) { + var params []interface{} + builder := strings.Builder{} + + // This is an additional security. We should never have permissions granted to userID 0. + // Only allow real users to get user/team permissions (anonymous/apikeys) + if userID > 0 { + builder.WriteString(` + SELECT ur.role_id + FROM user_role AS ur + WHERE ur.user_id = ? + AND (ur.org_id = ? OR ur.org_id = ?) + `) + params = []interface{}{userID, orgID, GlobalOrgID} + } + + if len(teamIDs) > 0 { + if builder.Len() > 0 { + builder.WriteString("UNION") + } + builder.WriteString(` + SELECT tr.role_id FROM team_role as tr + WHERE tr.team_id IN(?` + strings.Repeat(", ?", len(teamIDs)-1) + `) + AND tr.org_id = ? + `) + for _, id := range teamIDs { + params = append(params, id) + } + params = append(params, orgID) + } + + if len(roles) != 0 { + if builder.Len() > 0 { + builder.WriteString("UNION") + } + + builder.WriteString(` + SELECT br.role_id FROM builtin_role AS br + WHERE br.role IN (?` + strings.Repeat(", ?", len(roles)-1) + `) + AND (br.org_id = ? OR br.org_id = ?) + `) + for _, role := range roles { + params = append(params, role) + } + + params = append(params, orgID, GlobalOrgID) + } + + return "INNER JOIN (" + builder.String() + ") as all_role ON role.id = all_role.role_id", params +} diff --git a/pkg/services/annotations/annotationsimpl/xorm_store_test.go b/pkg/services/annotations/annotationsimpl/xorm_store_test.go index 56f566dbc4e..1b9a9a8073c 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store_test.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store_test.go @@ -5,7 +5,9 @@ import ( "fmt" "strings" "testing" + "time" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -407,13 +409,15 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { t.Skip("skipping integration test") } sql := db.InitTestDB(t) + var maximumTagsLength int64 = 60 repo := xormRepositoryImpl{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(sql, sql.Cfg), maximumTagsLength: maximumTagsLength} dashboardStore := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg)) testDashboard1 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + UserId: 1, + OrgId: 1, + IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 1", }), @@ -459,6 +463,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { UserID: 1, OrgID: 1, } + role := setupRBACRole(t, repo, user) type testStruct struct { description string @@ -519,6 +524,8 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { user.Permissions = map[int64]map[string][]string{1: tc.permissions} + setupRBACPermission(t, repo, role, user) + results, err := repo.Get(context.Background(), &annotations.ItemQuery{ OrgId: 1, SignedInUser: user, @@ -535,3 +542,61 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { }) } } + +func setupRBACRole(t *testing.T, repo xormRepositoryImpl, user *user.SignedInUser) *accesscontrol.Role { + t.Helper() + var role *accesscontrol.Role + err := repo.db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + role = &accesscontrol.Role{ + OrgID: user.OrgID, + UID: "test_role", + Name: "test:role", + Updated: time.Now(), + Created: time.Now(), + } + _, err := sess.Insert(role) + if err != nil { + return err + } + + _, err = sess.Insert(accesscontrol.UserRole{ + OrgID: role.OrgID, + RoleID: role.ID, + UserID: user.UserID, + Created: time.Now(), + }) + if err != nil { + return err + } + return nil + }) + + require.NoError(t, err) + return role +} + +func setupRBACPermission(t *testing.T, repo xormRepositoryImpl, role *accesscontrol.Role, user *user.SignedInUser) { + t.Helper() + err := repo.db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + if _, err := sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID); err != nil { + return err + } + + var acPermission []accesscontrol.Permission + for action, scopes := range user.Permissions[user.OrgID] { + for _, scope := range scopes { + acPermission = append(acPermission, accesscontrol.Permission{ + RoleID: role.ID, Action: action, Scope: scope, Created: time.Now(), Updated: time.Now(), + }) + } + } + + if _, err := sess.InsertMulti(&acPermission); err != nil { + return err + } + + return nil + }) + + require.NoError(t, err) +} diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 10e2fb84ef7..c087421e370 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -80,9 +80,9 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) { } type AccessControlDashboardPermissionFilter struct { - User *user.SignedInUser - dashboardActions []string + user *user.SignedInUser folderActions []string + dashboardActions []string } // NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the models.PermissionType and query type @@ -102,39 +102,80 @@ func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissi dashboardActions = append(dashboardActions, dashboards.ActionDashboardsWrite) } } - return AccessControlDashboardPermissionFilter{User: user, folderActions: folderActions, dashboardActions: dashboardActions} + return AccessControlDashboardPermissionFilter{user: user, folderActions: folderActions, dashboardActions: dashboardActions} } func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{}) { + if f.user == nil || f.user.Permissions == nil || f.user.Permissions[f.user.OrgID] == nil { + return "(1 = 0)", nil + } + dashWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeDashboardsPrefix) + folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix) + + filter, params := accesscontrol.UserRolesFilter(f.user.OrgID, f.user.UserID, f.user.Teams, accesscontrol.GetOrgRoles(f.user)) + rolesFilter := "AND role_id IN(SELECT distinct id FROM role " + filter + ")" var args []interface{} builder := strings.Builder{} - builder.WriteString("(") - + builder.WriteRune('(') if len(f.dashboardActions) > 0 { - builder.WriteString("((") + actionsToCheck := make([]interface{}, 0, len(f.dashboardActions)) + for _, action := range f.dashboardActions { + var hasWildcard bool + for _, scope := range f.user.Permissions[f.user.OrgID][action] { + if dashWildcards.Contains(scope) || folderWildcards.Contains(scope) { + hasWildcard = true + break + } + } + if !hasWildcard { + actionsToCheck = append(actionsToCheck, action) + } + } - dashFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeDashboardsPrefix, f.dashboardActions...) - builder.WriteString(dashFilter.Where) - args = append(args, dashFilter.Args...) + if len(actionsToCheck) > 0 { + builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE action IN (?" + strings.Repeat(", ?", len(actionsToCheck)-1) + ") AND scope LIKE 'dashboards:uid:%' " + rolesFilter + " GROUP BY role_id, scope HAVING COUNT(action) = ?) AND NOT dashboard.is_folder)") + args = append(args, actionsToCheck...) + args = append(args, params...) + args = append(args, len(actionsToCheck)) - builder.WriteString(" OR dashboard.folder_id IN(SELECT id FROM dashboard WHERE ") - dashFolderFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeFoldersPrefix, f.dashboardActions...) - - builder.WriteString(dashFolderFilter.Where) - builder.WriteString(")) AND NOT dashboard.is_folder)") - args = append(args, dashFolderFilter.Args...) + builder.WriteString(" OR ") + builder.WriteString("(dashboard.folder_id IN (SELECT id FROM dashboard as d WHERE d.uid IN (SELECT substr(scope, 13) FROM permission WHERE action IN (?" + strings.Repeat(", ?", len(actionsToCheck)-1) + ") AND scope LIKE 'folders:uid:%' " + rolesFilter + " GROUP BY role_id, scope HAVING COUNT(action) = ?)) AND NOT dashboard.is_folder)") + args = append(args, actionsToCheck...) + args = append(args, params...) + args = append(args, len(actionsToCheck)) + } else { + builder.WriteString("NOT dashboard.is_folder") + } } if len(f.folderActions) > 0 { if len(f.dashboardActions) > 0 { builder.WriteString(" OR ") } - builder.WriteString("(") - folderFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeFoldersPrefix, f.folderActions...) - builder.WriteString(folderFilter.Where) - builder.WriteString(" AND dashboard.is_folder)") - args = append(args, folderFilter.Args...) + + actionsToCheck := make([]interface{}, 0, len(f.folderActions)) + for _, action := range f.folderActions { + var hasWildcard bool + for _, scope := range f.user.Permissions[f.user.OrgID][action] { + if folderWildcards.Contains(scope) { + hasWildcard = true + break + } + } + if !hasWildcard { + actionsToCheck = append(actionsToCheck, action) + } + } + + if len(actionsToCheck) > 0 { + builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 13) FROM permission WHERE action IN (?" + strings.Repeat(", ?", len(actionsToCheck)-1) + ") AND scope LIKE 'folders:uid:%' " + rolesFilter + " GROUP BY role_id, scope HAVING COUNT(action) = ?) AND dashboard.is_folder)") + args = append(args, actionsToCheck...) + args = append(args, params...) + args = append(args, len(actionsToCheck)) + } else { + builder.WriteString("dashboard.is_folder") + } } - builder.WriteString(")") + builder.WriteRune(')') return builder.String(), args } diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 3c5d3e9b2e6..3ce86ebdbac 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -1,148 +1,229 @@ -package permissions +package permissions_test import ( - "fmt" + "context" + "strconv" "testing" + "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestNewAccessControlDashboardPermissionFilter(t *testing.T) { - randomType := "random_" + util.GenerateShortUID() - testCases := []struct { - permission models.PermissionType - queryType string - expectedDashboardActions []string - expectedFolderActions []string - }{ +func TestIntegration_DashboardPermissionFilter(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + type testCase struct { + desc string + queryType string + permission models.PermissionType + permissions []accesscontrol.Permission + expectedResult int + } + + tests := []testCase{ { - queryType: searchstore.TypeAlertFolder, - permission: models.PERMISSION_ADMIN, - expectedDashboardActions: nil, - expectedFolderActions: []string{ - dashboards.ActionFoldersRead, - accesscontrol.ActionAlertingRuleRead, - accesscontrol.ActionAlertingRuleCreate, - }, - }, - { - queryType: searchstore.TypeAlertFolder, - permission: models.PERMISSION_EDIT, - expectedDashboardActions: nil, - expectedFolderActions: []string{ - dashboards.ActionFoldersRead, - accesscontrol.ActionAlertingRuleRead, - accesscontrol.ActionAlertingRuleCreate, - }, - }, - { - queryType: searchstore.TypeAlertFolder, - permission: models.PERMISSION_VIEW, - expectedDashboardActions: nil, - expectedFolderActions: []string{ - dashboards.ActionFoldersRead, - accesscontrol.ActionAlertingRuleRead, - }, - }, - { - queryType: randomType, - permission: models.PERMISSION_ADMIN, - expectedDashboardActions: []string{ - dashboards.ActionDashboardsRead, - dashboards.ActionDashboardsWrite, - }, - expectedFolderActions: []string{ - dashboards.ActionFoldersRead, - dashboards.ActionDashboardsCreate, - }, - }, - { - queryType: randomType, - permission: models.PERMISSION_EDIT, - expectedDashboardActions: []string{ - dashboards.ActionDashboardsRead, - dashboards.ActionDashboardsWrite, - }, - expectedFolderActions: []string{ - dashboards.ActionFoldersRead, - dashboards.ActionDashboardsCreate, - }, - }, - { - queryType: randomType, + desc: "Should be able to view all dashboards with wildcard scope", permission: models.PERMISSION_VIEW, - expectedDashboardActions: []string{ - dashboards.ActionDashboardsRead, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, }, - expectedFolderActions: []string{ - dashboards.ActionFoldersRead, + expectedResult: 100, + }, + { + desc: "Should be able to view all dashboards with folder wildcard scope", + permission: models.PERMISSION_VIEW, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, + expectedResult: 100, + }, + { + desc: "Should be able to view a subset of dashboards with dashboard scopes", + permission: models.PERMISSION_VIEW, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:110"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:40"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:22"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:13"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:55"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:99"}, + }, + expectedResult: 6, + }, + { + desc: "Should be able to view a subset of dashboards with dashboard action and folder scope", + permission: models.PERMISSION_VIEW, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:8"}, + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:10"}, + }, + expectedResult: 20, + }, + { + desc: "Should be able to view all folders with folder wildcard", + permission: models.PERMISSION_VIEW, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:*"}, + }, + expectedResult: 10, + }, + { + desc: "Should be able to view a subset folders", + permission: models.PERMISSION_VIEW, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:6"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:9"}, + }, + expectedResult: 3, + }, + { + desc: "Should return folders and dashboard with 'edit' permission", + permission: models.PERMISSION_EDIT, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"}, + }, + expectedResult: 2, + }, + { + desc: "Should return folders that users can read alerts from", + permission: models.PERMISSION_VIEW, + queryType: searchstore.TypeAlertFolder, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:8"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8"}, + }, + expectedResult: 2, + }, + { + desc: "Should return folders that users can read alerts when user has read wildcard", + permission: models.PERMISSION_VIEW, + queryType: searchstore.TypeAlertFolder, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "*"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8"}, + }, + expectedResult: 2, }, } - for _, testCase := range testCases { - t.Run(fmt.Sprintf("query type %s, permissions %s", testCase.queryType, testCase.permission), func(t *testing.T) { - filters := NewAccessControlDashboardPermissionFilter(&user.SignedInUser{}, testCase.permission, testCase.queryType) + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + store := setupTest(t, 10, 100, tt.permissions) + usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}} + filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType) - require.Equal(t, testCase.expectedDashboardActions, filters.dashboardActions) - require.Equal(t, testCase.expectedFolderActions, filters.folderActions) + var result int + err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + q, params := filter.Where() + _, err := sess.SQL("SELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result) + return err + }) + require.NoError(t, err) + + assert.Equal(t, tt.expectedResult, result) }) } } -func TestAccessControlDashboardPermissionFilter_Where(t *testing.T) { - testCases := []struct { - title string - dashboardActions []string - folderActions []string - expectedResult string - }{ - { - title: "folder and dashboard actions are defined", - dashboardActions: []string{"test"}, - folderActions: []string{"test"}, - expectedResult: "((( 1 = 0 OR dashboard.folder_id IN(SELECT id FROM dashboard WHERE 1 = 0)) AND NOT dashboard.is_folder) OR ( 1 = 0 AND dashboard.is_folder))", - }, - { - title: "folder actions are defined but not dashboard actions", - dashboardActions: nil, - folderActions: []string{"test"}, - expectedResult: "(( 1 = 0 AND dashboard.is_folder))", - }, - { - title: "dashboard actions are defined but not folder actions", - dashboardActions: []string{"test"}, - folderActions: nil, - expectedResult: "((( 1 = 0 OR dashboard.folder_id IN(SELECT id FROM dashboard WHERE 1 = 0)) AND NOT dashboard.is_folder))", - }, - { - title: "dashboard actions are defined but not folder actions", - dashboardActions: nil, - folderActions: nil, - expectedResult: "()", - }, - } - - for _, testCase := range testCases { - t.Run(testCase.title, func(t *testing.T) { - filter := AccessControlDashboardPermissionFilter{ - User: &user.SignedInUser{Permissions: map[int64]map[string][]string{}}, - dashboardActions: testCase.dashboardActions, - folderActions: testCase.folderActions, +func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB { + store := db.InitTestDB(t) + err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + dashes := make([]models.Dashboard, 0, numFolders+numDashboards) + for i := 1; i <= numFolders; i++ { + str := strconv.Itoa(i) + dashes = append(dashes, models.Dashboard{ + OrgId: 1, + Slug: str, + Uid: str, + Title: str, + IsFolder: true, + Data: simplejson.New(), + Created: time.Now(), + Updated: time.Now(), + }) + } + // Seed 100 dashboard + for i := numFolders + 1; i <= numFolders+numDashboards; i++ { + str := strconv.Itoa(i) + folderID := numFolders + if i%numFolders != 0 { + folderID = i % numFolders } + dashes = append(dashes, models.Dashboard{ + OrgId: 1, + IsFolder: false, + FolderId: int64(folderID), + Uid: str, + Slug: str, + Title: str, + Data: simplejson.New(), + Created: time.Now(), + Updated: time.Now(), + }) + } - query, args := filter.Where() + _, err := sess.InsertMulti(&dashes) + if err != nil { + return err + } - assert.Empty(t, args) - assert.Equal(t, testCase.expectedResult, query) + role := &accesscontrol.Role{ + OrgID: 0, + UID: "basic_viewer", + Name: "basic:viewer", + Updated: time.Now(), + Created: time.Now(), + } + _, err = sess.Insert(role) + if err != nil { + return err + } + + _, err = sess.Insert(accesscontrol.BuiltinRole{ + OrgID: 0, + RoleID: role.ID, + Role: "Viewer", + Created: time.Now(), + Updated: time.Now(), }) - } + if err != nil { + return err + } + + for i := range permissions { + permissions[i].RoleID = role.ID + permissions[i].Created = time.Now() + permissions[i].Updated = time.Now() + } + if len(permissions) > 0 { + _, err = sess.InsertMulti(&permissions) + if err != nil { + return err + } + } + + return nil + }) + require.NoError(t, err) + return store } diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go new file mode 100644 index 00000000000..318ef30f9b3 --- /dev/null +++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go @@ -0,0 +1,143 @@ +package permissions_test + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/permissions" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards int) { + store := setupBenchMark(b, numUsers, numDashboards) + b.ResetTimer() + for i := 0; i < b.N; i++ { + usr := &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: {}}} + filter := permissions.NewAccessControlDashboardPermissionFilter(usr, models.PERMISSION_VIEW, "") + var result int + err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + q, params := filter.Where() + _, err := sess.SQL("SELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result) + return err + }) + require.NoError(b, err) + assert.Equal(b, numDashboards, result) + } +} + +func setupBenchMark(b *testing.B, numUsers, numDashboards int) db.DB { + store := db.InitTestDB(b) + now := time.Now() + err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + dashes := make([]models.Dashboard, 0, numDashboards) + for i := 1; i <= numDashboards; i++ { + str := strconv.Itoa(i) + dashes = append(dashes, models.Dashboard{ + OrgId: 1, + IsFolder: false, + Uid: str, + Slug: str, + Title: str, + Data: simplejson.New(), + Created: now, + Updated: now, + }) + } + + err := batch(len(dashes), 1000, func(start, end int) error { + _, err := sess.InsertMulti(dashes[start:end]) + return err + }) + require.NoError(b, err) + + roles := make([]accesscontrol.Role, 0, numUsers) + assignments := make([]accesscontrol.UserRole, 0, numUsers) + permissions := make([]accesscontrol.Permission, 0, numUsers*numDashboards) + for i := 1; i <= numUsers; i++ { + name := fmt.Sprintf("managed_%d", i) + roles = append(roles, accesscontrol.Role{ + UID: name, + Name: name, + Updated: now, + Created: now, + }) + assignments = append(assignments, accesscontrol.UserRole{ + RoleID: int64(i), + UserID: int64(i), + Created: now, + }) + for _, dash := range dashes { + permissions = append(permissions, accesscontrol.Permission{ + RoleID: int64(i), + Action: dashboards.ActionDashboardsRead, + Scope: dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.Uid), + Updated: now, + Created: now, + }) + } + } + + err = batch(len(roles), 5000, func(start, end int) error { + _, err := sess.InsertMulti(roles[start:end]) + return err + }) + require.NoError(b, err) + + err = batch(len(assignments), 5000, func(start, end int) error { + _, err := sess.InsertMulti(assignments[start:end]) + return err + }) + require.NoError(b, err) + + err = batch(len(permissions), 5000, func(start, end int) error { + _, err := sess.InsertMulti(permissions[start:end]) + return err + }) + require.NoError(b, err) + return nil + }) + + require.NoError(b, err) + return store +} + +func BenchmarkDashboardPermissionFilter_100_100(b *testing.B) { + benchmarkDashboardPermissionFilter(b, 100, 100) +} + +func BenchmarkDashboardPermissionFilter_100_1000(b *testing.B) { + benchmarkDashboardPermissionFilter(b, 100, 1000) +} + +func BenchmarkDashboardPermissionFilter_300_10000(b *testing.B) { + benchmarkDashboardPermissionFilter(b, 300, 10000) +} + +func batch(count, batchSize int, eachFn func(start, end int) error) error { + for i := 0; i < count; { + end := i + batchSize + if end > count { + end = count + } + + if err := eachFn(i, end); err != nil { + return err + } + + i = end + } + + return nil +}