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
This commit is contained in:
Yuriy Tseretyan
2022-03-16 10:07:04 -04:00
committed by GitHub
parent 7192b7caee
commit ea815d640f
4 changed files with 202 additions and 27 deletions

View File

@@ -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),
}
}

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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
}