grafana/pkg/tests/api/alerting/api_prometheus_test.go
Ieva 105313f5c2
RBAC: Adding action set resolver for RBAC evaluation (#86801)
* add action set resolver

* rename variables

* some fixes and some tests

* more tests

* more tests, and put action set storing behind a feature toggle

* undo change from cfg to feature mgmt - will cover it in a separate PR due to the amount of test changes

* fix dependency cycle, update some tests

* add one more test

* fix for feature toggle check not being set on test configs

* linting fixes

* check that action set name can be split nicely

* clean up tests by turning GetActionSetNames into a function

* undo accidental change

* test fix

* more test fixes
2024-05-09 10:18:03 +01:00

818 lines
24 KiB
Go

package alerting
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"testing"
"time"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
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/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
func TestIntegrationPrometheusRules(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create a user to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "default", "default")
interval, err := model.ParseDuration("10s")
require.NoError(t, err)
// an unauthenticated request to get rules should fail
{
promRulesURL := fmt.Sprintf("http://%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)
})
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
// When we have no alerting rules, it returns an empty list.
{
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 := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, `{"status": "success", "data": {"groups": []}}`, string(b))
}
// Now, let's create some rules
{
rules := apimodels.PostableRuleGroupConfig{
Name: "arulegroup",
Rules: []apimodels.PostableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotation1": "val1"},
},
// this rule does not explicitly set no data and error states
// therefore it should get the default values
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiring",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiringButSilenced",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
},
},
},
}
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err := enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
var respModel apimodels.UpdateRuleGroupResponse
require.NoError(t, json.Unmarshal(b, &respModel))
require.Len(t, respModel.Created, len(rules.Rules))
}
// Check that we cannot create a rule that has a panel_id and no dashboard_uid
{
rules := apimodels.PostableRuleGroupConfig{
Name: "anotherrulegroup",
Rules: []apimodels.PostableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
Labels: map[string]string{},
Annotations: map[string]string{"__panelId__": "1"},
},
// this rule does not explicitly set no data and error states
// therefore it should get the default values
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "NeverCreated",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
},
}
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err := enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
var res map[string]any
require.NoError(t, json.Unmarshal(b, &res))
require.Equal(t, "invalid rule specification at index [0]: both annotations __dashboardUid__ and __panelId__ must be specified", res["message"])
}
// 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 := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, `
{
"status": "success",
"data": {
"groups": [{
"name": "arulegroup",
"file": "default",
"rules": [{
"state": "inactive",
"name": "AlwaysFiring",
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
"duration": 10,
"annotations": {
"annotation1": "val1"
},
"labels": {
"label1": "val1"
},
"health": "ok",
"type": "alerting",
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}, {
"state": "inactive",
"name": "AlwaysFiringButSilenced",
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
"health": "ok",
"type": "alerting",
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}],
"totals": {
"inactive": 2
},
"interval": 60,
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}],
"totals": {
"inactive": 2
}
}
}`, string(b))
}
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec
require.Eventually(t, func() bool {
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, `
{
"status": "success",
"data": {
"groups": [{
"name": "arulegroup",
"file": "default",
"rules": [{
"state": "inactive",
"name": "AlwaysFiring",
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
"duration": 10,
"annotations": {
"annotation1": "val1"
},
"labels": {
"label1": "val1"
},
"health": "ok",
"type": "alerting",
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}, {
"state": "inactive",
"name": "AlwaysFiringButSilenced",
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
"health": "ok",
"type": "alerting",
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}],
"totals": {
"inactive": 2
},
"interval": 60,
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}],
"totals": {
"inactive": 2
}
}
}`, string(b))
return true
}, 18*time.Second, 2*time.Second)
}
}
func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create a user to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// Create the namespace we'll save our alerts to.
dashboardUID := "default"
apiClient.CreateFolder(t, dashboardUID, dashboardUID)
interval, err := model.ParseDuration("10s")
require.NoError(t, err)
// Now, let's create some rules
{
rules := apimodels.PostableRuleGroupConfig{
Name: "anotherrulegroup",
Rules: []apimodels.PostableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
Labels: map[string]string{},
Annotations: map[string]string{
"__dashboardUid__": dashboardUID,
"__panelId__": "1",
},
},
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiring",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiringButSilenced",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
},
},
},
}
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err := enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
var respModel apimodels.UpdateRuleGroupResponse
require.NoError(t, json.Unmarshal(b, &respModel))
require.Len(t, respModel.Created, len(rules.Rules))
}
expectedAllJSON := fmt.Sprintf(`
{
"status": "success",
"data": {
"groups": [{
"name": "anotherrulegroup",
"file": "default",
"rules": [{
"state": "inactive",
"name": "AlwaysFiring",
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
"duration": 10,
"annotations": {
"__dashboardUid__": "%s",
"__panelId__": "1"
},
"health": "ok",
"type": "alerting",
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}, {
"state": "inactive",
"name": "AlwaysFiringButSilenced",
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
"health": "ok",
"type": "alerting",
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}],
"totals": {
"inactive": 2
},
"interval": 60,
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}],
"totals": {
"inactive": 2
}
}
}`, dashboardUID)
expectedFilteredByJSON := fmt.Sprintf(`
{
"status": "success",
"data": {
"groups": [{
"name": "anotherrulegroup",
"file": "default",
"rules": [{
"state": "inactive",
"name": "AlwaysFiring",
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
"duration": 10,
"annotations": {
"__dashboardUid__": "%s",
"__panelId__": "1"
},
"health": "ok",
"type": "alerting",
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}],
"totals": {
"inactive": 1
},
"interval": 60,
"lastEvaluation": "0001-01-01T00:00:00Z",
"evaluationTime": 0
}],
"totals": {
"inactive": 1
}
}
}`, dashboardUID)
expectedNoneJSON := `
{
"status": "success",
"data": {
"groups": []
}
}`
// 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 := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, expectedAllJSON, string(b))
}
// Now, let's check we get the same rule when filtering by dashboard_uid
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s", grafanaListedAddr, dashboardUID)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, expectedFilteredByJSON, string(b))
}
// Now, let's check we get no rules when filtering by an unknown dashboard_uid
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s", grafanaListedAddr, "abc")
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, expectedNoneJSON, string(b))
}
// Now, let's check we get the same rule when filtering by dashboard_uid and panel_id
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s&panel_id=1", grafanaListedAddr, dashboardUID)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, expectedFilteredByJSON, string(b))
}
// Now, let's check we get no rules when filtering by dashboard_uid and unknown panel_id
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s&panel_id=2", grafanaListedAddr, dashboardUID)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, expectedNoneJSON, string(b))
}
// Now, let's check an invalid panel_id returns a 400 Bad Request response
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s&panel_id=invalid", grafanaListedAddr, dashboardUID)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var res map[string]any
require.NoError(t, json.Unmarshal(b, &res))
// These APIs return Prometheus-like errors.
require.Equal(t, "error", res["status"])
require.Equal(t, "bad_data", res["errorType"])
require.Equal(t, `invalid panel_id: strconv.ParseInt: parsing "invalid": invalid syntax`, res["error"])
}
// Now, let's check a panel_id without dashboard_uid returns a 400 Bad Request response
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?panel_id=1", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var res map[string]any
require.NoError(t, json.Unmarshal(b, &res))
// These APIs return Prometheus-like errors.
require.Equal(t, "error", res["status"])
require.Equal(t, "bad_data", res["errorType"])
require.Equal(t, "panel_id must be set with dashboard_uid", res["error"])
}
}
func TestIntegrationPrometheusRulesPermissions(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create a user to make authenticated requests
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// access control permissions store
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder2", "folder2")
// Create rule under folder1
createRule(t, apiClient, "folder1")
// Create rule under folder2
createRule(t, apiClient, "folder2")
// 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 := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
body := asJson(t, b)
// Sort, for test consistency.
sort.Slice(body.Data.Groups, func(i, j int) bool { return body.Data.Groups[i].File < body.Data.Groups[j].File })
require.Equal(t, "success", body.Status)
// The request should see both groups, and all rules underneath.
require.Len(t, body.Data.Groups, 2)
require.Len(t, body.Data.Groups[0].Rules, 1)
require.Len(t, body.Data.Groups[1].Rules, 1)
require.Equal(t, "folder1", body.Data.Groups[0].File)
require.Equal(t, "folder2", body.Data.Groups[1].File)
}
// remove permissions from folder2org.ROLE
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder2")
apiClient.ReloadCachedPermissions(t)
// 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 := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
body := asJson(t, b)
require.Equal(t, "success", body.Status)
require.Len(t, body.Data.Groups, 1)
require.Len(t, body.Data.Groups[0].Rules, 1)
require.Equal(t, "folder1", body.Data.Groups[0].File)
}
// remove permissions from folder1org.ROLE
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder1")
apiClient.ReloadCachedPermissions(t)
// make sure that no folders are 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 := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, `
{
"status": "success",
"data": {
"groups": []
}
}`, string(b))
}
}
func removeFolderPermission(t *testing.T, store resourcepermissions.Store, orgID, userID int64, role org.RoleType, uid string) {
t.Helper()
// remove user permissions on folder
_, _ = store.SetUserResourcePermission(context.Background(), orgID, accesscontrol.User{ID: userID}, resourcepermissions.SetResourcePermissionCommand{
Resource: "folders",
ResourceID: uid,
ResourceAttribute: "uid",
}, nil)
// remove org role permissions from folder
_, _ = store.SetBuiltInResourcePermission(context.Background(), orgID, string(role), resourcepermissions.SetResourcePermissionCommand{
Resource: "folders",
ResourceID: uid,
ResourceAttribute: "uid",
}, nil)
// remove org role children permissions from folder
for _, c := range role.Children() {
_, _ = store.SetBuiltInResourcePermission(context.Background(), orgID, string(c), resourcepermissions.SetResourcePermissionCommand{
Resource: "folders",
ResourceID: uid,
ResourceAttribute: "uid",
}, nil)
}
}
func asJson(t *testing.T, blob []byte) rulesResponse {
t.Helper()
var r rulesResponse
require.NoError(t, json.Unmarshal(blob, &r))
return r
}
type rulesResponse struct {
Status string
Data rulesData
}
type rulesData struct {
Groups []groupData
}
type groupData struct {
Name string
File string
Rules []any
}