mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
* 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
422 lines
16 KiB
Go
422 lines
16 KiB
Go
package alerting
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/prometheus/alertmanager/pkg/labels"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/alerting/models"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
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/ngalert/notifier"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
func TestIntegrationSilenceAuth(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)
|
|
|
|
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
|
DefaultOrgRole: string(org.RoleAdmin),
|
|
Password: "admin",
|
|
Login: "admin",
|
|
})
|
|
|
|
adminApiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
f1 := folder.Folder{
|
|
UID: util.GenerateShortUID(),
|
|
Title: "Folder 1",
|
|
}
|
|
adminApiClient.CreateFolder(t, f1.UID, f1.Title)
|
|
f2 := folder.Folder{
|
|
UID: util.GenerateShortUID(),
|
|
Title: "Folder 2",
|
|
}
|
|
adminApiClient.CreateFolder(t, f2.UID, f2.Title)
|
|
|
|
group1 := generateAlertRuleGroup(1, alertRuleGen())
|
|
group2 := generateAlertRuleGroup(1, alertRuleGen())
|
|
|
|
respModel, status, _ := adminApiClient.PostRulesGroupWithStatus(t, f1.UID, &group1)
|
|
require.Equal(t, http.StatusAccepted, status)
|
|
ruleInFolder1UID := respModel.Created[0]
|
|
respModel, status, _ = adminApiClient.PostRulesGroupWithStatus(t, f2.UID, &group2)
|
|
require.Equal(t, http.StatusAccepted, status)
|
|
ruleInFolder2UID := respModel.Created[0]
|
|
|
|
type silenceAction string
|
|
const (
|
|
readSilence silenceAction = "read"
|
|
createSilence silenceAction = "create"
|
|
updateSilence silenceAction = "update"
|
|
deleteSilence silenceAction = "delete"
|
|
)
|
|
|
|
type silenceType string
|
|
const (
|
|
generalSilence silenceType = "generalSilence"
|
|
ruleSilenceInFolder1 silenceType = "ruleSilenceInFolder1"
|
|
ruleSilenceInFolder2 silenceType = "ruleSilenceInFolder2"
|
|
)
|
|
|
|
silenceGens := map[silenceType]func() ngmodels.Silence{
|
|
generalSilence: ngmodels.SilenceGen(),
|
|
ruleSilenceInFolder1: ngmodels.SilenceGen(ngmodels.SilenceMuts.WithMatcher(models.RuleUIDLabel, ruleInFolder1UID, labels.MatchEqual)),
|
|
ruleSilenceInFolder2: ngmodels.SilenceGen(ngmodels.SilenceMuts.WithMatcher(models.RuleUIDLabel, ruleInFolder2UID, labels.MatchEqual)),
|
|
}
|
|
|
|
defaultStatus := map[silenceAction]map[bool]int{
|
|
updateSilence: {true: http.StatusAccepted, false: http.StatusForbidden},
|
|
deleteSilence: {true: http.StatusOK, false: http.StatusForbidden},
|
|
createSilence: {true: http.StatusAccepted, false: http.StatusForbidden},
|
|
readSilence: {true: http.StatusOK, false: http.StatusForbidden},
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
orgRole org.RoleType // default RoleNone
|
|
permissions []resourcepermissions.SetResourcePermissionCommand
|
|
defaultAllowed bool // Default allowed/forbidden for actions not in statusExceptions.
|
|
statusExceptions map[silenceType]map[silenceAction]int // Exceptions to defaultAllowed.
|
|
listContents []silenceType // nil = forbidden.
|
|
}{
|
|
// OSS Builtins
|
|
{
|
|
name: "Viewer permissions",
|
|
orgRole: org.RoleViewer,
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder1: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder2: {readSilence: http.StatusOK},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1, ruleSilenceInFolder2},
|
|
},
|
|
{
|
|
name: "Viewer permissions with elevated access to folder1",
|
|
orgRole: org.RoleViewer,
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: ossaccesscontrol.FolderEditActions, Resource: "folders", ResourceAttribute: "uid", ResourceID: f1.UID},
|
|
},
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder1: {readSilence: http.StatusOK, updateSilence: http.StatusAccepted, createSilence: http.StatusAccepted, deleteSilence: http.StatusOK},
|
|
ruleSilenceInFolder2: {readSilence: http.StatusOK},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1, ruleSilenceInFolder2},
|
|
},
|
|
{
|
|
name: "Editor permissions",
|
|
orgRole: org.RoleEditor,
|
|
defaultAllowed: true,
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1, ruleSilenceInFolder2},
|
|
},
|
|
{
|
|
name: "Admin permissions",
|
|
orgRole: org.RoleAdmin,
|
|
defaultAllowed: true,
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1, ruleSilenceInFolder2},
|
|
},
|
|
// RBAC
|
|
{
|
|
name: "No permissions",
|
|
orgRole: org.RoleNone,
|
|
},
|
|
{
|
|
name: "Global read",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{accesscontrol.ActionAlertingInstanceRead}},
|
|
},
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder1: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder2: {readSilence: http.StatusOK},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1, ruleSilenceInFolder2},
|
|
},
|
|
{
|
|
name: "Global read + create permissions",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{
|
|
accesscontrol.ActionAlertingInstanceRead,
|
|
accesscontrol.ActionAlertingInstanceCreate,
|
|
}},
|
|
},
|
|
defaultAllowed: true,
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {updateSilence: http.StatusForbidden, deleteSilence: http.StatusForbidden},
|
|
ruleSilenceInFolder1: {updateSilence: http.StatusForbidden, deleteSilence: http.StatusForbidden},
|
|
ruleSilenceInFolder2: {updateSilence: http.StatusForbidden, deleteSilence: http.StatusForbidden},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1, ruleSilenceInFolder2},
|
|
},
|
|
{
|
|
name: "Global read + update permissions",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{
|
|
accesscontrol.ActionAlertingInstanceRead,
|
|
accesscontrol.ActionAlertingInstanceUpdate,
|
|
}},
|
|
},
|
|
defaultAllowed: true,
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {createSilence: http.StatusForbidden},
|
|
ruleSilenceInFolder1: {createSilence: http.StatusForbidden},
|
|
ruleSilenceInFolder2: {createSilence: http.StatusForbidden},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1, ruleSilenceInFolder2},
|
|
},
|
|
{
|
|
name: "Global read + update + create permissions",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{
|
|
accesscontrol.ActionAlertingInstanceRead,
|
|
accesscontrol.ActionAlertingInstanceUpdate,
|
|
accesscontrol.ActionAlertingInstanceCreate,
|
|
}},
|
|
},
|
|
defaultAllowed: true,
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1, ruleSilenceInFolder2},
|
|
},
|
|
{
|
|
name: "Global update + create permissions, missing read",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{
|
|
accesscontrol.ActionAlertingInstanceUpdate,
|
|
accesscontrol.ActionAlertingInstanceCreate,
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
name: "Silence read in folder1",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{accesscontrol.ActionAlertingSilencesRead}, Resource: "folders", ResourceAttribute: "uid", ResourceID: f1.UID},
|
|
},
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder1: {readSilence: http.StatusOK},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1},
|
|
},
|
|
{
|
|
name: "Silence read in folder2",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{accesscontrol.ActionAlertingSilencesRead}, Resource: "folders", ResourceAttribute: "uid", ResourceID: f2.UID},
|
|
},
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder2: {readSilence: http.StatusOK},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder2},
|
|
},
|
|
{
|
|
name: "Silence read + create in folder1",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{
|
|
accesscontrol.ActionAlertingSilencesRead,
|
|
accesscontrol.ActionAlertingSilencesCreate,
|
|
}, Resource: "folders", ResourceAttribute: "uid", ResourceID: f1.UID},
|
|
},
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder1: {readSilence: http.StatusOK, createSilence: http.StatusAccepted},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1},
|
|
},
|
|
{
|
|
name: "Silence read + write in folder1",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{
|
|
accesscontrol.ActionAlertingSilencesRead,
|
|
accesscontrol.ActionAlertingSilencesWrite,
|
|
}, Resource: "folders", ResourceAttribute: "uid", ResourceID: f1.UID},
|
|
},
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder1: {readSilence: http.StatusOK, updateSilence: http.StatusAccepted, deleteSilence: http.StatusOK},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1},
|
|
},
|
|
{
|
|
name: "Silence read + write + create in folder1",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{
|
|
accesscontrol.ActionAlertingSilencesRead,
|
|
accesscontrol.ActionAlertingSilencesWrite,
|
|
accesscontrol.ActionAlertingSilencesCreate,
|
|
}, Resource: "folders", ResourceAttribute: "uid", ResourceID: f1.UID},
|
|
},
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
ruleSilenceInFolder1: {readSilence: http.StatusOK, updateSilence: http.StatusAccepted, createSilence: http.StatusAccepted, deleteSilence: http.StatusOK},
|
|
},
|
|
listContents: []silenceType{generalSilence, ruleSilenceInFolder1},
|
|
},
|
|
{
|
|
name: "Silence read + write + create in other folder",
|
|
permissions: []resourcepermissions.SetResourcePermissionCommand{
|
|
{Actions: []string{
|
|
accesscontrol.ActionAlertingSilencesRead,
|
|
accesscontrol.ActionAlertingSilencesWrite,
|
|
accesscontrol.ActionAlertingSilencesCreate,
|
|
}, Resource: "folders", ResourceAttribute: "uid", ResourceID: "unknown"},
|
|
},
|
|
statusExceptions: map[silenceType]map[silenceAction]int{
|
|
generalSilence: {readSilence: http.StatusOK},
|
|
},
|
|
listContents: []silenceType{generalSilence},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
randomLogin := util.GenerateShortUID()
|
|
orgRole := org.RoleNone
|
|
if tt.orgRole != "" {
|
|
orgRole = tt.orgRole
|
|
}
|
|
testUserId := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
|
DefaultOrgRole: string(orgRole),
|
|
Password: user.Password(randomLogin),
|
|
Login: randomLogin,
|
|
})
|
|
|
|
apiClient := newAlertingApiClient(grafanaListedAddr, randomLogin, randomLogin)
|
|
|
|
// Set permissions.
|
|
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
|
|
for _, cmd := range tt.permissions {
|
|
_, err := permissionsStore.SetUserResourcePermission(
|
|
context.Background(),
|
|
1,
|
|
accesscontrol.User{ID: testUserId},
|
|
cmd,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
apiClient.ReloadCachedPermissions(t)
|
|
|
|
expectedStatus := func(sType silenceType, action silenceAction) int {
|
|
expectedStatus, ok := defaultStatus[action][tt.defaultAllowed]
|
|
require.True(t, ok, "No default status for action")
|
|
if st, ok := tt.statusExceptions[sType][action]; ok {
|
|
expectedStatus = st
|
|
}
|
|
return expectedStatus
|
|
}
|
|
|
|
persistSilence := func(gen func() ngmodels.Silence) apimodels.PostableSilence {
|
|
silence := *notifier.SilenceToPostableSilence(gen())
|
|
silence.ID = ""
|
|
okBody, status, _ := adminApiClient.PostSilence(t, silence)
|
|
require.Equal(t, http.StatusAccepted, status)
|
|
require.NotEmpty(t, okBody.SilenceID)
|
|
silence.ID = okBody.SilenceID
|
|
return silence
|
|
}
|
|
|
|
tests := map[silenceAction]func(func() ngmodels.Silence, silenceType) (int, string){
|
|
readSilence: func(gen func() ngmodels.Silence, sType silenceType) (int, string) {
|
|
silence := persistSilence(gen)
|
|
_, status, body := apiClient.GetSilence(t, silence.ID)
|
|
return status, body
|
|
},
|
|
createSilence: func(gen func() ngmodels.Silence, sType silenceType) (int, string) {
|
|
silence := *notifier.SilenceToPostableSilence(gen())
|
|
silence.ID = ""
|
|
_, status, body := apiClient.PostSilence(t, silence)
|
|
return status, body
|
|
},
|
|
updateSilence: func(gen func() ngmodels.Silence, sType silenceType) (int, string) {
|
|
silence := persistSilence(gen)
|
|
_, status, body := apiClient.PostSilence(t, silence)
|
|
return status, body
|
|
},
|
|
deleteSilence: func(gen func() ngmodels.Silence, sType silenceType) (int, string) {
|
|
silence := persistSilence(gen)
|
|
_, status, body := apiClient.DeleteSilence(t, silence.ID)
|
|
return status, body
|
|
},
|
|
}
|
|
|
|
for action, test := range tests {
|
|
t.Run(string(action), func(t *testing.T) {
|
|
for sType, gen := range silenceGens {
|
|
expected := expectedStatus(sType, action)
|
|
t.Run(fmt.Sprintf("Silence: %s, Access: %d", sType, expected), func(t *testing.T) {
|
|
status, body := test(gen, sType)
|
|
t.Log(body)
|
|
require.Equal(t, expected, status)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("List contents", func(t *testing.T) {
|
|
ids := make(map[silenceType]string)
|
|
idToStype := make(map[string]silenceType)
|
|
|
|
// We Create new silences with a unique label. This is both to test the filter param and to
|
|
// simplify the test by having a known set of silences to list.
|
|
filterLabel := util.GenerateShortUID()
|
|
for sType, gen := range silenceGens {
|
|
genWithFilterLabels := func() ngmodels.Silence {
|
|
return ngmodels.CopySilenceWith(gen(), ngmodels.SilenceMuts.WithMatcher(filterLabel, filterLabel, labels.MatchEqual))
|
|
}
|
|
silence := persistSilence(genWithFilterLabels)
|
|
ids[sType] = silence.ID
|
|
idToStype[silence.ID] = sType
|
|
}
|
|
silences, status, body := apiClient.GetSilences(t, fmt.Sprintf("%s=%s", filterLabel, filterLabel))
|
|
t.Log(body)
|
|
if tt.listContents == nil {
|
|
require.Equal(t, http.StatusForbidden, status)
|
|
return
|
|
}
|
|
require.Equal(t, http.StatusOK, status)
|
|
|
|
idsInBody := make(map[string]struct{})
|
|
for _, s := range silences {
|
|
idsInBody[*s.ID] = struct{}{}
|
|
}
|
|
|
|
for _, sType := range tt.listContents {
|
|
id, ok := ids[sType]
|
|
require.True(t, ok)
|
|
assert.Containsf(t, idsInBody, id, "Silence of type %s not found in list", sType)
|
|
}
|
|
|
|
for _, s := range silences {
|
|
sType, ok := idToStype[*s.ID]
|
|
require.True(t, ok, "Unknown listed silence %s", *s.ID)
|
|
assert.Containsf(t, tt.listContents, sType, "Silence of type %s should not be found in list", sType)
|
|
}
|
|
assert.Len(t, silences, len(tt.listContents), "Listed silences count mismatch")
|
|
})
|
|
})
|
|
}
|
|
}
|