grafana/pkg/tests/api/alerting/api_namespace_test.go

482 lines
17 KiB
Go
Raw Normal View History

package alerting
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/configprovider"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util/testutil"
)
func TestIntegration_NamespacingForRules(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
store, cfg := env.SQLStore, env.Cfg
orgID := int64(1)
createUser(t, store, cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
OrgID: orgID,
Password: "editor",
Login: "editor",
})
createUser(t, store, cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
OrgID: orgID,
Password: "viewer",
Login: "viewer",
})
adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor")
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "viewer")
// create test folders, with a rule in each folder
folder1UID := "test-folder-1"
folder2UID := "test-folder-2"
folder3UID := "test-folder-3"
adminClient.CreateFolder(t, folder1UID, "Test Folder 1")
adminClient.CreateFolder(t, folder2UID, "Test Folder 2")
adminClient.CreateFolder(t, folder3UID, "Test Folder 3")
rule1 := createTestAlertRule("Test Rule 1", folder1UID)
rule2 := createTestAlertRule("Test Rule 2", folder2UID)
rule3 := createTestAlertRule("Test Rule 3", folder3UID)
group1 := apimodels.PostableRuleGroupConfig{
Name: "test-group-1",
Interval: 60,
Rules: []apimodels.PostableExtendedRuleNode{rule1},
}
group2 := apimodels.PostableRuleGroupConfig{
Name: "test-group-2",
Interval: 60,
Rules: []apimodels.PostableExtendedRuleNode{rule2},
}
group3 := apimodels.PostableRuleGroupConfig{
Name: "test-group-3",
Interval: 60,
Rules: []apimodels.PostableExtendedRuleNode{rule3},
}
adminClient.PostRulesGroup(t, folder1UID, &group1, false)
adminClient.PostRulesGroup(t, folder2UID, &group2, false)
adminClient.PostRulesGroup(t, folder3UID, &group3, false)
t.Run("admin, editor, and viewer should be able to see all rules in a given folder", func(t *testing.T) {
// admin
rules, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
require.Equal(t, http.StatusAccepted, status)
require.Len(t, rules, 1)
// editor
rules, status, _ = editorClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
require.Equal(t, http.StatusAccepted, status)
require.Len(t, rules, 1)
// viewer
rules, status, _ = viewerClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
require.Equal(t, http.StatusAccepted, status)
require.Len(t, rules, 1)
})
t.Run("admin should be able to access restricted folder, but no one else can", func(t *testing.T) {
restrictedFolderUID := "restricted-folder"
adminClient.CreateFolder(t, restrictedFolderUID, "Restricted Folder")
restrictedRule := createTestAlertRule("Restricted Rule", restrictedFolderUID)
restrictedGroup := apimodels.PostableRuleGroupConfig{
Name: "restricted-group",
Interval: 60,
Rules: []apimodels.PostableExtendedRuleNode{restrictedRule},
}
adminClient.PostRulesGroup(t, restrictedFolderUID, &restrictedGroup, false)
setFolderPermissions(t, grafanaListedAddr, restrictedFolderUID, []map[string]interface{}{
{
"userId": 1,
"permission": 4,
},
})
// admin ok
_, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
require.Equal(t, http.StatusAccepted, status)
// editor and viewer forbidden
_, status, _ = editorClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
require.Equal(t, http.StatusForbidden, status)
_, status, _ = viewerClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
require.Equal(t, http.StatusForbidden, status)
})
t.Run("errors when a folder does not exist", func(t *testing.T) {
_, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, "non-existent-folder")
// even if a folder does not exist, it will return a forbidden error (so users cannot enumerate folders)
require.Equal(t, http.StatusForbidden, status)
})
t.Run("permissions are respected for nested folders", func(t *testing.T) {
parentFolderUID := "parent-folder"
childFolderUID := "child-folder"
adminClient.CreateFolder(t, parentFolderUID, "Parent Folder")
adminClient.CreateFolder(t, childFolderUID, "Child Folder", parentFolderUID)
parentRule := createTestAlertRule("Parent Rule", parentFolderUID)
parentGroup := apimodels.PostableRuleGroupConfig{
Name: "parent-group",
Interval: 60,
Rules: []apimodels.PostableExtendedRuleNode{parentRule},
}
adminClient.PostRulesGroup(t, parentFolderUID, &parentGroup, false)
childRule := createTestAlertRule("Child Rule", childFolderUID)
childGroup := apimodels.PostableRuleGroupConfig{
Name: "child-group",
Interval: 60,
Rules: []apimodels.PostableExtendedRuleNode{childRule},
}
adminClient.PostRulesGroup(t, childFolderUID, &childGroup, false)
// allow admin to access parent folder
setFolderPermissions(t, grafanaListedAddr, parentFolderUID, []map[string]interface{}{
{
"userId": 1,
"permission": 4,
},
})
// admin can get both folders
allRules, status, _ := adminClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.Contains(t, allRules, "Parent Folder")
require.Contains(t, allRules, "Parent Folder/Child Folder")
// editor cannot access either
allRules, status, _ = editorClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.NotContains(t, allRules, "Parent Folder")
require.NotContains(t, allRules, "Parent Folder/Child Folder")
// viewer cannot access either folder
allRules, status, _ = viewerClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.NotContains(t, allRules, "Parent Folder")
require.NotContains(t, allRules, "Parent Folder/Child Folder")
})
t.Run("org separation", func(t *testing.T) {
cfgProvider, err := configprovider.ProvideService(cfg)
require.NoError(t, err)
orgService, err := orgimpl.ProvideService(store, cfg, quotaimpl.ProvideService(context.Background(), store, cfgProvider))
require.NoError(t, err)
newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "Test Org 2"})
require.NoError(t, err)
createUser(t, store, cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
OrgID: newOrg.ID,
Password: "other-admin",
Login: "other-admin-folder-perms",
})
otherOrgFolderUID := "other-org-folder"
otherOrgClient := newAlertingApiClient(grafanaListedAddr, "other-admin-folder-perms", "other-admin")
otherOrgClient.CreateFolder(t, otherOrgFolderUID, "Other Org Folder")
otherOrgRule := createTestAlertRule("Other Org Rule", otherOrgFolderUID)
otherOrgGroup := apimodels.PostableRuleGroupConfig{
Name: "other-org-group",
Interval: 60,
Rules: []apimodels.PostableExtendedRuleNode{otherOrgRule},
}
otherOrgClient.PostRulesGroup(t, otherOrgFolderUID, &otherOrgGroup, false)
// admin from org 1 cannot access org 2 alert rules
allRules, status, _ := adminClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.NotContains(t, allRules, otherOrgFolderUID)
// admin from org 2 cannot access org 1 alert rules
allRules, status, _ = otherOrgClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.NotContains(t, allRules, folder1UID)
require.NotContains(t, allRules, folder2UID)
require.NotContains(t, allRules, folder3UID)
})
}
func TestIntegration_NamespacingForPrometheusRules(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
store, cfg := env.SQLStore, env.Cfg
orgID := int64(1)
createUser(t, store, cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
OrgID: orgID,
Password: "editor",
Login: "editor",
})
createUser(t, store, cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
OrgID: orgID,
Password: "viewer",
Login: "viewer",
})
adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor")
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "viewer")
// create prometheus rules in 3 separate folders
ds := adminClient.CreateDatasource(t, "prometheus")
dsUID := ds.Body.Datasource.UID
folder1UID := "prometheus-folder-1"
folder2UID := "prometheus-folder-2"
folder3UID := "prometheus-folder-3"
adminClient.CreateFolder(t, folder1UID, "Prometheus Folder 1")
adminClient.CreateFolder(t, folder2UID, "Prometheus Folder 2")
adminClient.CreateFolder(t, folder3UID, "Prometheus Folder 3")
duration1m := model.Duration(1 * 60 * 1000)
prometheusRules1 := map[string][]apimodels.PrometheusRuleGroup{
"Prometheus Folder 1": {
{
Name: "test-group-1",
Rules: []apimodels.PrometheusRule{
{
Alert: "HighCPUUsage",
Expr: "cpu_usage > 80",
For: &duration1m,
Labels: map[string]string{
"severity": "warning",
},
Annotations: map[string]string{
"summary": "High CPU usage detected",
},
},
},
},
},
}
prometheusRules2 := map[string][]apimodels.PrometheusRuleGroup{
"Prometheus Folder 2": {
{
Name: "test-group-2",
Rules: []apimodels.PrometheusRule{
{
Alert: "HighMemoryUsage",
Expr: "memory_usage > 90",
For: &duration1m,
Labels: map[string]string{
"severity": "critical",
},
Annotations: map[string]string{
"summary": "High memory usage detected",
},
},
},
},
},
}
prometheusRules3 := map[string][]apimodels.PrometheusRuleGroup{
"Prometheus Folder 3": {
{
Name: "test-group-3",
Rules: []apimodels.PrometheusRule{
{
Alert: "DiskSpaceLow",
Expr: "disk_usage > 95",
For: &duration1m,
Labels: map[string]string{
"severity": "warning",
},
Annotations: map[string]string{
"summary": "Disk space is running low",
},
},
},
},
},
}
// then import them
headers := map[string]string{
"Content-Type": "application/json",
"X-Datasource-UID": dsUID,
}
adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules1, headers)
adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules2, headers)
adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules3, headers)
t.Run("admin, editor, and viewer should be able to get all Prometheus rules", func(t *testing.T) {
// admin
rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
require.Len(t, rules, 3)
require.Contains(t, rules, "Prometheus Folder 1")
require.Contains(t, rules, "Prometheus Folder 2")
require.Contains(t, rules, "Prometheus Folder 3")
// editor
rules = editorClient.ConvertPrometheusGetAllRules(t, headers)
require.Len(t, rules, 3)
require.Contains(t, rules, "Prometheus Folder 1")
require.Contains(t, rules, "Prometheus Folder 2")
require.Contains(t, rules, "Prometheus Folder 3")
// viewer
rules = viewerClient.ConvertPrometheusGetAllRules(t, headers)
require.Len(t, rules, 3)
require.Contains(t, rules, "Prometheus Folder 1")
require.Contains(t, rules, "Prometheus Folder 2")
require.Contains(t, rules, "Prometheus Folder 3")
})
t.Run("only admin can view restricted folder", func(t *testing.T) {
restrictedFolderUID := "restricted-prometheus-folder"
adminClient.CreateFolder(t, restrictedFolderUID, "Restricted Prometheus Folder")
restrictedPrometheusRules := map[string][]apimodels.PrometheusRuleGroup{
"Restricted Prometheus Folder": {
{
Name: "restricted-group",
Rules: []apimodels.PrometheusRule{
{
Alert: "RestrictedAlert",
Expr: "restricted_metric > 100",
For: &duration1m,
Labels: map[string]string{
"severity": "critical",
},
},
},
},
},
}
adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, restrictedPrometheusRules, headers)
setFolderPermissions(t, grafanaListedAddr, restrictedFolderUID, []map[string]interface{}{
{
"userId": 1,
"permission": 4,
},
})
// admin can see the restricted folder
rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
require.Contains(t, rules, "Restricted Prometheus Folder")
// editor and viewer cannot
rules = editorClient.ConvertPrometheusGetAllRules(t, headers)
require.NotContains(t, rules, "Restricted Prometheus Folder")
rules = viewerClient.ConvertPrometheusGetAllRules(t, headers)
require.NotContains(t, rules, "Restricted Prometheus Folder")
})
t.Run("should maintain org separation for Prometheus rules", func(t *testing.T) {
cfgProvider, err := configprovider.ProvideService(cfg)
require.NoError(t, err)
orgService, err := orgimpl.ProvideService(store, cfg, quotaimpl.ProvideService(context.Background(), store, cfgProvider))
require.NoError(t, err)
newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "Prometheus Test Org 2"})
require.NoError(t, err)
createUser(t, store, cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
OrgID: newOrg.ID,
Password: "other-prometheus-admin",
Login: "other-prometheus-admin",
})
otherOrgClient := newAlertingApiClient(grafanaListedAddr, "other-prometheus-admin", "other-prometheus-admin")
otherOrgDs := otherOrgClient.CreateDatasource(t, "prometheus")
otherOrgDsUID := otherOrgDs.Body.Datasource.UID
otherOrgFolderUID := "other-org-prometheus-folder"
otherOrgClient.CreateFolder(t, otherOrgFolderUID, "Other Org Prometheus Folder")
otherOrgPrometheusRules := map[string][]apimodels.PrometheusRuleGroup{
"Other Org Prometheus Folder": {
{
Name: "other-org-group",
Rules: []apimodels.PrometheusRule{
{
Alert: "OtherOrgAlert",
Expr: "other_org_metric > 75",
For: &duration1m,
},
},
},
},
}
otherOrgHeaders := map[string]string{
"Content-Type": "application/json",
"X-Datasource-UID": otherOrgDsUID,
}
otherOrgClient.ConvertPrometheusPostRuleGroups(t, otherOrgDsUID, otherOrgPrometheusRules, otherOrgHeaders)
// admin from org 1 cannot see org 2 rules
rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
require.NotContains(t, rules, "Other Org Prometheus Folder")
// admin from org 2 cannot see org 1 rules
rules = otherOrgClient.ConvertPrometheusGetAllRules(t, otherOrgHeaders)
require.NotContains(t, rules, "Prometheus Folder 1")
require.NotContains(t, rules, "Prometheus Folder 2")
require.NotContains(t, rules, "Prometheus Folder 3")
require.Contains(t, rules, "Other Org Prometheus Folder")
})
}
func createTestAlertRule(title, folderUID string) apimodels.PostableExtendedRuleNode {
return apimodels.PostableExtendedRuleNode{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: title,
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: 600,
To: 0,
},
DatasourceUID: "-100",
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
NoDataState: "NoData",
ExecErrState: "Error",
},
}
}
func setFolderPermissions(t *testing.T, grafanaListedAddr string, folderUID string, permissions []map[string]interface{}) {
t.Helper()
permissionPayload := map[string]interface{}{
"items": permissions,
}
payloadBytes, err := json.Marshal(permissionPayload)
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID)
resp, err := http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}