diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md index 640b5a12ebf..c3a42054a0f 100644 --- a/docs/sources/http_api/folder.md +++ b/docs/sources/http_api/folder.md @@ -24,7 +24,7 @@ that you cannot use this API for retrieving information about the General folder `GET /api/folders` -Returns all folders that the authenticated user has permission to view. You can control the maximum number of folders returned through the `limit` query parameter, the default is 1000. +Returns all folders that the authenticated user has permission to view. You can control the maximum number of folders returned through the `limit` query parameter, the default is 1000. You can also pass the `page` query parameter for fetching folders from a page other than the first one. **Example Request**: diff --git a/pkg/api/folder.go b/pkg/api/folder.go index fabbd7c66bd..f89841b4691 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -16,7 +16,7 @@ import ( func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) - folders, err := s.GetFolders(c.QueryInt64("limit")) + folders, err := s.GetFolders(c.QueryInt64("limit"), c.QueryInt64("page")) if err != nil { return ToFolderErrorResponse(err) diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 080ef9b4e72..2b4b54d9a64 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -217,7 +217,7 @@ type fakeFolderService struct { DeletedFolderUids []string } -func (s *fakeFolderService) GetFolders(limit int64) ([]*models.Folder, error) { +func (s *fakeFolderService) GetFolders(limit int64, page int64) ([]*models.Folder, error) { return s.GetFoldersResult, s.GetFoldersError } diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go index 79cdbfaa32b..31087485eec 100644 --- a/pkg/services/dashboards/folder_service.go +++ b/pkg/services/dashboards/folder_service.go @@ -13,7 +13,7 @@ import ( // FolderService is a service for operating on folders. type FolderService interface { - GetFolders(limit int64) ([]*models.Folder, error) + GetFolders(limit int64, page int64) ([]*models.Folder, error) GetFolderByID(id int64) (*models.Folder, error) GetFolderByUID(uid string) (*models.Folder, error) GetFolderByTitle(title string) (*models.Folder, error) @@ -32,7 +32,7 @@ var NewFolderService = func(orgID int64, user *models.SignedInUser, store dashbo } } -func (dr *dashboardServiceImpl) GetFolders(limit int64) ([]*models.Folder, error) { +func (dr *dashboardServiceImpl) GetFolders(limit int64, page int64) ([]*models.Folder, error) { searchQuery := search.Query{ SignedInUser: dr.user, DashboardIds: make([]int64, 0), @@ -41,6 +41,7 @@ func (dr *dashboardServiceImpl) GetFolders(limit int64) ([]*models.Folder, error OrgId: dr.orgId, Type: "dash-folder", Permission: models.PERMISSION_VIEW, + Page: page, } if err := bus.Dispatch(&searchQuery); err != nil { diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 002c8071def..7398653d78a 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -2,7 +2,6 @@ package api import ( "encoding/json" - "errors" "fmt" "net/http" "time" @@ -62,8 +61,19 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res }, } + namespaceMap, err := srv.store.GetNamespaces(c.OrgId, c.SignedInUser) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user") + } + + namespaceUIDs := make([]string, len(namespaceMap)) + for k := range namespaceMap { + namespaceUIDs = append(namespaceUIDs, k) + } + ruleGroupQuery := ngmodels.ListOrgRuleGroupsQuery{ - OrgID: c.SignedInUser.OrgId, + OrgID: c.SignedInUser.OrgId, + NamespaceUIDs: namespaceUIDs, } if err := srv.store.GetOrgRuleGroups(&ruleGroupQuery); err != nil { ruleResponse.DiscoveryBase.Status = "error" @@ -77,13 +87,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res continue } groupId, namespaceUID, namespace := r[0], r[1], r[2] - if _, err := srv.store.GetNamespaceByUID(namespaceUID, c.SignedInUser.OrgId, c.SignedInUser); err != nil { - if errors.Is(err, models.ErrFolderAccessDenied) { - // do not include it in the response - continue - } - return toNamespaceErrorResponse(err) - } alertRuleQuery := ngmodels.ListRuleGroupAlertRulesQuery{OrgID: c.SignedInUser.OrgId, NamespaceUID: namespaceUID, RuleGroup: groupId} if err := srv.store.GetRuleGroupAlertRules(&alertRuleQuery); err != nil { ruleResponse.DiscoveryBase.Status = "error" diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index b71675774aa..6798a833f45 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -146,26 +146,34 @@ func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Resp } func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response { - q := ngmodels.ListAlertRulesQuery{ - OrgID: c.SignedInUser.OrgId, + namespaceMap, err := srv.store.GetNamespaces(c.OrgId, c.SignedInUser) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user") } + + namespaceUIDs := make([]string, len(namespaceMap)) + for k := range namespaceMap { + namespaceUIDs = append(namespaceUIDs, k) + } + + q := ngmodels.ListAlertRulesQuery{ + OrgID: c.SignedInUser.OrgId, + NamespaceUIDs: namespaceUIDs, + } + if err := srv.store.GetOrgAlertRules(&q); err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") } configs := make(map[string]map[string]apimodels.GettableRuleGroupConfig) for _, r := range q.Result { - folder, err := srv.store.GetNamespaceByUID(r.NamespaceUID, c.SignedInUser.OrgId, c.SignedInUser) - if err != nil { - if errors.Is(err, models.ErrFolderAccessDenied) { - // do not fail if used does not have access to a specific namespace - // just do not include it in the response - continue - } - return toNamespaceErrorResponse(err) + folder, ok := namespaceMap[r.NamespaceUID] + if !ok { + srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", r.NamespaceUID, "rule", r.UID) + continue } namespace := folder.Title - _, ok := configs[namespace] + _, ok = configs[namespace] if !ok { ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) configs[namespace] = make(map[string]apimodels.GettableRuleGroupConfig) diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 74209b7ea38..a15ca75d52f 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -133,7 +133,8 @@ type GetAlertRuleByUIDQuery struct { // ListAlertRulesQuery is the query for listing alert rules type ListAlertRulesQuery struct { - OrgID int64 + OrgID int64 + NamespaceUIDs []string Result []*AlertRule } @@ -159,7 +160,8 @@ type ListRuleGroupAlertRulesQuery struct { // ListOrgRuleGroupsQuery is the query for listing unique rule groups type ListOrgRuleGroupsQuery struct { - OrgID int64 + OrgID int64 + NamespaceUIDs []string Result [][]string } diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 2a57519c743..4faa4a0610b 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "time" "github.com/grafana/grafana/pkg/services/guardian" @@ -46,8 +47,8 @@ type RuleStore interface { GetOrgAlertRules(query *ngmodels.ListAlertRulesQuery) error GetNamespaceAlertRules(query *ngmodels.ListNamespaceAlertRulesQuery) error GetRuleGroupAlertRules(query *ngmodels.ListRuleGroupAlertRulesQuery) error + GetNamespaces(int64, *models.SignedInUser) (map[string]*models.Folder, error) GetNamespaceByTitle(string, int64, *models.SignedInUser, bool) (*models.Folder, error) - GetNamespaceByUID(string, int64, *models.SignedInUser) (*models.Folder, error) GetOrgRuleGroups(query *ngmodels.ListOrgRuleGroupsQuery) error UpsertAlertRules([]UpsertRule) error UpdateRuleGroup(UpdateRuleGroupCmd) error @@ -320,7 +321,18 @@ func (st DBstore) GetOrgAlertRules(query *ngmodels.ListAlertRulesQuery) error { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { alertRules := make([]*ngmodels.AlertRule, 0) q := "SELECT * FROM alert_rule WHERE org_id = ?" - if err := sess.SQL(q, query.OrgID).Find(&alertRules); err != nil { + params := []interface{}{query.OrgID} + + if len(query.NamespaceUIDs) > 0 { + placeholders := make([]string, 0, len(query.NamespaceUIDs)) + for _, folderUID := range query.NamespaceUIDs { + params = append(params, folderUID) + placeholders = append(placeholders, "?") + } + q = fmt.Sprintf("%s AND namespace_uid IN (%s)", q, strings.Join(placeholders, ",")) + } + + if err := sess.SQL(q, params...).Find(&alertRules); err != nil { return err } @@ -359,6 +371,30 @@ func (st DBstore) GetRuleGroupAlertRules(query *ngmodels.ListRuleGroupAlertRules }) } +// GetNamespaces returns the folders that are visible to the user +func (st DBstore) GetNamespaces(orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) { + s := dashboards.NewFolderService(orgID, user, st.SQLStore) + namespaceMap := make(map[string]*models.Folder) + var page int64 = 1 + for { + // if limit is negative; it fetches at most 1000 + folders, err := s.GetFolders(-1, page) + if err != nil { + return nil, err + } + + if len(folders) == 0 { + break + } + + for _, f := range folders { + namespaceMap[f.Uid] = f + } + page += 1 + } + return namespaceMap, nil +} + // GetNamespaceByTitle is a handler for retrieving a namespace by its title. Alerting rules follow a Grafana folder-like structure which we call namespaces. func (st DBstore) GetNamespaceByTitle(namespace string, orgID int64, user *models.SignedInUser, withCanSave bool) (*models.Folder, error) { s := dashboards.NewFolderService(orgID, user, st.SQLStore) @@ -380,17 +416,6 @@ func (st DBstore) GetNamespaceByTitle(namespace string, orgID int64, user *model return folder, nil } -// GetNamespaceByUID is a handler for retrieving namespace by its UID. -func (st DBstore) GetNamespaceByUID(UID string, orgID int64, user *models.SignedInUser) (*models.Folder, error) { - s := dashboards.NewFolderService(orgID, user, st.SQLStore) - folder, err := s.GetFolderByUID(UID) - if err != nil { - return nil, err - } - - return folder, nil -} - // GetAlertRulesForScheduling returns alert rule info (identifier, interval, version state) // that is useful for it's scheduling. func (st DBstore) GetAlertRulesForScheduling(query *ngmodels.ListAlertRulesQuery) error { @@ -542,8 +567,20 @@ func (st DBstore) UpdateRuleGroup(cmd UpdateRuleGroupCmd) error { func (st DBstore) GetOrgRuleGroups(query *ngmodels.ListOrgRuleGroupsQuery) error { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { var ruleGroups [][]string - q := "SELECT DISTINCT rule_group, namespace_uid, (select title from dashboard where org_id = alert_rule.org_id and uid = alert_rule.namespace_uid) AS namespace_title FROM alert_rule WHERE org_id = ? ORDER BY namespace_title" - if err := sess.SQL(q, query.OrgID).Find(&ruleGroups); err != nil { + q := "SELECT DISTINCT rule_group, namespace_uid, (select title from dashboard where org_id = alert_rule.org_id and uid = alert_rule.namespace_uid) AS namespace_title FROM alert_rule WHERE org_id = ?" + params := []interface{}{query.OrgID} + + if len(query.NamespaceUIDs) > 0 { + placeholders := make([]string, 0, len(query.NamespaceUIDs)) + for _, folderUID := range query.NamespaceUIDs { + params = append(params, folderUID) + placeholders = append(placeholders, "?") + } + q = fmt.Sprintf(" %s AND namespace_uid IN (%s)", q, strings.Join(placeholders, ",")) + } + q = fmt.Sprintf(" %s ORDER BY namespace_title", q) + + if err := sess.SQL(q, params...).Find(&ruleGroups); err != nil { return err }