diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index f00b1ec0124..3dfbc483316 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -85,7 +85,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) } - namespaceMap, err := srv.store.GetNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser) + namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user") } diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 74b9d2ade0a..7094c6aa956 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -196,7 +196,7 @@ func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Resp } func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response { - namespaceMap, err := srv.store.GetNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser) + namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user") } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 3e0eec85e0c..df2588e1fbb 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -93,6 +93,7 @@ func (ng *AlertNG) init() error { SQLStore: ng.SQLStore, Logger: ng.Log, FolderService: ng.folderService, + AccessControl: ng.accesscontrol, } decryptFn := ng.SecretsService.GetDecryptedValue diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index e76d33274b8..06beea3a1d2 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -6,13 +6,12 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/services/guardian" - "github.com/grafana/grafana/pkg/models" - + "github.com/grafana/grafana/pkg/services/guardian" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/util" ) @@ -42,7 +41,7 @@ type RuleStore interface { GetOrgAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error GetNamespaceAlertRules(ctx context.Context, query *ngmodels.ListNamespaceAlertRulesQuery) error GetAlertRules(ctx context.Context, query *ngmodels.GetAlertRulesQuery) error - GetNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error) + GetUserVisibleNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error) GetNamespaceByTitle(context.Context, string, int64, *models.SignedInUser, bool) (*models.Folder, error) GetOrgRuleGroups(ctx context.Context, query *ngmodels.ListOrgRuleGroupsQuery) error UpsertAlertRules(ctx context.Context, rule []UpsertRule) error @@ -270,23 +269,44 @@ func (st DBstore) GetAlertRules(ctx context.Context, query *ngmodels.GetAlertRul }) } -// GetNamespaces returns the folders that are visible to the user -func (st DBstore) GetNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) { +// GetNamespaces returns the folders that are visible to the user and have at least one alert in it +func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) { namespaceMap := make(map[string]*models.Folder) + + searchQuery := models.FindPersistedDashboardsQuery{ + OrgId: orgID, + SignedInUser: user, + Type: searchstore.TypeAlertFolder, + Limit: -1, + Permission: models.PERMISSION_VIEW, + Sort: models.SortOption{}, + Filters: []interface{}{ + searchstore.FolderWithAlertsFilter{}, + }, + } + var page int64 = 1 for { - // if limit is negative; it fetches at most 1000 - folders, err := st.FolderService.GetFolders(ctx, user, orgID, -1, page) + query := searchQuery + query.Page = page + proj, err := st.SQLStore.FindDashboards(ctx, &query) if err != nil { return nil, err } - if len(folders) == 0 { + if len(proj) == 0 { break } - for _, f := range folders { - namespaceMap[f.Uid] = f + for _, hit := range proj { + if !hit.IsFolder { + continue + } + namespaceMap[hit.UID] = &models.Folder{ + Id: hit.ID, + Uid: hit.UID, + Title: hit.Title, + } } page += 1 } @@ -300,7 +320,8 @@ func (st DBstore) GetNamespaceByTitle(ctx context.Context, namespace string, org return nil, err } - if withCanSave { + // if access control is disabled, check that the user is allowed to save in the folder. + if withCanSave && st.AccessControl.IsDisabled() { g := guardian.New(ctx, folder.Id, orgID, user) if canSave, err := g.CanSave(); err != nil || !canSave { if err != nil { diff --git a/pkg/services/ngalert/store/database.go b/pkg/services/ngalert/store/database.go index 0ff8ea92eba..90b61a5e44c 100644 --- a/pkg/services/ngalert/store/database.go +++ b/pkg/services/ngalert/store/database.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -34,4 +35,5 @@ type DBstore struct { SQLStore *sqlstore.SQLStore Logger log.Logger FolderService dashboards.FolderService + AccessControl accesscontrol.AccessControl } diff --git a/pkg/services/ngalert/store/testing.go b/pkg/services/ngalert/store/testing.go index 6b6d89b6ba6..527626c3e98 100644 --- a/pkg/services/ngalert/store/testing.go +++ b/pkg/services/ngalert/store/testing.go @@ -208,7 +208,7 @@ func (f *FakeRuleStore) GetAlertRules(_ context.Context, q *models.GetAlertRules q.Result = result return nil } -func (f *FakeRuleStore) GetNamespaces(_ context.Context, orgID int64, _ *models2.SignedInUser) (map[string]*models2.Folder, error) { +func (f *FakeRuleStore) GetUserVisibleNamespaces(_ context.Context, orgID int64, _ *models2.SignedInUser) (map[string]*models2.Folder, error) { f.mtx.Lock() defer f.mtx.Unlock() diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index c4ff0978446..21751538381 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -91,7 +91,7 @@ func NewAccessControlDashboardPermissionFilter(user *models.SignedInUser, permis if queryType == searchstore.TypeAlertFolder { folderActions = append(folderActions, accesscontrol.ActionAlertingRuleRead) if needEdit { - folderActions = append(folderActions, accesscontrol.ActionAlertingRuleUpdate) + folderActions = append(folderActions, accesscontrol.ActionAlertingRuleCreate) } } else { dashboardActions = append(dashboardActions, accesscontrol.ActionDashboardsRead) diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index e514a783ce1..264954d2739 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -29,7 +29,7 @@ func TestNewAccessControlDashboardPermissionFilter(t *testing.T) { expectedFolderActions: []string{ dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead, - accesscontrol.ActionAlertingRuleUpdate, + accesscontrol.ActionAlertingRuleCreate, }, }, { @@ -39,7 +39,7 @@ func TestNewAccessControlDashboardPermissionFilter(t *testing.T) { expectedFolderActions: []string{ dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead, - accesscontrol.ActionAlertingRuleUpdate, + accesscontrol.ActionAlertingRuleCreate, }, }, { diff --git a/pkg/services/sqlstore/searchstore/filters.go b/pkg/services/sqlstore/searchstore/filters.go index c1c43933105..99810190c38 100644 --- a/pkg/services/sqlstore/searchstore/filters.go +++ b/pkg/services/sqlstore/searchstore/filters.go @@ -149,3 +149,13 @@ func sqlIDin(column string, ids []int64) (string, []interface{}) { } return fmt.Sprintf("%s IN %s", column, sqlArray), params } + +// FolderWithAlertsFilter applies a filter that makes the result contain only folders that contain alert rules +type FolderWithAlertsFilter struct { +} + +var _ FilterWhere = &FolderWithAlertsFilter{} + +func (f FolderWithAlertsFilter) Where() (string, []interface{}) { + return "EXISTS (SELECT 1 FROM alert_rule WHERE alert_rule.namespace_uid = dashboard.uid)", nil +}