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:
Yuri Tseretyan 2024-04-25 15:20:37 -04:00 committed by GitHub
parent 0f98bd3b7b
commit dff7cb9afb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 378 additions and 342 deletions

View File

@ -9,6 +9,7 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
const (
@ -78,10 +79,6 @@ var (
}
)
type Silence interface {
GetRuleUID() *string
}
type RuleUIDToNamespaceStore interface {
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.
// 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.
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)
if err != nil || canAll { // return early if user can either read all silences or there is an error
return silences, err
@ -113,8 +110,8 @@ func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Reques
if err != nil || !canSome {
return nil, err
}
result := make([]Silence, 0, len(silences))
silencesByRuleUID := make(map[string][]Silence, len(silences))
result := make([]*models.Silence, 0, len(silences))
silencesByRuleUID := make(map[string][]*models.Silence, len(silences))
for _, silence := range silences {
ruleUID := silence.GetRuleUID()
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
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)
if canAll || err != nil { // return early if user can either read all silences or there is error
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
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)
if err != nil || canAny {
// 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
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)
if err != nil || canAny {
// return early if user can either update any silence or there is an error

View File

@ -11,6 +11,7 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
"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/tsdb/cloudwatch/utils"
)
@ -18,15 +19,15 @@ import (
var orgID = rand.Int63()
func TestFilterByAccess(t *testing.T) {
global := testSilence{ID: "global", RuleUID: nil}
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
global := testSilence("global", nil)
ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid"))
folder1 := "rule-1-folder-uid"
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"
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,
ruleSilence1,
ruleSilence2,
@ -36,19 +37,19 @@ func TestFilterByAccess(t *testing.T) {
testCases := []struct {
name string
user identity.Requester
expected []Silence
expected []*models.Silence
expectedDbAccess bool
}{
{
name: "no silence access, empty list",
user: newUser(),
expected: []Silence{},
expected: []*models.Silence{},
expectedDbAccess: false,
},
{
name: "instance reader should get all",
user: newUser(ac.Permission{Action: instancesRead}),
expected: []Silence{
expected: []*models.Silence{
global,
ruleSilence1,
ruleSilence2,
@ -59,7 +60,7 @@ func TestFilterByAccess(t *testing.T) {
{
name: "silence reader should get global + folder",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
expected: []Silence{
expected: []*models.Silence{
global,
ruleSilence1,
},
@ -71,8 +72,8 @@ func TestFilterByAccess(t *testing.T) {
ac := &recordingAccessControlFake{}
store := &fakeRuleUIDToNamespaceStore{
Response: map[string]string{
*ruleSilence1.RuleUID: folder1,
*ruleSilence2.RuleUID: folder2,
*ruleSilence1.GetRuleUID(): folder1,
*ruleSilence2.GetRuleUID(): folder2,
},
}
svc := NewSilenceService(ac, store)
@ -93,60 +94,60 @@ func TestFilterByAccess(t *testing.T) {
}
func TestAuthorizeReadSilence(t *testing.T) {
global := testSilence{ID: "global", RuleUID: nil}
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
global := testSilence("global", nil)
ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid"))
folder1 := "rule-1-folder-uid"
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"
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
notFoundRule := testSilence("unknown-rule", utils.Pointer("unknown-rule-uid"))
testCases := []struct {
name string
user identity.Requester
silence []testSilence
silence []*models.Silence
expectedErr error
expectedDbAccess bool
}{
{
name: "not authorized without permissions",
user: newUser(),
silence: []testSilence{global, ruleSilence1, notFoundRule},
silence: []*models.Silence{global, ruleSilence1, notFoundRule},
expectedErr: ErrAuthorizationBase,
expectedDbAccess: false,
},
{
name: "instance reader can read any silence",
user: newUser(ac.Permission{Action: instancesRead}),
silence: []testSilence{global, ruleSilence1, notFoundRule},
silence: []*models.Silence{global, ruleSilence1, notFoundRule},
expectedErr: nil,
expectedDbAccess: false,
},
{
name: "silence reader can read global",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
silence: []testSilence{global},
silence: []*models.Silence{global},
expectedErr: nil,
expectedDbAccess: false,
},
{
name: "silence reader can read from allowed folder",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
silence: []testSilence{ruleSilence1},
silence: []*models.Silence{ruleSilence1},
expectedErr: nil,
expectedDbAccess: true,
},
{
name: "silence reader cannot read from other folders",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
silence: []testSilence{ruleSilence2},
silence: []*models.Silence{ruleSilence2},
expectedErr: ErrAuthorizationBase,
expectedDbAccess: true,
},
{
name: "silence reader cannot read unknown rule",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
silence: []testSilence{notFoundRule},
silence: []*models.Silence{notFoundRule},
expectedErr: ErrAuthorizationBase,
expectedDbAccess: true,
},
@ -155,12 +156,12 @@ func TestAuthorizeReadSilence(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
for _, silence := range testCase.silence {
t.Run(silence.ID, func(t *testing.T) {
t.Run(*silence.ID, func(t *testing.T) {
ac := &recordingAccessControlFake{}
store := &fakeRuleUIDToNamespaceStore{
Response: map[string]string{
*ruleSilence1.RuleUID: folder1,
*ruleSilence2.RuleUID: folder2,
*ruleSilence1.GetRuleUID(): folder1,
*ruleSilence2.GetRuleUID(): folder2,
},
}
svc := NewSilenceService(ac, store)
@ -183,16 +184,16 @@ func TestAuthorizeReadSilence(t *testing.T) {
}
func TestAuthorizeCreateSilence(t *testing.T) {
global := testSilence{ID: "global", RuleUID: nil}
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
global := testSilence("global", nil)
ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid"))
folder1 := "rule-1-folder-uid"
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"
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,
ruleSilence1,
ruleSilence2,
@ -208,7 +209,7 @@ func TestAuthorizeCreateSilence(t *testing.T) {
user identity.Requester
expectedErr error
expectedDbAccess bool
overrides map[testSilence]override
overrides map[*models.Silence]override
}{
{
name: "not authorized without permissions",
@ -243,7 +244,7 @@ func TestAuthorizeCreateSilence(t *testing.T) {
{
name: "instance read + silence create",
user: newUser(ac.Permission{Action: silenceCreate, Scope: folder1Scope}, ac.Permission{Action: instancesRead}),
overrides: map[testSilence]override{
overrides: map[*models.Silence]override{
global: {
expectedErr: ErrAuthorizationBase,
expectedDbAccess: false,
@ -259,7 +260,7 @@ func TestAuthorizeCreateSilence(t *testing.T) {
{
name: "silence read + instance create",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesCreate}),
overrides: map[testSilence]override{
overrides: map[*models.Silence]override{
global: {
expectedErr: nil,
expectedDbAccess: false,
@ -275,7 +276,7 @@ func TestAuthorizeCreateSilence(t *testing.T) {
{
name: "silence read + create",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceCreate, Scope: folder1Scope}),
overrides: map[testSilence]override{
overrides: map[*models.Silence]override{
global: {
expectedErr: ErrAuthorizationBase,
expectedDbAccess: false,
@ -299,12 +300,12 @@ func TestAuthorizeCreateSilence(t *testing.T) {
expectedErr = s.expectedErr
expectedDbAccess = s.expectedDbAccess
}
t.Run(silence.ID, func(t *testing.T) {
t.Run(*silence.ID, func(t *testing.T) {
ac := &recordingAccessControlFake{}
store := &fakeRuleUIDToNamespaceStore{
Response: map[string]string{
*ruleSilence1.RuleUID: folder1,
*ruleSilence2.RuleUID: folder2,
*ruleSilence1.GetRuleUID(): folder1,
*ruleSilence2.GetRuleUID(): folder2,
},
}
svc := NewSilenceService(ac, store)
@ -328,16 +329,16 @@ func TestAuthorizeCreateSilence(t *testing.T) {
}
func TestAuthorizeUpdateSilence(t *testing.T) {
global := testSilence{ID: "global", RuleUID: nil}
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
global := testSilence("global", nil)
ruleSilence1 := testSilence("rule-1", utils.Pointer("rule-1-uid"))
folder1 := "rule-1-folder-uid"
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"
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,
ruleSilence1,
ruleSilence2,
@ -353,7 +354,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
user identity.Requester
expectedErr error
expectedDbAccess bool
overrides map[testSilence]override
overrides map[*models.Silence]override
}{
{
name: "not authorized without permissions",
@ -388,7 +389,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
{
name: "instance read + silence write",
user: newUser(ac.Permission{Action: silenceWrite, Scope: folder1Scope}, ac.Permission{Action: instancesRead}),
overrides: map[testSilence]override{
overrides: map[*models.Silence]override{
global: {
expectedErr: ErrAuthorizationBase,
expectedDbAccess: false,
@ -404,7 +405,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
{
name: "silence read + instance write",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesWrite}),
overrides: map[testSilence]override{
overrides: map[*models.Silence]override{
global: {
expectedErr: nil,
expectedDbAccess: false,
@ -420,7 +421,7 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
{
name: "silence read + write",
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceWrite, Scope: folder1Scope}),
overrides: map[testSilence]override{
overrides: map[*models.Silence]override{
global: {
expectedErr: ErrAuthorizationBase,
expectedDbAccess: false,
@ -444,12 +445,12 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
expectedErr = s.expectedErr
expectedDbAccess = s.expectedDbAccess
}
t.Run(silence.ID, func(t *testing.T) {
t.Run(*silence.ID, func(t *testing.T) {
ac := &recordingAccessControlFake{}
store := &fakeRuleUIDToNamespaceStore{
Response: map[string]string{
*ruleSilence1.RuleUID: folder1,
*ruleSilence2.RuleUID: folder2,
*ruleSilence1.GetRuleUID(): folder1,
*ruleSilence2.GetRuleUID(): folder2,
},
}
svc := NewSilenceService(ac, store)
@ -472,13 +473,8 @@ func TestAuthorizeUpdateSilence(t *testing.T) {
}
}
type testSilence struct {
ID string
RuleUID *string
}
func (t testSilence) GetRuleUID() *string {
return t.RuleUID
func testSilence(id string, ruleUID *string) *models.Silence {
return &models.Silence{ID: &id, RuleUID: ruleUID}
}
type fakeRuleUIDToNamespaceStore struct {

View File

@ -9,15 +9,12 @@ import (
"strings"
"time"
"github.com/go-openapi/strfmt"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"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/services/ngalert/store"
@ -59,50 +56,6 @@ func (srv AlertmanagerSrv) RouteGetAMStatus(c *contextmodel.ReqContext) response
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 {
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID())
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"})
}
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 {
canSeeAutogen := c.SignedInUser.HasRole(org.RoleAdmin)
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)
}
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 {
confId, err := strconv.ParseInt(id, 10, 64)
if err != nil {

View 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"})
}

View 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 = ""
}

View File

@ -4,23 +4,20 @@ import (
"context"
"crypto/md5"
"encoding/json"
"math/rand"
"net/http"
"testing"
"time"
"github.com/go-openapi/strfmt"
"github.com/google/go-cmp/cmp"
"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/client_golang/prometheus"
"github.com/stretchr/testify/require"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@ -35,7 +32,6 @@ import (
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"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 {
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 {
return &contextmodel.ReqContext{
Context: &web.Context{

View 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
}