mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Move alertmanager api silence code to separate files (#86947)
* Move alertmanager api silence code to separate files unchanged * Replace with silence model instead interface --------- Co-authored-by: Matt Jacobson <matthew.jacobson@grafana.com>
This commit is contained in:
parent
0f98bd3b7b
commit
dff7cb9afb
@ -9,6 +9,7 @@ import (
|
|||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -78,10 +79,6 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Silence interface {
|
|
||||||
GetRuleUID() *string
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleUIDToNamespaceStore interface {
|
type RuleUIDToNamespaceStore interface {
|
||||||
GetNamespacesByRuleUID(ctx context.Context, orgID int64, uids ...string) (map[string]string, error)
|
GetNamespacesByRuleUID(ctx context.Context, orgID int64, uids ...string) (map[string]string, error)
|
||||||
}
|
}
|
||||||
@ -104,7 +101,7 @@ func NewSilenceService(ac ac.AccessControl, store RuleUIDToNamespaceStore) *Sile
|
|||||||
// Global silence (one that is not attached to a particular rule) is considered available to all users.
|
// Global silence (one that is not attached to a particular rule) is considered available to all users.
|
||||||
// For silences that are not attached to a rule, are checked against authorization.
|
// For silences that are not attached to a rule, are checked against authorization.
|
||||||
// This method is more preferred when many silences need to be checked.
|
// This method is more preferred when many silences need to be checked.
|
||||||
func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Requester, silences ...Silence) ([]Silence, error) {
|
func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Requester, silences ...*models.Silence) ([]*models.Silence, error) {
|
||||||
canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator)
|
canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator)
|
||||||
if err != nil || canAll { // return early if user can either read all silences or there is an error
|
if err != nil || canAll { // return early if user can either read all silences or there is an error
|
||||||
return silences, err
|
return silences, err
|
||||||
@ -113,8 +110,8 @@ func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Reques
|
|||||||
if err != nil || !canSome {
|
if err != nil || !canSome {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result := make([]Silence, 0, len(silences))
|
result := make([]*models.Silence, 0, len(silences))
|
||||||
silencesByRuleUID := make(map[string][]Silence, len(silences))
|
silencesByRuleUID := make(map[string][]*models.Silence, len(silences))
|
||||||
for _, silence := range silences {
|
for _, silence := range silences {
|
||||||
ruleUID := silence.GetRuleUID()
|
ruleUID := silence.GetRuleUID()
|
||||||
if ruleUID == nil { // if this is a general silence
|
if ruleUID == nil { // if this is a general silence
|
||||||
@ -154,7 +151,7 @@ func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeReadSilence checks if user has access to read a silence
|
// AuthorizeReadSilence checks if user has access to read a silence
|
||||||
func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity.Requester, silence Silence) error {
|
func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
|
||||||
canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator)
|
canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator)
|
||||||
if canAll || err != nil { // return early if user can either read all silences or there is error
|
if canAll || err != nil { // return early if user can either read all silences or there is error
|
||||||
return err
|
return err
|
||||||
@ -186,7 +183,7 @@ func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeCreateSilence checks if user has access to create a silence. Returns ErrAuthorizationBase if user is not authorized
|
// AuthorizeCreateSilence checks if user has access to create a silence. Returns ErrAuthorizationBase if user is not authorized
|
||||||
func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identity.Requester, silence Silence) error {
|
func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
|
||||||
canAny, err := s.HasAccess(ctx, user, createAnySilenceEvaluator)
|
canAny, err := s.HasAccess(ctx, user, createAnySilenceEvaluator)
|
||||||
if err != nil || canAny {
|
if err != nil || canAny {
|
||||||
// return early if user can either create any silence or there is an error
|
// return early if user can either create any silence or there is an error
|
||||||
@ -215,7 +212,7 @@ func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identit
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeUpdateSilence checks if user has access to update\expire a silence. Returns ErrAuthorizationBase if user is not authorized
|
// AuthorizeUpdateSilence checks if user has access to update\expire a silence. Returns ErrAuthorizationBase if user is not authorized
|
||||||
func (s SilenceService) AuthorizeUpdateSilence(ctx context.Context, user identity.Requester, silence Silence) error {
|
func (s SilenceService) AuthorizeUpdateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
|
||||||
canAny, err := s.HasAccess(ctx, user, updateAnySilenceEvaluator)
|
canAny, err := s.HasAccess(ctx, user, updateAnySilenceEvaluator)
|
||||||
if err != nil || canAny {
|
if err != nil || canAny {
|
||||||
// return early if user can either update any silence or there is an error
|
// return early if user can either update any silence or there is an error
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
||||||
)
|
)
|
||||||
@ -18,15 +19,15 @@ import (
|
|||||||
var orgID = rand.Int63()
|
var orgID = rand.Int63()
|
||||||
|
|
||||||
func TestFilterByAccess(t *testing.T) {
|
func TestFilterByAccess(t *testing.T) {
|
||||||
global := testSilence{ID: "global", RuleUID: nil}
|
global := testSilence("global", nil)
|
||||||
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
|
ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid"))
|
||||||
folder1 := "rule-1-folder-uid"
|
folder1 := "rule-1-folder-uid"
|
||||||
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
||||||
ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")}
|
ruleSilence2 := testSilence("rule-2", utils.Pointer("rule-2-uid"))
|
||||||
folder2 := "rule-2-folder-uid"
|
folder2 := "rule-2-folder-uid"
|
||||||
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
|
notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid"))
|
||||||
|
|
||||||
silences := []Silence{
|
silences := []*models.Silence{
|
||||||
global,
|
global,
|
||||||
ruleSilence1,
|
ruleSilence1,
|
||||||
ruleSilence2,
|
ruleSilence2,
|
||||||
@ -36,19 +37,19 @@ func TestFilterByAccess(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
user identity.Requester
|
user identity.Requester
|
||||||
expected []Silence
|
expected []*models.Silence
|
||||||
expectedDbAccess bool
|
expectedDbAccess bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no silence access, empty list",
|
name: "no silence access, empty list",
|
||||||
user: newUser(),
|
user: newUser(),
|
||||||
expected: []Silence{},
|
expected: []*models.Silence{},
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "instance reader should get all",
|
name: "instance reader should get all",
|
||||||
user: newUser(ac.Permission{Action: instancesRead}),
|
user: newUser(ac.Permission{Action: instancesRead}),
|
||||||
expected: []Silence{
|
expected: []*models.Silence{
|
||||||
global,
|
global,
|
||||||
ruleSilence1,
|
ruleSilence1,
|
||||||
ruleSilence2,
|
ruleSilence2,
|
||||||
@ -59,7 +60,7 @@ func TestFilterByAccess(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "silence reader should get global + folder",
|
name: "silence reader should get global + folder",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||||
expected: []Silence{
|
expected: []*models.Silence{
|
||||||
global,
|
global,
|
||||||
ruleSilence1,
|
ruleSilence1,
|
||||||
},
|
},
|
||||||
@ -71,8 +72,8 @@ func TestFilterByAccess(t *testing.T) {
|
|||||||
ac := &recordingAccessControlFake{}
|
ac := &recordingAccessControlFake{}
|
||||||
store := &fakeRuleUIDToNamespaceStore{
|
store := &fakeRuleUIDToNamespaceStore{
|
||||||
Response: map[string]string{
|
Response: map[string]string{
|
||||||
*ruleSilence1.RuleUID: folder1,
|
*ruleSilence1.GetRuleUID(): folder1,
|
||||||
*ruleSilence2.RuleUID: folder2,
|
*ruleSilence2.GetRuleUID(): folder2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
svc := NewSilenceService(ac, store)
|
svc := NewSilenceService(ac, store)
|
||||||
@ -93,60 +94,60 @@ func TestFilterByAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthorizeReadSilence(t *testing.T) {
|
func TestAuthorizeReadSilence(t *testing.T) {
|
||||||
global := testSilence{ID: "global", RuleUID: nil}
|
global := testSilence("global", nil)
|
||||||
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
|
ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid"))
|
||||||
folder1 := "rule-1-folder-uid"
|
folder1 := "rule-1-folder-uid"
|
||||||
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
||||||
ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")}
|
ruleSilence2 := testSilence("rule-2", utils.Pointer("rule-2-uid"))
|
||||||
folder2 := "rule-2-folder-uid"
|
folder2 := "rule-2-folder-uid"
|
||||||
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
|
notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid"))
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
user identity.Requester
|
user identity.Requester
|
||||||
silence []testSilence
|
silence []*models.Silence
|
||||||
expectedErr error
|
expectedErr error
|
||||||
expectedDbAccess bool
|
expectedDbAccess bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "not authorized without permissions",
|
name: "not authorized without permissions",
|
||||||
user: newUser(),
|
user: newUser(),
|
||||||
silence: []testSilence{global, ruleSilence1, notFoundRule},
|
silence: []*models.Silence{global, ruleSilence1, notFoundRule},
|
||||||
expectedErr: ErrAuthorizationBase,
|
expectedErr: ErrAuthorizationBase,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "instance reader can read any silence",
|
name: "instance reader can read any silence",
|
||||||
user: newUser(ac.Permission{Action: instancesRead}),
|
user: newUser(ac.Permission{Action: instancesRead}),
|
||||||
silence: []testSilence{global, ruleSilence1, notFoundRule},
|
silence: []*models.Silence{global, ruleSilence1, notFoundRule},
|
||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "silence reader can read global",
|
name: "silence reader can read global",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||||
silence: []testSilence{global},
|
silence: []*models.Silence{global},
|
||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "silence reader can read from allowed folder",
|
name: "silence reader can read from allowed folder",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||||
silence: []testSilence{ruleSilence1},
|
silence: []*models.Silence{ruleSilence1},
|
||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
expectedDbAccess: true,
|
expectedDbAccess: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "silence reader cannot read from other folders",
|
name: "silence reader cannot read from other folders",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||||
silence: []testSilence{ruleSilence2},
|
silence: []*models.Silence{ruleSilence2},
|
||||||
expectedErr: ErrAuthorizationBase,
|
expectedErr: ErrAuthorizationBase,
|
||||||
expectedDbAccess: true,
|
expectedDbAccess: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "silence reader cannot read unknown rule",
|
name: "silence reader cannot read unknown rule",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||||
silence: []testSilence{notFoundRule},
|
silence: []*models.Silence{notFoundRule},
|
||||||
expectedErr: ErrAuthorizationBase,
|
expectedErr: ErrAuthorizationBase,
|
||||||
expectedDbAccess: true,
|
expectedDbAccess: true,
|
||||||
},
|
},
|
||||||
@ -155,12 +156,12 @@ func TestAuthorizeReadSilence(t *testing.T) {
|
|||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
for _, silence := range testCase.silence {
|
for _, silence := range testCase.silence {
|
||||||
t.Run(silence.ID, func(t *testing.T) {
|
t.Run(*silence.ID, func(t *testing.T) {
|
||||||
ac := &recordingAccessControlFake{}
|
ac := &recordingAccessControlFake{}
|
||||||
store := &fakeRuleUIDToNamespaceStore{
|
store := &fakeRuleUIDToNamespaceStore{
|
||||||
Response: map[string]string{
|
Response: map[string]string{
|
||||||
*ruleSilence1.RuleUID: folder1,
|
*ruleSilence1.GetRuleUID(): folder1,
|
||||||
*ruleSilence2.RuleUID: folder2,
|
*ruleSilence2.GetRuleUID(): folder2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
svc := NewSilenceService(ac, store)
|
svc := NewSilenceService(ac, store)
|
||||||
@ -183,16 +184,16 @@ func TestAuthorizeReadSilence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthorizeCreateSilence(t *testing.T) {
|
func TestAuthorizeCreateSilence(t *testing.T) {
|
||||||
global := testSilence{ID: "global", RuleUID: nil}
|
global := testSilence("global", nil)
|
||||||
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
|
ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid"))
|
||||||
folder1 := "rule-1-folder-uid"
|
folder1 := "rule-1-folder-uid"
|
||||||
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
||||||
ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")}
|
ruleSilence2 := testSilence("rule-2", utils.Pointer("rule-2-uid"))
|
||||||
folder2 := "rule-2-folder-uid"
|
folder2 := "rule-2-folder-uid"
|
||||||
folder2Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2)
|
folder2Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2)
|
||||||
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
|
notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid"))
|
||||||
|
|
||||||
silences := []testSilence{
|
silences := []*models.Silence{
|
||||||
global,
|
global,
|
||||||
ruleSilence1,
|
ruleSilence1,
|
||||||
ruleSilence2,
|
ruleSilence2,
|
||||||
@ -208,7 +209,7 @@ func TestAuthorizeCreateSilence(t *testing.T) {
|
|||||||
user identity.Requester
|
user identity.Requester
|
||||||
expectedErr error
|
expectedErr error
|
||||||
expectedDbAccess bool
|
expectedDbAccess bool
|
||||||
overrides map[testSilence]override
|
overrides map[*models.Silence]override
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "not authorized without permissions",
|
name: "not authorized without permissions",
|
||||||
@ -243,7 +244,7 @@ func TestAuthorizeCreateSilence(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "instance read + silence create",
|
name: "instance read + silence create",
|
||||||
user: newUser(ac.Permission{Action: silenceCreate, Scope: folder1Scope}, ac.Permission{Action: instancesRead}),
|
user: newUser(ac.Permission{Action: silenceCreate, Scope: folder1Scope}, ac.Permission{Action: instancesRead}),
|
||||||
overrides: map[testSilence]override{
|
overrides: map[*models.Silence]override{
|
||||||
global: {
|
global: {
|
||||||
expectedErr: ErrAuthorizationBase,
|
expectedErr: ErrAuthorizationBase,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
@ -259,7 +260,7 @@ func TestAuthorizeCreateSilence(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "silence read + instance create",
|
name: "silence read + instance create",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesCreate}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesCreate}),
|
||||||
overrides: map[testSilence]override{
|
overrides: map[*models.Silence]override{
|
||||||
global: {
|
global: {
|
||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
@ -275,7 +276,7 @@ func TestAuthorizeCreateSilence(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "silence read + create",
|
name: "silence read + create",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceCreate, Scope: folder1Scope}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceCreate, Scope: folder1Scope}),
|
||||||
overrides: map[testSilence]override{
|
overrides: map[*models.Silence]override{
|
||||||
global: {
|
global: {
|
||||||
expectedErr: ErrAuthorizationBase,
|
expectedErr: ErrAuthorizationBase,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
@ -299,12 +300,12 @@ func TestAuthorizeCreateSilence(t *testing.T) {
|
|||||||
expectedErr = s.expectedErr
|
expectedErr = s.expectedErr
|
||||||
expectedDbAccess = s.expectedDbAccess
|
expectedDbAccess = s.expectedDbAccess
|
||||||
}
|
}
|
||||||
t.Run(silence.ID, func(t *testing.T) {
|
t.Run(*silence.ID, func(t *testing.T) {
|
||||||
ac := &recordingAccessControlFake{}
|
ac := &recordingAccessControlFake{}
|
||||||
store := &fakeRuleUIDToNamespaceStore{
|
store := &fakeRuleUIDToNamespaceStore{
|
||||||
Response: map[string]string{
|
Response: map[string]string{
|
||||||
*ruleSilence1.RuleUID: folder1,
|
*ruleSilence1.GetRuleUID(): folder1,
|
||||||
*ruleSilence2.RuleUID: folder2,
|
*ruleSilence2.GetRuleUID(): folder2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
svc := NewSilenceService(ac, store)
|
svc := NewSilenceService(ac, store)
|
||||||
@ -328,16 +329,16 @@ func TestAuthorizeCreateSilence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthorizeUpdateSilence(t *testing.T) {
|
func TestAuthorizeUpdateSilence(t *testing.T) {
|
||||||
global := testSilence{ID: "global", RuleUID: nil}
|
global := testSilence("global", nil)
|
||||||
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
|
ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid"))
|
||||||
folder1 := "rule-1-folder-uid"
|
folder1 := "rule-1-folder-uid"
|
||||||
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
||||||
ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")}
|
ruleSilence2 := testSilence("rule-2", utils.Pointer("rule-2-uid"))
|
||||||
folder2 := "rule-2-folder-uid"
|
folder2 := "rule-2-folder-uid"
|
||||||
folder2Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2)
|
folder2Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2)
|
||||||
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
|
notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid"))
|
||||||
|
|
||||||
silences := []testSilence{
|
silences := []*models.Silence{
|
||||||
global,
|
global,
|
||||||
ruleSilence1,
|
ruleSilence1,
|
||||||
ruleSilence2,
|
ruleSilence2,
|
||||||
@ -353,7 +354,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
|
|||||||
user identity.Requester
|
user identity.Requester
|
||||||
expectedErr error
|
expectedErr error
|
||||||
expectedDbAccess bool
|
expectedDbAccess bool
|
||||||
overrides map[testSilence]override
|
overrides map[*models.Silence]override
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "not authorized without permissions",
|
name: "not authorized without permissions",
|
||||||
@ -388,7 +389,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "instance read + silence write",
|
name: "instance read + silence write",
|
||||||
user: newUser(ac.Permission{Action: silenceWrite, Scope: folder1Scope}, ac.Permission{Action: instancesRead}),
|
user: newUser(ac.Permission{Action: silenceWrite, Scope: folder1Scope}, ac.Permission{Action: instancesRead}),
|
||||||
overrides: map[testSilence]override{
|
overrides: map[*models.Silence]override{
|
||||||
global: {
|
global: {
|
||||||
expectedErr: ErrAuthorizationBase,
|
expectedErr: ErrAuthorizationBase,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
@ -404,7 +405,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "silence read + instance write",
|
name: "silence read + instance write",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesWrite}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesWrite}),
|
||||||
overrides: map[testSilence]override{
|
overrides: map[*models.Silence]override{
|
||||||
global: {
|
global: {
|
||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
@ -420,7 +421,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "silence read + write",
|
name: "silence read + write",
|
||||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceWrite, Scope: folder1Scope}),
|
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceWrite, Scope: folder1Scope}),
|
||||||
overrides: map[testSilence]override{
|
overrides: map[*models.Silence]override{
|
||||||
global: {
|
global: {
|
||||||
expectedErr: ErrAuthorizationBase,
|
expectedErr: ErrAuthorizationBase,
|
||||||
expectedDbAccess: false,
|
expectedDbAccess: false,
|
||||||
@ -444,12 +445,12 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
|
|||||||
expectedErr = s.expectedErr
|
expectedErr = s.expectedErr
|
||||||
expectedDbAccess = s.expectedDbAccess
|
expectedDbAccess = s.expectedDbAccess
|
||||||
}
|
}
|
||||||
t.Run(silence.ID, func(t *testing.T) {
|
t.Run(*silence.ID, func(t *testing.T) {
|
||||||
ac := &recordingAccessControlFake{}
|
ac := &recordingAccessControlFake{}
|
||||||
store := &fakeRuleUIDToNamespaceStore{
|
store := &fakeRuleUIDToNamespaceStore{
|
||||||
Response: map[string]string{
|
Response: map[string]string{
|
||||||
*ruleSilence1.RuleUID: folder1,
|
*ruleSilence1.GetRuleUID(): folder1,
|
||||||
*ruleSilence2.RuleUID: folder2,
|
*ruleSilence2.GetRuleUID(): folder2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
svc := NewSilenceService(ac, store)
|
svc := NewSilenceService(ac, store)
|
||||||
@ -472,13 +473,8 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type testSilence struct {
|
func testSilence(id string, ruleUID *string) *models.Silence {
|
||||||
ID string
|
return &models.Silence{ID: &id, RuleUID: ruleUID}
|
||||||
RuleUID *string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t testSilence) GetRuleUID() *string {
|
|
||||||
return t.RuleUID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeRuleUIDToNamespaceStore struct {
|
type fakeRuleUIDToNamespaceStore struct {
|
||||||
|
@ -9,15 +9,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-openapi/strfmt"
|
|
||||||
|
|
||||||
alertingNotify "github.com/grafana/alerting/notify"
|
alertingNotify "github.com/grafana/alerting/notify"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||||
authz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
|
||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
@ -59,50 +56,6 @@ func (srv AlertmanagerSrv) RouteGetAMStatus(c *contextmodel.ReqContext) response
|
|||||||
return response.JSON(http.StatusOK, status)
|
return response.JSON(http.StatusOK, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv AlertmanagerSrv) RouteCreateSilence(c *contextmodel.ReqContext, postableSilence apimodels.PostableSilence) response.Response {
|
|
||||||
err := postableSilence.Validate(strfmt.Default)
|
|
||||||
if err != nil {
|
|
||||||
srv.log.Error("Silence failed validation", "error", err)
|
|
||||||
return ErrResp(http.StatusBadRequest, err, "silence failed validation")
|
|
||||||
}
|
|
||||||
|
|
||||||
action := accesscontrol.ActionAlertingInstanceUpdate
|
|
||||||
if postableSilence.ID == "" {
|
|
||||||
action = accesscontrol.ActionAlertingInstanceCreate
|
|
||||||
}
|
|
||||||
evaluator := accesscontrol.EvalPermission(action)
|
|
||||||
if !accesscontrol.HasAccess(srv.ac, c)(evaluator) {
|
|
||||||
errAction := "update"
|
|
||||||
if postableSilence.ID == "" {
|
|
||||||
errAction = "create"
|
|
||||||
}
|
|
||||||
return response.Err(authz.NewAuthorizationErrorWithPermissions(fmt.Sprintf("%s silences", errAction), evaluator))
|
|
||||||
}
|
|
||||||
|
|
||||||
silenceID, err := srv.mam.CreateSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), &postableSilence)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) {
|
|
||||||
return ErrResp(http.StatusNotFound, err, "")
|
|
||||||
}
|
|
||||||
if errors.Is(err, notifier.ErrAlertmanagerNotReady) {
|
|
||||||
return ErrResp(http.StatusConflict, err, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, alertingNotify.ErrSilenceNotFound) {
|
|
||||||
return ErrResp(http.StatusNotFound, err, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, alertingNotify.ErrCreateSilenceBadPayload) {
|
|
||||||
return ErrResp(http.StatusBadRequest, err, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrResp(http.StatusInternalServerError, err, "failed to create silence")
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusAccepted, apimodels.PostSilencesOKBody{
|
|
||||||
SilenceID: silenceID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *contextmodel.ReqContext) response.Response {
|
func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *contextmodel.ReqContext) response.Response {
|
||||||
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID())
|
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID())
|
||||||
if errResp != nil {
|
if errResp != nil {
|
||||||
@ -117,22 +70,6 @@ func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *contextmodel.ReqContext)
|
|||||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration deleted; the default is applied"})
|
return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration deleted; the default is applied"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv AlertmanagerSrv) RouteDeleteSilence(c *contextmodel.ReqContext, silenceID string) response.Response {
|
|
||||||
if err := srv.mam.DeleteSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), silenceID); err != nil {
|
|
||||||
if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) {
|
|
||||||
return ErrResp(http.StatusNotFound, err, "")
|
|
||||||
}
|
|
||||||
if errors.Is(err, notifier.ErrAlertmanagerNotReady) {
|
|
||||||
return ErrResp(http.StatusConflict, err, "")
|
|
||||||
}
|
|
||||||
if errors.Is(err, alertingNotify.ErrSilenceNotFound) {
|
|
||||||
return ErrResp(http.StatusNotFound, err, "")
|
|
||||||
}
|
|
||||||
return ErrResp(http.StatusInternalServerError, err, "")
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *contextmodel.ReqContext) response.Response {
|
func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *contextmodel.ReqContext) response.Response {
|
||||||
canSeeAutogen := c.SignedInUser.HasRole(org.RoleAdmin)
|
canSeeAutogen := c.SignedInUser.HasRole(org.RoleAdmin)
|
||||||
config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID(), canSeeAutogen)
|
config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID(), canSeeAutogen)
|
||||||
@ -208,40 +145,6 @@ func (srv AlertmanagerSrv) RouteGetAMAlerts(c *contextmodel.ReqContext) response
|
|||||||
return response.JSON(http.StatusOK, alerts)
|
return response.JSON(http.StatusOK, alerts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv AlertmanagerSrv) RouteGetSilence(c *contextmodel.ReqContext, silenceID string) response.Response {
|
|
||||||
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID())
|
|
||||||
if errResp != nil {
|
|
||||||
return errResp
|
|
||||||
}
|
|
||||||
|
|
||||||
gettableSilence, err := am.GetSilence(c.Req.Context(), silenceID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, alertingNotify.ErrSilenceNotFound) {
|
|
||||||
return ErrResp(http.StatusNotFound, err, "")
|
|
||||||
}
|
|
||||||
// any other error here should be an unexpected failure and thus an internal error
|
|
||||||
return ErrResp(http.StatusInternalServerError, err, "")
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusOK, gettableSilence)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv AlertmanagerSrv) RouteGetSilences(c *contextmodel.ReqContext) response.Response {
|
|
||||||
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID())
|
|
||||||
if errResp != nil {
|
|
||||||
return errResp
|
|
||||||
}
|
|
||||||
|
|
||||||
gettableSilences, err := am.ListSilences(c.Req.Context(), c.QueryStrings("filter"))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, alertingNotify.ErrListSilencesBadPayload) {
|
|
||||||
return ErrResp(http.StatusBadRequest, err, "")
|
|
||||||
}
|
|
||||||
// any other error here should be an unexpected failure and thus an internal error
|
|
||||||
return ErrResp(http.StatusInternalServerError, err, "")
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusOK, gettableSilences)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv AlertmanagerSrv) RoutePostGrafanaAlertingConfigHistoryActivate(c *contextmodel.ReqContext, id string) response.Response {
|
func (srv AlertmanagerSrv) RoutePostGrafanaAlertingConfigHistoryActivate(c *contextmodel.ReqContext, id string) response.Response {
|
||||||
confId, err := strconv.ParseInt(id, 10, 64)
|
confId, err := strconv.ParseInt(id, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
112
pkg/services/ngalert/api/api_alertmanager_silences.go
Normal file
112
pkg/services/ngalert/api/api_alertmanager_silences.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
|
||||||
|
alertingNotify "github.com/grafana/alerting/notify"
|
||||||
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||||
|
authz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||||||
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (srv AlertmanagerSrv) RouteGetSilence(c *contextmodel.ReqContext, silenceID string) response.Response {
|
||||||
|
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID())
|
||||||
|
if errResp != nil {
|
||||||
|
return errResp
|
||||||
|
}
|
||||||
|
|
||||||
|
gettableSilence, err := am.GetSilence(c.Req.Context(), silenceID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, alertingNotify.ErrSilenceNotFound) {
|
||||||
|
return ErrResp(http.StatusNotFound, err, "")
|
||||||
|
}
|
||||||
|
// any other error here should be an unexpected failure and thus an internal error
|
||||||
|
return ErrResp(http.StatusInternalServerError, err, "")
|
||||||
|
}
|
||||||
|
return response.JSON(http.StatusOK, gettableSilence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv AlertmanagerSrv) RouteGetSilences(c *contextmodel.ReqContext) response.Response {
|
||||||
|
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID())
|
||||||
|
if errResp != nil {
|
||||||
|
return errResp
|
||||||
|
}
|
||||||
|
|
||||||
|
gettableSilences, err := am.ListSilences(c.Req.Context(), c.QueryStrings("filter"))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, alertingNotify.ErrListSilencesBadPayload) {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
// any other error here should be an unexpected failure and thus an internal error
|
||||||
|
return ErrResp(http.StatusInternalServerError, err, "")
|
||||||
|
}
|
||||||
|
return response.JSON(http.StatusOK, gettableSilences)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv AlertmanagerSrv) RouteCreateSilence(c *contextmodel.ReqContext, postableSilence apimodels.PostableSilence) response.Response {
|
||||||
|
err := postableSilence.Validate(strfmt.Default)
|
||||||
|
if err != nil {
|
||||||
|
srv.log.Error("Silence failed validation", "error", err)
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "silence failed validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
action := accesscontrol.ActionAlertingInstanceUpdate
|
||||||
|
if postableSilence.ID == "" {
|
||||||
|
action = accesscontrol.ActionAlertingInstanceCreate
|
||||||
|
}
|
||||||
|
evaluator := accesscontrol.EvalPermission(action)
|
||||||
|
if !accesscontrol.HasAccess(srv.ac, c)(evaluator) {
|
||||||
|
errAction := "update"
|
||||||
|
if postableSilence.ID == "" {
|
||||||
|
errAction = "create"
|
||||||
|
}
|
||||||
|
return response.Err(authz.NewAuthorizationErrorWithPermissions(fmt.Sprintf("%s silences", errAction), evaluator))
|
||||||
|
}
|
||||||
|
|
||||||
|
silenceID, err := srv.mam.CreateSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), &postableSilence)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) {
|
||||||
|
return ErrResp(http.StatusNotFound, err, "")
|
||||||
|
}
|
||||||
|
if errors.Is(err, notifier.ErrAlertmanagerNotReady) {
|
||||||
|
return ErrResp(http.StatusConflict, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, alertingNotify.ErrSilenceNotFound) {
|
||||||
|
return ErrResp(http.StatusNotFound, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, alertingNotify.ErrCreateSilenceBadPayload) {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrResp(http.StatusInternalServerError, err, "failed to create silence")
|
||||||
|
}
|
||||||
|
return response.JSON(http.StatusAccepted, apimodels.PostSilencesOKBody{
|
||||||
|
SilenceID: silenceID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv AlertmanagerSrv) RouteDeleteSilence(c *contextmodel.ReqContext, silenceID string) response.Response {
|
||||||
|
if err := srv.mam.DeleteSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), silenceID); err != nil {
|
||||||
|
if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) {
|
||||||
|
return ErrResp(http.StatusNotFound, err, "")
|
||||||
|
}
|
||||||
|
if errors.Is(err, notifier.ErrAlertmanagerNotReady) {
|
||||||
|
return ErrResp(http.StatusConflict, err, "")
|
||||||
|
}
|
||||||
|
if errors.Is(err, alertingNotify.ErrSilenceNotFound) {
|
||||||
|
return ErrResp(http.StatusNotFound, err, "")
|
||||||
|
}
|
||||||
|
return ErrResp(http.StatusInternalServerError, err, "")
|
||||||
|
}
|
||||||
|
return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"})
|
||||||
|
}
|
194
pkg/services/ngalert/api/api_alertmanager_silences_test.go
Normal file
194
pkg/services/ngalert/api/api_alertmanager_silences_test.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
amv2 "github.com/prometheus/alertmanager/api/v2/models"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||||
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
"github.com/grafana/grafana/pkg/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSilenceCreate(t *testing.T) {
|
||||||
|
makeSilence := func(comment string, createdBy string,
|
||||||
|
startsAt, endsAt strfmt.DateTime, matchers amv2.Matchers) amv2.Silence {
|
||||||
|
return amv2.Silence{
|
||||||
|
Comment: &comment,
|
||||||
|
CreatedBy: &createdBy,
|
||||||
|
StartsAt: &startsAt,
|
||||||
|
EndsAt: &endsAt,
|
||||||
|
Matchers: matchers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
dt := func(t time.Time) strfmt.DateTime { return strfmt.DateTime(t) }
|
||||||
|
tru := true
|
||||||
|
testString := "testName"
|
||||||
|
matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &tru, IsRegex: &tru, Value: &testString}}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
silence amv2.Silence
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{"Valid Silence",
|
||||||
|
makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers),
|
||||||
|
http.StatusAccepted,
|
||||||
|
},
|
||||||
|
{"No Comment Silence",
|
||||||
|
func() amv2.Silence {
|
||||||
|
s := makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers)
|
||||||
|
s.Comment = nil
|
||||||
|
return s
|
||||||
|
}(),
|
||||||
|
http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cas := range cases {
|
||||||
|
t.Run(cas.name, func(t *testing.T) {
|
||||||
|
rc := contextmodel.ReqContext{
|
||||||
|
Context: &web.Context{
|
||||||
|
Req: &http.Request{},
|
||||||
|
},
|
||||||
|
SignedInUser: &user.SignedInUser{
|
||||||
|
OrgRole: org.RoleEditor,
|
||||||
|
OrgID: 1,
|
||||||
|
Permissions: map[int64]map[string][]string{
|
||||||
|
1: {accesscontrol.ActionAlertingInstanceCreate: {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := createSut(t)
|
||||||
|
|
||||||
|
resp := srv.RouteCreateSilence(&rc, amv2.PostableSilence{
|
||||||
|
ID: "",
|
||||||
|
Silence: cas.silence,
|
||||||
|
})
|
||||||
|
require.Equal(t, cas.status, resp.Status())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteCreateSilence(t *testing.T) {
|
||||||
|
tesCases := []struct {
|
||||||
|
name string
|
||||||
|
silence func() apimodels.PostableSilence
|
||||||
|
permissions map[int64]map[string][]string
|
||||||
|
expectedStatus int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "new silence, role-based access control is enabled, not authorized",
|
||||||
|
silence: silenceGen(withEmptyID),
|
||||||
|
permissions: map[int64]map[string][]string{
|
||||||
|
1: {},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new silence, role-based access control is enabled, authorized",
|
||||||
|
silence: silenceGen(withEmptyID),
|
||||||
|
permissions: map[int64]map[string][]string{
|
||||||
|
1: {accesscontrol.ActionAlertingInstanceCreate: {}},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusAccepted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update silence, role-based access control is enabled, not authorized",
|
||||||
|
silence: silenceGen(),
|
||||||
|
permissions: map[int64]map[string][]string{
|
||||||
|
1: {accesscontrol.ActionAlertingInstanceCreate: {}},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update silence, role-based access control is enabled, authorized",
|
||||||
|
silence: silenceGen(),
|
||||||
|
permissions: map[int64]map[string][]string{
|
||||||
|
1: {accesscontrol.ActionAlertingInstanceUpdate: {}},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusAccepted,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tesCase := range tesCases {
|
||||||
|
t.Run(tesCase.name, func(t *testing.T) {
|
||||||
|
sut := createSut(t)
|
||||||
|
|
||||||
|
rc := contextmodel.ReqContext{
|
||||||
|
Context: &web.Context{
|
||||||
|
Req: &http.Request{},
|
||||||
|
},
|
||||||
|
SignedInUser: &user.SignedInUser{
|
||||||
|
Permissions: tesCase.permissions,
|
||||||
|
OrgID: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
silence := tesCase.silence()
|
||||||
|
|
||||||
|
if silence.ID != "" {
|
||||||
|
alertmanagerFor, err := sut.mam.AlertmanagerFor(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
silence.ID = ""
|
||||||
|
newID, err := alertmanagerFor.CreateSilence(context.Background(), &silence)
|
||||||
|
require.NoError(t, err)
|
||||||
|
silence.ID = newID
|
||||||
|
}
|
||||||
|
|
||||||
|
response := sut.RouteCreateSilence(&rc, silence)
|
||||||
|
require.Equal(t, tesCase.expectedStatus, response.Status())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func silenceGen(mutatorFuncs ...func(*apimodels.PostableSilence)) func() apimodels.PostableSilence {
|
||||||
|
return func() apimodels.PostableSilence {
|
||||||
|
testString := util.GenerateShortUID()
|
||||||
|
isEqual := rand.Int()%2 == 0
|
||||||
|
isRegex := rand.Int()%2 == 0
|
||||||
|
value := util.GenerateShortUID()
|
||||||
|
if isRegex {
|
||||||
|
value = ".*" + util.GenerateShortUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &isEqual, IsRegex: &isRegex, Value: &value}}
|
||||||
|
comment := util.GenerateShortUID()
|
||||||
|
starts := strfmt.DateTime(timeNow().Add(-time.Duration(rand.Int63n(9)+1) * time.Second))
|
||||||
|
ends := strfmt.DateTime(timeNow().Add(time.Duration(rand.Int63n(9)+1) * time.Second))
|
||||||
|
createdBy := "User-" + util.GenerateShortUID()
|
||||||
|
s := apimodels.PostableSilence{
|
||||||
|
ID: util.GenerateShortUID(),
|
||||||
|
Silence: amv2.Silence{
|
||||||
|
Comment: &comment,
|
||||||
|
CreatedBy: &createdBy,
|
||||||
|
EndsAt: &ends,
|
||||||
|
Matchers: matchers,
|
||||||
|
StartsAt: &starts,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mutator := range mutatorFuncs {
|
||||||
|
mutator(&s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withEmptyID(silence *apimodels.PostableSilence) {
|
||||||
|
silence.ID = ""
|
||||||
|
}
|
@ -4,23 +4,20 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-openapi/strfmt"
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
alertingNotify "github.com/grafana/alerting/notify"
|
|
||||||
amv2 "github.com/prometheus/alertmanager/api/v2/models"
|
|
||||||
"github.com/prometheus/alertmanager/pkg/labels"
|
"github.com/prometheus/alertmanager/pkg/labels"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
alertingNotify "github.com/grafana/alerting/notify"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
@ -35,7 +32,6 @@ import (
|
|||||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -624,141 +620,6 @@ func TestRoutePostTestTemplates(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSilenceCreate(t *testing.T) {
|
|
||||||
makeSilence := func(comment string, createdBy string,
|
|
||||||
startsAt, endsAt strfmt.DateTime, matchers amv2.Matchers) amv2.Silence {
|
|
||||||
return amv2.Silence{
|
|
||||||
Comment: &comment,
|
|
||||||
CreatedBy: &createdBy,
|
|
||||||
StartsAt: &startsAt,
|
|
||||||
EndsAt: &endsAt,
|
|
||||||
Matchers: matchers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
dt := func(t time.Time) strfmt.DateTime { return strfmt.DateTime(t) }
|
|
||||||
tru := true
|
|
||||||
testString := "testName"
|
|
||||||
matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &tru, IsRegex: &tru, Value: &testString}}
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
silence amv2.Silence
|
|
||||||
status int
|
|
||||||
}{
|
|
||||||
{"Valid Silence",
|
|
||||||
makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers),
|
|
||||||
http.StatusAccepted,
|
|
||||||
},
|
|
||||||
{"No Comment Silence",
|
|
||||||
func() amv2.Silence {
|
|
||||||
s := makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers)
|
|
||||||
s.Comment = nil
|
|
||||||
return s
|
|
||||||
}(),
|
|
||||||
http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cas := range cases {
|
|
||||||
t.Run(cas.name, func(t *testing.T) {
|
|
||||||
rc := contextmodel.ReqContext{
|
|
||||||
Context: &web.Context{
|
|
||||||
Req: &http.Request{},
|
|
||||||
},
|
|
||||||
SignedInUser: &user.SignedInUser{
|
|
||||||
OrgRole: org.RoleEditor,
|
|
||||||
OrgID: 1,
|
|
||||||
Permissions: map[int64]map[string][]string{
|
|
||||||
1: {accesscontrol.ActionAlertingInstanceCreate: {}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := createSut(t)
|
|
||||||
|
|
||||||
resp := srv.RouteCreateSilence(&rc, amv2.PostableSilence{
|
|
||||||
ID: "",
|
|
||||||
Silence: cas.silence,
|
|
||||||
})
|
|
||||||
require.Equal(t, cas.status, resp.Status())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRouteCreateSilence(t *testing.T) {
|
|
||||||
tesCases := []struct {
|
|
||||||
name string
|
|
||||||
silence func() apimodels.PostableSilence
|
|
||||||
permissions map[int64]map[string][]string
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "new silence, role-based access control is enabled, not authorized",
|
|
||||||
silence: silenceGen(withEmptyID),
|
|
||||||
permissions: map[int64]map[string][]string{
|
|
||||||
1: {},
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "new silence, role-based access control is enabled, authorized",
|
|
||||||
silence: silenceGen(withEmptyID),
|
|
||||||
permissions: map[int64]map[string][]string{
|
|
||||||
1: {accesscontrol.ActionAlertingInstanceCreate: {}},
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusAccepted,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "update silence, role-based access control is enabled, not authorized",
|
|
||||||
silence: silenceGen(),
|
|
||||||
permissions: map[int64]map[string][]string{
|
|
||||||
1: {accesscontrol.ActionAlertingInstanceCreate: {}},
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "update silence, role-based access control is enabled, authorized",
|
|
||||||
silence: silenceGen(),
|
|
||||||
permissions: map[int64]map[string][]string{
|
|
||||||
1: {accesscontrol.ActionAlertingInstanceUpdate: {}},
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusAccepted,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tesCase := range tesCases {
|
|
||||||
t.Run(tesCase.name, func(t *testing.T) {
|
|
||||||
sut := createSut(t)
|
|
||||||
|
|
||||||
rc := contextmodel.ReqContext{
|
|
||||||
Context: &web.Context{
|
|
||||||
Req: &http.Request{},
|
|
||||||
},
|
|
||||||
SignedInUser: &user.SignedInUser{
|
|
||||||
Permissions: tesCase.permissions,
|
|
||||||
OrgID: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
silence := tesCase.silence()
|
|
||||||
|
|
||||||
if silence.ID != "" {
|
|
||||||
alertmanagerFor, err := sut.mam.AlertmanagerFor(1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
silence.ID = ""
|
|
||||||
newID, err := alertmanagerFor.CreateSilence(context.Background(), &silence)
|
|
||||||
require.NoError(t, err)
|
|
||||||
silence.ID = newID
|
|
||||||
}
|
|
||||||
|
|
||||||
response := sut.RouteCreateSilence(&rc, silence)
|
|
||||||
require.Equal(t, tesCase.expectedStatus, response.Status())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createSut(t *testing.T) AlertmanagerSrv {
|
func createSut(t *testing.T) AlertmanagerSrv {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -973,44 +834,6 @@ var brokenConfig = `
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
func silenceGen(mutatorFuncs ...func(*apimodels.PostableSilence)) func() apimodels.PostableSilence {
|
|
||||||
return func() apimodels.PostableSilence {
|
|
||||||
testString := util.GenerateShortUID()
|
|
||||||
isEqual := rand.Int()%2 == 0
|
|
||||||
isRegex := rand.Int()%2 == 0
|
|
||||||
value := util.GenerateShortUID()
|
|
||||||
if isRegex {
|
|
||||||
value = ".*" + util.GenerateShortUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &isEqual, IsRegex: &isRegex, Value: &value}}
|
|
||||||
comment := util.GenerateShortUID()
|
|
||||||
starts := strfmt.DateTime(timeNow().Add(-time.Duration(rand.Int63n(9)+1) * time.Second))
|
|
||||||
ends := strfmt.DateTime(timeNow().Add(time.Duration(rand.Int63n(9)+1) * time.Second))
|
|
||||||
createdBy := "User-" + util.GenerateShortUID()
|
|
||||||
s := apimodels.PostableSilence{
|
|
||||||
ID: util.GenerateShortUID(),
|
|
||||||
Silence: amv2.Silence{
|
|
||||||
Comment: &comment,
|
|
||||||
CreatedBy: &createdBy,
|
|
||||||
EndsAt: &ends,
|
|
||||||
Matchers: matchers,
|
|
||||||
StartsAt: &starts,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mutator := range mutatorFuncs {
|
|
||||||
mutator(&s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withEmptyID(silence *apimodels.PostableSilence) {
|
|
||||||
silence.ID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func createRequestCtxInOrg(org int64) *contextmodel.ReqContext {
|
func createRequestCtxInOrg(org int64) *contextmodel.ReqContext {
|
||||||
return &contextmodel.ReqContext{
|
return &contextmodel.ReqContext{
|
||||||
Context: &web.Context{
|
Context: &web.Context{
|
||||||
|
11
pkg/services/ngalert/models/silence.go
Normal file
11
pkg/services/ngalert/models/silence.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Silence is the model-layer representation of an alertmanager silence.
|
||||||
|
type Silence struct { // TODO implement using matchers
|
||||||
|
ID *string
|
||||||
|
RuleUID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Silence) GetRuleUID() *string {
|
||||||
|
return s.RuleUID
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user