grafana/pkg/tests/api/alerting/api_testing_test.go
Ieva cfa1a2c55f
RBAC: Split non-empty scopes into kind, attribute and identifier fields for better search performance (#71933)
* add a feature toggle

* add the fields for attribute, kind and identifier to permission

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* set the new fields when new permissions are stored

* add migrations

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* remove comments

* Update pkg/services/accesscontrol/migrator/migrator.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* feedback: put column migrations behind the feature toggle, added an index, changed how wildcard scopes are split

* PR feedback: add a comment and revert an accidentally changed file

* PR feedback: handle the case with : in resource identifier

* switch from checking feature toggle through cfg to checking it through featuremgmt

* don't put the column migrations behind a feature toggle after all - this breaks permission queries from db

---------

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
2023-07-21 15:23:01 +01:00

412 lines
14 KiB
Go

package alerting
import (
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
alertingModels "github.com/grafana/alerting/models"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/common/model"
"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/datasources"
"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/setting"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util"
)
const (
TESTDATA_UID = "testdata"
)
func TestGrafanaRuleConfig(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{},
EnableLog: false,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
userId := createUser(t, env.SQLStore, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
apiCli := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
dsCmd := &datasources.AddDataSourceCommand{
Name: "TestDatasource",
Type: "testdata",
Access: datasources.DS_ACCESS_PROXY,
UID: TESTDATA_UID,
UserID: userId,
OrgID: 1,
}
_, err := env.Server.HTTPServer.DataSourcesService.AddDataSource(context.Background(), dsCmd)
require.NoError(t, err)
dynamicLabels := []string{"GA", "FL", "AL", "AZ"}
dynamicLabelsJson, _ := json.Marshal(&dynamicLabels)
testdataQueryModel := json.RawMessage(fmt.Sprintf(`{
"refId": "A",
"hide": false,
"scenarioId": "usa",
"usa": {
"mode": "timeseries",
"period": "1m",
"states": %s,
"fields": [
"baz"
]
}
}`, string(dynamicLabelsJson)))
genRule := func(ruleGen func() apimodels.PostableExtendedRuleNode) apimodels.PostableExtendedRuleNodeExtended {
return apimodels.PostableExtendedRuleNodeExtended{
Rule: ruleGen(),
NamespaceUID: "NamespaceUID",
NamespaceTitle: "NamespaceTitle",
}
}
t.Run("valid rule should accept request", func(t *testing.T) {
status, body := apiCli.SubmitRuleForTesting(t, genRule(alertRuleGen()))
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
})
t.Run("valid rule should return alerts in response", func(t *testing.T) {
status, body := apiCli.SubmitRuleForTesting(t, genRule(alertRuleGen()))
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 1)
})
t.Run("valid rule should return static annotations", func(t *testing.T) {
rule := genRule(testdataRule(testdataQueryModel, nil, nil))
rule.Rule.Annotations = map[string]string{
"foo": "bar",
"foo2": "bar2",
}
status, body := apiCli.SubmitRuleForTesting(t, rule)
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 4)
for _, alert := range result {
require.Equal(t, "bar", alert.Annotations["foo"])
require.Equal(t, "bar2", alert.Annotations["foo2"])
}
})
t.Run("valid rule should return static labels", func(t *testing.T) {
rule := genRule(testdataRule(testdataQueryModel, nil, nil))
rule.Rule.Labels = map[string]string{
"foo": "bar",
"foo2": "bar2",
}
status, body := apiCli.SubmitRuleForTesting(t, rule)
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 4)
for _, alert := range result {
require.Equal(t, "bar", alert.Labels["foo"])
require.Equal(t, "bar2", alert.Labels["foo2"])
}
})
t.Run("valid rule should return interpolated annotations", func(t *testing.T) {
rule := genRule(testdataRule(testdataQueryModel, nil, nil))
rule.Rule.Annotations = map[string]string{
"value": "{{ $value }}",
"values.B": "{{ $values.B }}",
"values.C": "{{ $values.C }}",
}
status, body := apiCli.SubmitRuleForTesting(t, rule)
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 4)
for i, alert := range result {
require.NotEmpty(t, alert.Annotations["values.B"])
require.NotEmpty(t, alert.Annotations["values.C"])
valueB := fmt.Sprintf("[ var='B' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Annotations["values.B"])
valueC := fmt.Sprintf("[ var='C' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Annotations["values.C"])
require.Contains(t, alert.Annotations["value"], valueB)
require.Contains(t, alert.Annotations["value"], valueC)
}
})
t.Run("valid rule should return interpolated labels", func(t *testing.T) {
rule := genRule(testdataRule(testdataQueryModel, nil, nil))
rule.Rule.Labels = map[string]string{
"value": "{{ $value }}",
"values.B": "{{ $values.B }}",
"values.C": "{{ $values.C }}",
}
status, body := apiCli.SubmitRuleForTesting(t, rule)
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 4)
for i, alert := range result {
require.NotEmpty(t, alert.Labels["values.B"])
require.NotEmpty(t, alert.Labels["values.C"])
valueB := fmt.Sprintf("[ var='B' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Labels["values.B"])
valueC := fmt.Sprintf("[ var='C' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Labels["values.C"])
require.Contains(t, alert.Labels["value"], valueB)
require.Contains(t, alert.Labels["value"], valueC)
}
})
t.Run("valid rule should use functions with annotations", func(t *testing.T) {
rule := genRule(testdataRule(testdataQueryModel, nil, nil))
rule.Rule.Annotations = map[string]string{
"externalURL": "{{ externalURL }}",
"humanize": "{{ humanize 1000.0 }}",
}
status, body := apiCli.SubmitRuleForTesting(t, rule)
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 4)
for _, alert := range result {
require.Equal(t, "http://localhost:3000/", alert.Annotations["externalURL"])
require.Equal(t, "1k", alert.Annotations["humanize"])
}
})
t.Run("valid rule should use functions with labels", func(t *testing.T) {
rule := genRule(testdataRule(testdataQueryModel, nil, nil))
rule.Rule.Labels = map[string]string{
"externalURL": "{{ externalURL }}",
"humanize": "{{ humanize 1000.0 }}",
}
status, body := apiCli.SubmitRuleForTesting(t, rule)
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 4)
for _, alert := range result {
require.Equal(t, "http://localhost:3000/", alert.Labels["externalURL"])
require.Equal(t, "1k", alert.Labels["humanize"])
}
})
t.Run("valid rule should return dynamic labels", func(t *testing.T) {
rule := genRule(testdataRule(testdataQueryModel, nil, nil))
status, body := apiCli.SubmitRuleForTesting(t, rule)
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 4)
for i, alert := range result {
require.Equal(t, dynamicLabels[i], alert.Labels["state"])
}
})
t.Run("valid rule should return built-in labels", func(t *testing.T) {
rule := genRule(testdataRule(testdataQueryModel, nil, nil))
status, body := apiCli.SubmitRuleForTesting(t, rule)
require.Equal(t, http.StatusOK, status)
var result []amv2.PostableAlert
require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame")
require.Len(t, result, 4)
for _, alert := range result {
require.Equal(t, rule.Rule.GrafanaManagedAlert.Title, alert.Labels[model.AlertNameLabel])
require.Equal(t, rule.NamespaceUID, alert.Labels[alertingModels.NamespaceUIDLabel])
require.Equal(t, rule.NamespaceTitle, alert.Labels[ngmodels.FolderTitleLabel])
}
})
t.Run("invalid rule should reject request", func(t *testing.T) {
req := genRule(alertRuleGen())
req.Rule = apimodels.PostableExtendedRuleNode{}
status, _ := apiCli.SubmitRuleForTesting(t, req)
require.Equal(t, http.StatusBadRequest, status)
})
t.Run("authentication permissions", func(t *testing.T) {
if !setting.IsEnterprise {
t.Skip("Enterprise-only test")
}
t.Skip("flakey tests - skipping") //TODO: Fix tests and remove skip.
testUserId := createUser(t, env.SQLStore, user.CreateUserCommand{
DefaultOrgRole: "DOESNOTEXIST", // Needed so that the SignedInUser has OrgId=1. Otherwise, datasource will not be found.
Password: "test",
Login: "test",
})
testUserApiCli := newAlertingApiClient(grafanaListedAddr, "test", "test")
t.Run("fail if can't read rules", func(t *testing.T) {
status, body := testUserApiCli.SubmitRuleForTesting(t, genRule(testdataRule(testdataQueryModel, nil, nil)))
require.Contains(t, body, accesscontrol.ActionAlertingRuleRead)
require.Equalf(t, http.StatusForbidden, status, "Response: %s", body)
})
// access control permissions store
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
accesscontrol.GlobalOrgID,
accesscontrol.User{ID: testUserId},
resourcepermissions.SetResourcePermissionCommand{
Actions: []string{
accesscontrol.ActionAlertingRuleRead,
},
Resource: "folders",
ResourceID: "*",
ResourceAttribute: "uid",
}, nil)
require.NoError(t, err)
testUserApiCli.ReloadCachedPermissions(t)
t.Run("fail if can't query data sources", func(t *testing.T) {
status, body := testUserApiCli.SubmitRuleForTesting(t, genRule(testdataRule(testdataQueryModel, nil, nil)))
require.Contains(t, body, "user is not authorized to query one or many data sources used by the rule")
require.Equalf(t, http.StatusUnauthorized, status, "Response: %s", body)
})
_, err = permissionsStore.SetUserResourcePermission(context.Background(),
accesscontrol.GlobalOrgID,
accesscontrol.User{ID: testUserId},
resourcepermissions.SetResourcePermissionCommand{
Actions: []string{
datasources.ActionQuery,
},
Resource: "datasources",
ResourceID: TESTDATA_UID,
ResourceAttribute: "uid",
}, nil)
require.NoError(t, err)
testUserApiCli.ReloadCachedPermissions(t)
t.Run("succeed if can query data sources", func(t *testing.T) {
status, body := testUserApiCli.SubmitRuleForTesting(t, genRule(testdataRule(testdataQueryModel, nil, nil)))
require.Equalf(t, http.StatusOK, status, "Response: %s", body)
})
})
}
func testdataRule(queryModel json.RawMessage, labels map[string]string, annotations map[string]string) func() apimodels.PostableExtendedRuleNode {
return func() apimodels.PostableExtendedRuleNode {
forDuration := model.Duration(10 * time.Second)
return apimodels.PostableExtendedRuleNode{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &forDuration,
Labels: labels,
Annotations: annotations,
},
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: fmt.Sprintf("rule-%s", util.GenerateShortUID()),
Condition: "C",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{From: 600, To: 0},
DatasourceUID: TESTDATA_UID,
Model: queryModel,
},
{ // Simple reduce last A.
RefID: "B",
RelativeTimeRange: apimodels.RelativeTimeRange{From: 0, To: 0},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"refId": "B",
"hide": false,
"type": "reduce",
"datasource": {
"uid": "__expr__",
"type": "__expr__"
},
"conditions": [
{
"type": "query",
"evaluator": {
"params": [],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"B"
]
},
"reducer": {
"params": [],
"type": "last"
}
}
],
"reducer": "last",
"expression": "A"
}`),
},
{ // Threshold B > 0.
RefID: "C",
RelativeTimeRange: apimodels.RelativeTimeRange{From: 0, To: 0},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"refId": "C",
"hide": false,
"type": "threshold",
"datasource": {
"uid": "__expr__",
"type": "__expr__"
},
"conditions": [
{
"type": "query",
"evaluator": {
"params": [
0
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"C"
]
},
"reducer": {
"params": [],
"type": "last"
}
}
],
"expression": "B"
}`),
},
},
},
}
}
}