mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 01:16:31 -06:00
RBAC: Improve performance of dashboard filter query (#56813)
* RBAC: Move UserRolesFilter to domain package * Dashboard Permissions: Rewrite rbac filter to check access in sql * RBAC: Add break when wildcard is found * RBAC: Add tests for dashboard filter * RBAC: Update tests * RBAC: Cover more test cases Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
parent
d2a70bc42d
commit
7386f8652c
@ -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 = ?"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
143
pkg/services/sqlstore/permissions/dashboards_bench_test.go
Normal file
143
pkg/services/sqlstore/permissions/dashboards_bench_test.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user