From 8a3edf280ea9703bb62f1cf8d5f67c24c2a7a1fd Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Mon, 5 Jul 2021 10:49:14 +0300 Subject: [PATCH] Alerting: Fix prometheus API to check folder permissions (#36301) --- pkg/services/ngalert/api/api_prometheus.go | 8 + pkg/services/ngalert/store/alert_rule.go | 2 +- pkg/tests/api/alerting/api_prometheus_test.go | 147 ++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 613a83929a0..e9781533e4e 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "fmt" "net/http" "time" @@ -76,6 +77,13 @@ 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/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 7d86922c827..2a57519c743 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -542,7 +542,7 @@ 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) FROM alert_rule WHERE org_id = ?" + 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 { return err } diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index 18f535060e1..0bf2769cfec 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -258,3 +258,150 @@ func TestPrometheusRules(t *testing.T) { }, 18*time.Second, 2*time.Second) } } + +func TestPrometheusRulesPermissions(t *testing.T) { + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + EnableFeatureToggles: []string{"ngalert"}, + DisableAnonymous: true, + }) + store := testinfra.SetUpDatabase(t, dir) + // override bus to get the GetSignedInUserQuery handler + store.Bus = bus.GetBus() + grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) + + // Create a user to make authenticated requests + require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password")) + + // Create a namespace under default organisation (orgID = 1) where we'll save some alerts. + _, err := createFolder(t, store, 0, "folder1") + require.NoError(t, err) + + // Create another namespace under default organisation (orgID = 1) where we'll save some alerts. + _, err = createFolder(t, store, 0, "folder2") + require.NoError(t, err) + + // Create rule under folder1 + createRule(t, grafanaListedAddr, "folder1", "grafana", "password") + + // Create rule under folder2 + createRule(t, grafanaListedAddr, "folder2", "grafana", "password") + + // Now, let's see how this looks like. + { + promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr) + // nolint:gosec + resp, err := http.Get(promRulesURL) + require.NoError(t, err) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + require.JSONEq(t, ` +{ + "status": "success", + "data": { + "groups": [{ + "name": "arulegroup", + "file": "folder1", + "rules": [{ + "state": "inactive", + "name": "rule under folder folder1", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"-100\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "duration": 120, + "annotations": { + "annotation1": "val1" + }, + "labels": { + "label1": "val1" + }, + "health": "ok", + "lastError": "", + "type": "alerting", + "lastEvaluation": "0001-01-01T00:00:00Z", + "evaluationTime": 0 + }], + "interval": 60, + "lastEvaluation": "0001-01-01T00:00:00Z", + "evaluationTime": 0 + }, + { + "name": "arulegroup", + "file": "folder2", + "rules": [{ + "state": "inactive", + "name": "rule under folder folder2", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"-100\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "duration": 120, + "annotations": { + "annotation1": "val1" + }, + "labels": { + "label1": "val1" + }, + "health": "ok", + "lastError": "", + "type": "alerting", + "lastEvaluation": "0001-01-01T00:00:00Z", + "evaluationTime": 0 + }], + "interval": 60, + "lastEvaluation": "0001-01-01T00:00:00Z", + "evaluationTime": 0 + }] + } +}`, string(b)) + } + + // remove permissions from folder2 + require.NoError(t, store.UpdateDashboardACL(2, nil)) + + // make sure that folder2 is not included in the response + { + promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr) + // nolint:gosec + resp, err := http.Get(promRulesURL) + require.NoError(t, err) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + require.JSONEq(t, ` +{ + "status": "success", + "data": { + "groups": [{ + "name": "arulegroup", + "file": "folder1", + "rules": [{ + "state": "inactive", + "name": "rule under folder folder1", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"-100\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "duration": 120, + "annotations": { + "annotation1": "val1" + }, + "labels": { + "label1": "val1" + }, + "health": "ok", + "lastError": "", + "type": "alerting", + "lastEvaluation": "0001-01-01T00:00:00Z", + "evaluationTime": 0 + }], + "interval": 60, + "lastEvaluation": "0001-01-01T00:00:00Z", + "evaluationTime": 0 + }] + } +}`, string(b)) + } +}