From ea815d640f7c1113af2021aea732450ba2b1a9d0 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Wed, 16 Mar 2022 10:07:04 -0400 Subject: [PATCH] Search Service to support search for folders available for alerting (#46483) * support new query type "alert-folder" * move action calculation to the constructor of the filter * update filter to support query type `dash-folder-alerting` and empty dashboard actions * require folders:read to access alert rules --- pkg/services/sqlstore/dashboard.go | 6 +- .../sqlstore/permissions/dashboard.go | 69 +++++--- .../sqlstore/permissions/dashboard_test.go | 147 ++++++++++++++++++ pkg/services/sqlstore/searchstore/filters.go | 7 +- 4 files changed, 202 insertions(+), 27 deletions(-) create mode 100644 pkg/services/sqlstore/permissions/dashboard_test.go diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 85c202d46f8..e74e4aa9565 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" @@ -92,9 +93,10 @@ func (ss *SQLStore) FindDashboards(ctx context.Context, query *search.FindPersis }, } - if ss.Cfg.IsFeatureToggleEnabled("accesscontrol") { + if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + // if access control is enabled, overwrite the filters so far filters = []interface{}{ - permissions.AccessControlDashboardPermissionFilter{User: query.SignedInUser, PermissionLevel: query.Permission}, + permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type), } } diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index ffd8c7a2baf..3431d9dd563 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" ) type DashboardPermissionFilter struct { @@ -77,37 +78,61 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) { } type AccessControlDashboardPermissionFilter struct { - User *models.SignedInUser - PermissionLevel models.PermissionType + User *models.SignedInUser + dashboardActions []string + folderActions []string +} + +// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the models.PermissionType and query type +func NewAccessControlDashboardPermissionFilter(user *models.SignedInUser, permissionLevel models.PermissionType, queryType string) AccessControlDashboardPermissionFilter { + needEdit := permissionLevel > models.PERMISSION_VIEW + folderActions := []string{dashboards.ActionFoldersRead} + var dashboardActions []string + if queryType == searchstore.TypeAlertFolder { + folderActions = append(folderActions, accesscontrol.ActionAlertingRuleRead) + if needEdit { + folderActions = append(folderActions, accesscontrol.ActionAlertingRuleUpdate) + } + } else { + dashboardActions = append(dashboardActions, accesscontrol.ActionDashboardsRead) + if needEdit { + folderActions = append(folderActions, accesscontrol.ActionDashboardsCreate) + dashboardActions = append(dashboardActions, accesscontrol.ActionDashboardsWrite) + } + } + return AccessControlDashboardPermissionFilter{User: user, folderActions: folderActions, dashboardActions: dashboardActions} } func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{}) { - folderActions := []string{dashboards.ActionFoldersRead} - dashboardActions := []string{accesscontrol.ActionDashboardsRead} - if f.PermissionLevel == models.PERMISSION_EDIT { - folderActions = append(folderActions, accesscontrol.ActionDashboardsCreate) - dashboardActions = append(dashboardActions, accesscontrol.ActionDashboardsWrite) - } - var args []interface{} builder := strings.Builder{} - builder.WriteString("(((") + builder.WriteString("(") - dashFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "dashboards", dashboardActions...) - builder.WriteString(dashFilter.Where) - args = append(args, dashFilter.Args...) + if len(f.dashboardActions) > 0 { + builder.WriteString("((") + dashFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "dashboards", f.dashboardActions...) + builder.WriteString(dashFilter.Where) + args = append(args, dashFilter.Args...) - builder.WriteString(" OR ") + builder.WriteString(" OR ") - dashFolderFilter, _ := accesscontrol.Filter(f.User, "dashboard.folder_id", "folders", dashboardActions...) - builder.WriteString(dashFolderFilter.Where) - builder.WriteString(") AND NOT dashboard.is_folder) OR (") - args = append(args, dashFolderFilter.Args...) + dashFolderFilter, _ := accesscontrol.Filter(f.User, "dashboard.folder_id", "folders", f.dashboardActions...) + builder.WriteString(dashFolderFilter.Where) + builder.WriteString(") AND NOT dashboard.is_folder)") + args = append(args, dashFolderFilter.Args...) + } - folderFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "folders", folderActions...) - builder.WriteString(folderFilter.Where) - builder.WriteString(" AND dashboard.is_folder))") - args = append(args, folderFilter.Args...) + if len(f.folderActions) > 0 { + if len(f.dashboardActions) > 0 { + builder.WriteString(" OR ") + } + builder.WriteString("(") + folderFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "folders", f.folderActions...) + builder.WriteString(folderFilter.Where) + builder.WriteString(" AND dashboard.is_folder)") + args = append(args, folderFilter.Args...) + } + builder.WriteString(")") return builder.String(), args } diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go new file mode 100644 index 00000000000..ab4684246f3 --- /dev/null +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -0,0 +1,147 @@ +package permissions + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "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/sqlstore/searchstore" + "github.com/grafana/grafana/pkg/util" +) + +func TestNewAccessControlDashboardPermissionFilter(t *testing.T) { + randomType := "random_" + util.GenerateShortUID() + testCases := []struct { + permission models.PermissionType + queryType string + expectedDashboardActions []string + expectedFolderActions []string + }{ + { + queryType: searchstore.TypeAlertFolder, + permission: models.PERMISSION_ADMIN, + expectedDashboardActions: nil, + expectedFolderActions: []string{ + dashboards.ActionFoldersRead, + accesscontrol.ActionAlertingRuleRead, + accesscontrol.ActionAlertingRuleUpdate, + }, + }, + { + queryType: searchstore.TypeAlertFolder, + permission: models.PERMISSION_EDIT, + expectedDashboardActions: nil, + expectedFolderActions: []string{ + dashboards.ActionFoldersRead, + accesscontrol.ActionAlertingRuleRead, + accesscontrol.ActionAlertingRuleUpdate, + }, + }, + { + queryType: searchstore.TypeAlertFolder, + permission: models.PERMISSION_VIEW, + expectedDashboardActions: nil, + expectedFolderActions: []string{ + dashboards.ActionFoldersRead, + accesscontrol.ActionAlertingRuleRead, + }, + }, + { + queryType: randomType, + permission: models.PERMISSION_ADMIN, + expectedDashboardActions: []string{ + accesscontrol.ActionDashboardsRead, + accesscontrol.ActionDashboardsWrite, + }, + expectedFolderActions: []string{ + dashboards.ActionFoldersRead, + accesscontrol.ActionDashboardsCreate, + }, + }, + { + queryType: randomType, + permission: models.PERMISSION_EDIT, + expectedDashboardActions: []string{ + accesscontrol.ActionDashboardsRead, + accesscontrol.ActionDashboardsWrite, + }, + expectedFolderActions: []string{ + dashboards.ActionFoldersRead, + accesscontrol.ActionDashboardsCreate, + }, + }, + { + queryType: randomType, + permission: models.PERMISSION_VIEW, + expectedDashboardActions: []string{ + accesscontrol.ActionDashboardsRead, + }, + expectedFolderActions: []string{ + dashboards.ActionFoldersRead, + }, + }, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("query type %s, permissions %s", testCase.queryType, testCase.permission), func(t *testing.T) { + filters := NewAccessControlDashboardPermissionFilter(&models.SignedInUser{}, testCase.permission, testCase.queryType) + + require.Equal(t, testCase.expectedDashboardActions, filters.dashboardActions) + require.Equal(t, testCase.expectedFolderActions, filters.folderActions) + }) + } +} + +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 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 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: &models.SignedInUser{Permissions: map[int64]map[string][]string{}}, + dashboardActions: testCase.dashboardActions, + folderActions: testCase.folderActions, + } + + query, args := filter.Where() + + assert.Empty(t, args) + assert.Equal(t, testCase.expectedResult, query) + }) + } +} diff --git a/pkg/services/sqlstore/searchstore/filters.go b/pkg/services/sqlstore/searchstore/filters.go index fc20ee494bc..c1c43933105 100644 --- a/pkg/services/sqlstore/searchstore/filters.go +++ b/pkg/services/sqlstore/searchstore/filters.go @@ -38,8 +38,9 @@ type FilterSelect interface { } const ( - TypeFolder = "dash-folder" - TypeDashboard = "dash-db" + TypeFolder = "dash-folder" + TypeDashboard = "dash-db" + TypeAlertFolder = "dash-folder-alerting" ) type TypeFilter struct { @@ -48,7 +49,7 @@ type TypeFilter struct { } func (f TypeFilter) Where() (string, []interface{}) { - if f.Type == TypeFolder { + if f.Type == TypeFolder || f.Type == TypeAlertFolder { return "dashboard.is_folder = " + f.Dialect.BooleanStr(true), nil }