Alerting: Receivers API (read only endpoints) (#81751)

* Add single receiver method

* Add receiver permissions

* Add single/multi GET endpoints for receivers

* Remove stable tag from time intervals

See end of PR description here: https://github.com/grafana/grafana/pull/81672
This commit is contained in:
William Wernert 2024-02-05 13:12:15 -05:00 committed by GitHub
parent 49b49e28af
commit 2ab7d3c725
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 971 additions and 340 deletions

View File

@ -449,6 +449,11 @@ const (
ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read" ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read"
ActionAlertingNotificationsTimeIntervalsWrite = "alert.notifications.time-intervals:write" ActionAlertingNotificationsTimeIntervalsWrite = "alert.notifications.time-intervals:write"
// Alerting receiver actions
ActionAlertingReceiversList = "alert.notifications.receivers:list"
ActionAlertingReceiversRead = "alert.notifications.receivers:read"
ActionAlertingReceiversReadSecrets = "alert.notifications.receivers.secrets:read"
// External alerting rule actions. We can only narrow it down to writes or reads, as we don't control the atomicity in the external system. // External alerting rule actions. We can only narrow it down to writes or reads, as we don't control the atomicity in the external system.
ActionAlertingRuleExternalWrite = "alert.rules.external:write" ActionAlertingRuleExternalWrite = "alert.rules.external:write"
ActionAlertingRuleExternalRead = "alert.rules.external:read" ActionAlertingRuleExternalRead = "alert.rules.external:read"

View File

@ -25,8 +25,12 @@ var (
Action: accesscontrol.ActionAlertingRuleExternalRead, Action: accesscontrol.ActionAlertingRuleExternalRead,
Scope: datasources.ScopeAll, Scope: datasources.ScopeAll,
}, },
// Following are needed for simplified notification policies
{ {
Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, // This is needed for simplified notification policies Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
},
{
Action: accesscontrol.ActionAlertingReceiversList,
}, },
}, },
}, },
@ -115,6 +119,9 @@ var (
{ {
Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
}, },
{
Action: accesscontrol.ActionAlertingReceiversRead,
},
}, },
}, },
} }

View File

@ -64,6 +64,7 @@ type API struct {
StateManager *state.Manager StateManager *state.Manager
AccessControl ac.AccessControl AccessControl ac.AccessControl
Policies *provisioning.NotificationPolicyService Policies *provisioning.NotificationPolicyService
ReceiverService *notifier.ReceiverService
ContactPointService *provisioning.ContactPointService ContactPointService *provisioning.ContactPointService
Templates *provisioning.TemplateService Templates *provisioning.TemplateService
MuteTimings *provisioning.MuteTimingService MuteTimings *provisioning.MuteTimingService
@ -152,7 +153,11 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
hist: api.Historian, hist: api.Historian,
}), m) }), m)
api.RegisterNotificationsApiEndpoints(NewNotificationsApi(api.MuteTimings), m) api.RegisterNotificationsApiEndpoints(NewNotificationsApi(&NotificationSrv{
logger: logger,
receiverService: api.ReceiverService,
muteTimingService: api.MuteTimings,
}), m)
// Inject upgrade endpoints if legacy alerting is enabled and the feature flag is enabled. // Inject upgrade endpoints if legacy alerting is enabled and the feature flag is enabled.
if !api.Cfg.UnifiedAlerting.IsEnabled() && api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingPreviewUpgrade) { if !api.Cfg.UnifiedAlerting.IsEnabled() && api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingPreviewUpgrade) {

View File

@ -0,0 +1,83 @@
package api
import (
"context"
"errors"
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
)
type NotificationSrv struct {
logger log.Logger
receiverService ReceiverService
muteTimingService MuteTimingService // defined in api_provisioning.go
}
type ReceiverService interface {
GetReceiver(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error)
GetReceivers(ctx context.Context, q models.GetReceiversQuery, u identity.Requester) ([]definitions.GettableApiReceiver, error)
}
func (srv *NotificationSrv) RouteGetTimeInterval(c *contextmodel.ReqContext, name string) response.Response {
muteTimeInterval, err := srv.muteTimingService.GetMuteTiming(c.Req.Context(), name, c.OrgID)
if err != nil {
return errorToResponse(err)
}
return response.JSON(http.StatusOK, muteTimeInterval) // TODO convert to timing interval
}
func (srv *NotificationSrv) RouteGetTimeIntervals(c *contextmodel.ReqContext) response.Response {
muteTimeIntervals, err := srv.muteTimingService.GetMuteTimings(c.Req.Context(), c.OrgID)
if err != nil {
return errorToResponse(err)
}
return response.JSON(http.StatusOK, muteTimeIntervals) // TODO convert to timing interval
}
func (srv *NotificationSrv) RouteGetReceiver(c *contextmodel.ReqContext, name string) response.Response {
q := models.GetReceiverQuery{
OrgID: c.SignedInUser.OrgID,
Name: name,
Decrypt: c.QueryBool("decrypt"),
}
receiver, err := srv.receiverService.GetReceiver(c.Req.Context(), q, c.SignedInUser)
if err != nil {
if errors.Is(err, notifier.ErrNotFound) {
return ErrResp(http.StatusNotFound, err, "receiver not found")
}
if errors.Is(err, notifier.ErrPermissionDenied) {
return ErrResp(http.StatusForbidden, err, "permission denied")
}
return ErrResp(http.StatusInternalServerError, err, "failed to get receiver")
}
return response.JSON(http.StatusOK, receiver)
}
func (srv *NotificationSrv) RouteGetReceivers(c *contextmodel.ReqContext) response.Response {
q := models.GetReceiversQuery{
OrgID: c.SignedInUser.OrgID,
Names: c.QueryStrings("names"),
Limit: c.QueryInt("limit"),
Offset: c.QueryInt("offset"),
Decrypt: c.QueryBool("decrypt"),
}
receivers, err := srv.receiverService.GetReceivers(c.Req.Context(), q, c.SignedInUser)
if err != nil {
if errors.Is(err, notifier.ErrPermissionDenied) {
return ErrResp(http.StatusForbidden, err, "permission denied")
}
return ErrResp(http.StatusInternalServerError, err, "failed to get receiver groups")
}
return response.JSON(http.StatusOK, receivers)
}

View File

@ -0,0 +1,188 @@
package api
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web"
am_config "github.com/prometheus/alertmanager/config"
"github.com/stretchr/testify/require"
)
func TestRouteGetReceiver(t *testing.T) {
fakeReceiverSvc := fakes.NewFakeReceiverService()
t.Run("returns expected model", func(t *testing.T) {
expected := definitions.GettableApiReceiver{
Receiver: am_config.Receiver{
Name: "receiver1",
},
GettableGrafanaReceivers: definitions.GettableGrafanaReceivers{
GrafanaManagedReceivers: []*definitions.GettableGrafanaReceiver{
{
UID: "uid1",
Name: "receiver1",
Type: "slack",
},
},
},
}
fakeReceiverSvc.GetReceiverFn = func(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error) {
return expected, nil
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
resp := handler.handleRouteGetReceiver(&rc, "receiver1")
require.Equal(t, http.StatusOK, resp.Status())
json, err := json.Marshal(expected)
require.NoError(t, err)
require.Equal(t, json, resp.Body())
})
t.Run("builds query from request context and url param", func(t *testing.T) {
fakeReceiverSvc.GetReceiverFn = func(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error) {
return definitions.GettableApiReceiver{}, nil
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
rc.Context.Req.Form.Set("decrypt", "true")
resp := handler.handleRouteGetReceiver(&rc, "receiver1")
require.Equal(t, http.StatusOK, resp.Status())
call := fakeReceiverSvc.PopMethodCall()
require.Equal(t, "GetReceiver", call.Method)
expectedQ := models.GetReceiverQuery{
Name: "receiver1",
Decrypt: true,
OrgID: 1,
}
require.Equal(t, expectedQ, call.Args[1])
})
t.Run("should pass along not found response", func(t *testing.T) {
fakeReceiverSvc.GetReceiverFn = func(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error) {
return definitions.GettableApiReceiver{}, notifier.ErrNotFound
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
resp := handler.handleRouteGetReceiver(&rc, "receiver1")
require.Equal(t, http.StatusNotFound, resp.Status())
})
t.Run("should pass along permission denied response", func(t *testing.T) {
fakeReceiverSvc.GetReceiverFn = func(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error) {
return definitions.GettableApiReceiver{}, notifier.ErrPermissionDenied
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
resp := handler.handleRouteGetReceiver(&rc, "receiver1")
require.Equal(t, http.StatusForbidden, resp.Status())
})
}
func TestRouteGetReceivers(t *testing.T) {
fakeReceiverSvc := fakes.NewFakeReceiverService()
t.Run("returns expected model", func(t *testing.T) {
expected := []definitions.GettableApiReceiver{
{
Receiver: am_config.Receiver{
Name: "receiver1",
},
GettableGrafanaReceivers: definitions.GettableGrafanaReceivers{
GrafanaManagedReceivers: []*definitions.GettableGrafanaReceiver{
{
UID: "uid1",
Name: "receiver1",
Type: "slack",
},
},
},
},
}
fakeReceiverSvc.GetReceiversFn = func(ctx context.Context, q models.GetReceiversQuery, u identity.Requester) ([]definitions.GettableApiReceiver, error) {
return expected, nil
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
rc.Context.Req.Form.Set("names", "receiver1")
resp := handler.handleRouteGetReceivers(&rc)
require.Equal(t, http.StatusOK, resp.Status())
json, err := json.Marshal(expected)
require.NoError(t, err)
require.Equal(t, json, resp.Body())
})
t.Run("builds query from request context", func(t *testing.T) {
fakeReceiverSvc.GetReceiversFn = func(ctx context.Context, q models.GetReceiversQuery, u identity.Requester) ([]definitions.GettableApiReceiver, error) {
return []definitions.GettableApiReceiver{}, nil
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
rc.Context.Req.Form.Set("names", "receiver1")
rc.Context.Req.Form.Add("names", "receiver2")
rc.Context.Req.Form.Set("limit", "1")
rc.Context.Req.Form.Set("offset", "2")
rc.Context.Req.Form.Set("decrypt", "true")
resp := handler.handleRouteGetReceivers(&rc)
require.Equal(t, http.StatusOK, resp.Status())
call := fakeReceiverSvc.PopMethodCall()
require.Equal(t, "GetReceivers", call.Method)
expectedQ := models.GetReceiversQuery{
Names: []string{"receiver1", "receiver2"},
Limit: 1,
Offset: 2,
Decrypt: true,
OrgID: 1,
}
require.Equal(t, expectedQ, call.Args[1])
})
t.Run("should pass along permission denied response", func(t *testing.T) {
fakeReceiverSvc.GetReceiversFn = func(ctx context.Context, q models.GetReceiversQuery, u identity.Requester) ([]definitions.GettableApiReceiver, error) {
return nil, notifier.ErrPermissionDenied
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
resp := handler.handleRouteGetReceivers(&rc)
require.Equal(t, http.StatusForbidden, resp.Status())
})
}
func newNotificationSrv(receiverService ReceiverService) *NotificationSrv {
return &NotificationSrv{
logger: log.NewNopLogger(),
receiverService: receiverService,
}
}
func testReqCtx(method string) contextmodel.ReqContext {
return contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{
Header: make(http.Header),
Form: make(url.Values),
},
Resp: web.NewResponseWriter(method, httptest.NewRecorder()),
},
SignedInUser: &user.SignedInUser{
OrgID: 1,
},
Logger: &logtest.Fake{},
}
}

View File

@ -8,6 +8,7 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"path" "path"
"strings"
"testing" "testing"
"time" "time"
@ -1363,9 +1364,13 @@ func TestProvisioningApiContactPointExport(t *testing.T) {
}) })
t.Run("decrypt true without alert.provisioning.secrets:read permissions returns 403", func(t *testing.T) { t.Run("decrypt true without alert.provisioning.secrets:read permissions returns 403", func(t *testing.T) {
recPermCheck := false
env := createTestEnv(t, testConfig) env := createTestEnv(t, testConfig)
env.ac = &recordingAccessControlFake{ env.ac = &recordingAccessControlFake{
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingProvisioningReadSecrets) {
recPermCheck = true
}
return false, nil return false, nil
}, },
} }
@ -1377,16 +1382,18 @@ func TestProvisioningApiContactPointExport(t *testing.T) {
response := sut.RouteGetContactPointsExport(&rc) response := sut.RouteGetContactPointsExport(&rc)
require.True(t, recPermCheck)
require.Equal(t, 403, response.Status()) require.Equal(t, 403, response.Status())
require.Len(t, env.ac.EvaluateRecordings, 1)
require.Equal(t, accesscontrol.ActionAlertingProvisioningReadSecrets, env.ac.EvaluateRecordings[0].Evaluator.String())
}) })
t.Run("decrypt true with admin returns 200", func(t *testing.T) { t.Run("decrypt true with admin returns 200", func(t *testing.T) {
recPermCheck := false
env := createTestEnv(t, testConfig) env := createTestEnv(t, testConfig)
env.ac = &recordingAccessControlFake{ env.ac = &recordingAccessControlFake{
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
require.Equal(t, accesscontrol.ActionAlertingProvisioningReadSecrets, evaluator.String()) if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingProvisioningReadSecrets) {
recPermCheck = true
}
return true, nil return true, nil
}, },
} }
@ -1399,9 +1406,8 @@ func TestProvisioningApiContactPointExport(t *testing.T) {
response := sut.RouteGetContactPointsExport(&rc) response := sut.RouteGetContactPointsExport(&rc)
response.WriteTo(&rc) response.WriteTo(&rc)
require.True(t, recPermCheck)
require.Equal(t, 200, response.Status()) require.Equal(t, 200, response.Status())
require.Len(t, env.ac.EvaluateRecordings, 1)
require.Equal(t, accesscontrol.ActionAlertingProvisioningReadSecrets, env.ac.EvaluateRecordings[0].Evaluator.String())
}) })
t.Run("json body content is as expected", func(t *testing.T) { t.Run("json body content is as expected", func(t *testing.T) {

View File

@ -43,10 +43,27 @@ func (api *API) authorize(method, path string) web.Handler {
ac.EvalPermission(ac.ActionAlertingRuleCreate, scope), ac.EvalPermission(ac.ActionAlertingRuleCreate, scope),
ac.EvalPermission(ac.ActionAlertingRuleDelete, scope), ac.EvalPermission(ac.ActionAlertingRuleDelete, scope),
) )
// Grafana rule state history paths // Grafana rule state history paths
case http.MethodGet + "/api/v1/rules/history": case http.MethodGet + "/api/v1/rules/history":
eval = ac.EvalPermission(ac.ActionAlertingRuleRead) eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
// Grafana receivers paths
case http.MethodGet + "/api/v1/notifications/receivers":
// additional authorization is done at the service level
eval = ac.EvalAny(
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
ac.EvalPermission(ac.ActionAlertingReceiversList),
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
)
case http.MethodGet + "/api/v1/notifications/receivers/{Name}":
// TODO: scope to :Name
eval = ac.EvalAny(
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
)
// Grafana unified alerting upgrade paths // Grafana unified alerting upgrade paths
case http.MethodGet + "/api/v1/upgrade/org": case http.MethodGet + "/api/v1/upgrade/org":
return middleware.ReqOrgAdmin return middleware.ReqOrgAdmin

View File

@ -40,7 +40,7 @@ func TestAuthorize(t *testing.T) {
} }
paths[p] = methods paths[p] = methods
} }
require.Len(t, paths, 62) require.Len(t, paths, 64)
ac := acmock.New() ac := acmock.New()
api := &API{AccessControl: ac} api := &API{AccessControl: ac}

View File

@ -19,10 +19,20 @@ import (
) )
type NotificationsApi interface { type NotificationsApi interface {
RouteGetReceiver(*contextmodel.ReqContext) response.Response
RouteGetReceivers(*contextmodel.ReqContext) response.Response
RouteNotificationsGetTimeInterval(*contextmodel.ReqContext) response.Response RouteNotificationsGetTimeInterval(*contextmodel.ReqContext) response.Response
RouteNotificationsGetTimeIntervals(*contextmodel.ReqContext) response.Response RouteNotificationsGetTimeIntervals(*contextmodel.ReqContext) response.Response
} }
func (f *NotificationsApiHandler) RouteGetReceiver(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"]
return f.handleRouteGetReceiver(ctx, nameParam)
}
func (f *NotificationsApiHandler) RouteGetReceivers(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteGetReceivers(ctx)
}
func (f *NotificationsApiHandler) RouteNotificationsGetTimeInterval(ctx *contextmodel.ReqContext) response.Response { func (f *NotificationsApiHandler) RouteNotificationsGetTimeInterval(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters // Parse Path Parameters
nameParam := web.Params(ctx.Req)[":name"] nameParam := web.Params(ctx.Req)[":name"]
@ -34,6 +44,30 @@ func (f *NotificationsApiHandler) RouteNotificationsGetTimeIntervals(ctx *contex
func (api *API) RegisterNotificationsApiEndpoints(srv NotificationsApi, m *metrics.API) { func (api *API) RegisterNotificationsApiEndpoints(srv NotificationsApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) { api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Get(
toMacaronPath("/api/v1/notifications/receivers/{Name}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/v1/notifications/receivers/{Name}"),
metrics.Instrument(
http.MethodGet,
"/api/v1/notifications/receivers/{Name}",
api.Hooks.Wrap(srv.RouteGetReceiver),
m,
),
)
group.Get(
toMacaronPath("/api/v1/notifications/receivers"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/v1/notifications/receivers"),
metrics.Instrument(
http.MethodGet,
"/api/v1/notifications/receivers",
api.Hooks.Wrap(srv.RouteGetReceivers),
m,
),
)
group.Get( group.Get(
toMacaronPath("/api/v1/notifications/time-intervals/{name}"), toMacaronPath("/api/v1/notifications/time-intervals/{name}"),
requestmeta.SetOwner(requestmeta.TeamAlerting), requestmeta.SetOwner(requestmeta.TeamAlerting),

View File

@ -1,34 +1,32 @@
package api package api
import ( import (
"net/http"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
) )
type NotificationsApiHandler struct { type NotificationsApiHandler struct {
muteTimingService MuteTimingService notificationSrv *NotificationSrv
} }
func NewNotificationsApi(muteTimingService MuteTimingService) NotificationsApi { func NewNotificationsApi(notificationSrv *NotificationSrv) *NotificationsApiHandler {
return &NotificationsApiHandler{ return &NotificationsApiHandler{
muteTimingService: muteTimingService, notificationSrv: notificationSrv,
} }
} }
func (f *NotificationsApiHandler) handleRouteNotificationsGetTimeInterval(ctx *contextmodel.ReqContext, name string) response.Response { func (f *NotificationsApiHandler) handleRouteNotificationsGetTimeInterval(ctx *contextmodel.ReqContext, name string) response.Response {
model, err := f.muteTimingService.GetMuteTiming(ctx.Req.Context(), name, ctx.OrgID) return f.notificationSrv.RouteGetTimeInterval(ctx, name)
if err != nil {
return errorToResponse(err)
}
return response.JSON(http.StatusOK, model) // TODO convert to timing interval
} }
func (f *NotificationsApiHandler) handleRouteNotificationsGetTimeIntervals(ctx *contextmodel.ReqContext) response.Response { func (f *NotificationsApiHandler) handleRouteNotificationsGetTimeIntervals(ctx *contextmodel.ReqContext) response.Response {
model, err := f.muteTimingService.GetMuteTimings(ctx.Req.Context(), ctx.OrgID) return f.notificationSrv.RouteGetTimeIntervals(ctx)
if err != nil { }
return errorToResponse(err)
} func (f *NotificationsApiHandler) handleRouteGetReceiver(ctx *contextmodel.ReqContext, name string) response.Response {
return response.JSON(http.StatusOK, model) // TODO convert to timing interval return f.notificationSrv.RouteGetReceiver(ctx, name)
}
func (f *NotificationsApiHandler) handleRouteGetReceivers(ctx *contextmodel.ReqContext) response.Response {
return f.notificationSrv.RouteGetReceivers(ctx)
} }

View File

@ -4511,7 +4511,6 @@
"type": "object" "type": "object"
}, },
"alertGroup": { "alertGroup": {
"description": "AlertGroup alert group",
"properties": { "properties": {
"alerts": { "alerts": {
"description": "alerts", "description": "alerts",
@ -4535,7 +4534,6 @@
"type": "object" "type": "object"
}, },
"alertGroups": { "alertGroups": {
"description": "AlertGroups alert groups",
"items": { "items": {
"$ref": "#/definitions/alertGroup" "$ref": "#/definitions/alertGroup"
}, },
@ -4695,13 +4693,13 @@
"type": "object" "type": "object"
}, },
"gettableAlerts": { "gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": { "items": {
"$ref": "#/definitions/gettableAlert" "$ref": "#/definitions/gettableAlert"
}, },
"type": "array" "type": "array"
}, },
"gettableSilence": { "gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": { "properties": {
"comment": { "comment": {
"description": "comment", "description": "comment",
@ -4750,6 +4748,7 @@
"type": "object" "type": "object"
}, },
"gettableSilences": { "gettableSilences": {
"description": "GettableSilences gettable silences",
"items": { "items": {
"$ref": "#/definitions/gettableSilence" "$ref": "#/definitions/gettableSilence"
}, },
@ -4900,7 +4899,6 @@
"type": "array" "type": "array"
}, },
"postableSilence": { "postableSilence": {
"description": "PostableSilence postable silence",
"properties": { "properties": {
"comment": { "comment": {
"description": "comment", "description": "comment",
@ -5057,61 +5055,6 @@
"version": "1.1.0" "version": "1.1.0"
}, },
"paths": { "paths": {
"/v1/notifications/time-intervals": {
"get": {
"description": "Get all the time intervals",
"operationId": "RouteNotificationsGetTimeIntervals",
"responses": {
"200": {
"$ref": "#/responses/GetAllIntervalsResponse"
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"tags": [
"notifications"
]
}
},
"/v1/notifications/time-intervals/{name}": {
"get": {
"operationId": "RouteNotificationsGetTimeInterval",
"parameters": [
{
"description": "Time interval name",
"in": "path",
"name": "name",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"$ref": "#/responses/GetIntervalsByNameResponse"
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Get a time interval by name.",
"tags": [
"notifications"
]
}
},
"/v1/provisioning/alert-rules": { "/v1/provisioning/alert-rules": {
"get": { "get": {
"operationId": "RouteGetAlertRules", "operationId": "RouteGetAlertRules",
@ -6168,6 +6111,21 @@
"$ref": "#/definitions/GettableTimeIntervals" "$ref": "#/definitions/GettableTimeIntervals"
} }
}, },
"GetReceiverResponse": {
"description": "",
"schema": {
"$ref": "#/definitions/GettableApiReceiver"
}
},
"GetReceiversResponse": {
"description": "",
"schema": {
"items": {
"$ref": "#/definitions/GettableApiReceiver"
},
"type": "array"
}
},
"GettableHistoricUserConfigs": { "GettableHistoricUserConfigs": {
"description": "", "description": "",
"schema": { "schema": {

View File

@ -0,0 +1,56 @@
package definitions
// swagger:route GET /v1/notifications/receivers/{Name} notifications RouteGetReceiver
//
// Get a receiver by name.
//
// Responses:
// 200: GetReceiverResponse
// 403: PermissionDenied
// 404: NotFound
// swagger:route GET /v1/notifications/receivers notifications RouteGetReceivers
//
// Get all receivers.
//
// Responses:
// 200: GetReceiversResponse
// 403: PermissionDenied
// swagger:parameters RouteGetReceiver
type GetReceiverParams struct {
// in:path
// required: true
Name string `json:"name"`
// in:query
// required: false
Decrypt bool `json:"decrypt"`
}
// swagger:parameters RouteGetReceivers
type GetReceiversParams struct {
// in:query
// required: false
Names []string `json:"names"`
// in:query
// required: false
Limit int `json:"limit"`
// in:query
// required: false
Offset int `json:"offset"`
// in:query
// required: false
Decrypt bool `json:"decrypt"`
}
// swagger:response GetReceiverResponse
type GetReceiverResponse struct {
// in:body
Body GettableApiReceiver
}
// swagger:response GetReceiversResponse
type GetReceiversResponse struct {
// in:body
Body []GettableApiReceiver
}

View File

@ -1,6 +1,6 @@
package definitions package definitions
// swagger:route GET /v1/notifications/time-intervals notifications stable RouteNotificationsGetTimeIntervals // swagger:route GET /v1/notifications/time-intervals notifications RouteNotificationsGetTimeIntervals
// //
// Get all the time intervals // Get all the time intervals
// //
@ -8,7 +8,7 @@ package definitions
// 200: GetAllIntervalsResponse // 200: GetAllIntervalsResponse
// 403: ForbiddenError // 403: ForbiddenError
// swagger:route GET /v1/notifications/time-intervals/{name} notifications stable RouteNotificationsGetTimeInterval // swagger:route GET /v1/notifications/time-intervals/{name} notifications RouteNotificationsGetTimeInterval
// //
// Get a time interval by name. // Get a time interval by name.
// //
@ -17,7 +17,7 @@ package definitions
// 404: NotFound // 404: NotFound
// 403: ForbiddenError // 403: ForbiddenError
// swagger:parameters stable RouteNotificationsGetTimeInterval // swagger:parameters RouteNotificationsGetTimeInterval
type RouteTimeIntervalNameParam struct { type RouteTimeIntervalNameParam struct {
// Time interval name // Time interval name
// in:path // in:path

View File

@ -4512,7 +4512,6 @@
"type": "object" "type": "object"
}, },
"alertGroup": { "alertGroup": {
"description": "AlertGroup alert group",
"properties": { "properties": {
"alerts": { "alerts": {
"description": "alerts", "description": "alerts",
@ -4536,6 +4535,7 @@
"type": "object" "type": "object"
}, },
"alertGroups": { "alertGroups": {
"description": "AlertGroups alert groups",
"items": { "items": {
"$ref": "#/definitions/alertGroup" "$ref": "#/definitions/alertGroup"
}, },
@ -4640,6 +4640,7 @@
"type": "object" "type": "object"
}, },
"gettableAlert": { "gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": { "properties": {
"annotations": { "annotations": {
"$ref": "#/definitions/labelSet" "$ref": "#/definitions/labelSet"
@ -4701,7 +4702,6 @@
"type": "array" "type": "array"
}, },
"gettableSilence": { "gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": { "properties": {
"comment": { "comment": {
"description": "comment", "description": "comment",
@ -4756,6 +4756,7 @@
"type": "array" "type": "array"
}, },
"integration": { "integration": {
"description": "Integration integration",
"properties": { "properties": {
"lastNotifyAttempt": { "lastNotifyAttempt": {
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
@ -4936,7 +4937,6 @@
"type": "object" "type": "object"
}, },
"receiver": { "receiver": {
"description": "Receiver receiver",
"properties": { "properties": {
"active": { "active": {
"description": "active", "description": "active",
@ -7046,6 +7046,86 @@
] ]
} }
}, },
"/v1/notifications/receivers": {
"get": {
"operationId": "RouteGetReceivers",
"parameters": [
{
"in": "query",
"items": {
"type": "string"
},
"name": "names",
"type": "array"
},
{
"format": "int64",
"in": "query",
"name": "limit",
"type": "integer"
},
{
"format": "int64",
"in": "query",
"name": "offset",
"type": "integer"
},
{
"in": "query",
"name": "decrypt",
"type": "boolean"
}
],
"responses": {
"200": {
"$ref": "#/responses/GetReceiversResponse"
},
"403": {
"description": "PermissionDenied",
"schema": {
"$ref": "#/definitions/PermissionDenied"
}
}
},
"summary": "Get all receivers.",
"tags": [
"notifications"
]
}
},
"/v1/notifications/receivers/{Name}": {
"get": {
"operationId": "RouteGetReceiver",
"parameters": [
{
"in": "path",
"name": "name",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "decrypt",
"type": "boolean"
}
],
"responses": {
"200": {
"$ref": "#/responses/GetReceiverResponse"
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Get a receiver by name.",
"tags": [
"notifications"
]
}
},
"/v1/notifications/time-intervals": { "/v1/notifications/time-intervals": {
"get": { "get": {
"description": "Get all the time intervals", "description": "Get all the time intervals",
@ -8507,6 +8587,21 @@
"$ref": "#/definitions/GettableTimeIntervals" "$ref": "#/definitions/GettableTimeIntervals"
} }
}, },
"GetReceiverResponse": {
"description": "",
"schema": {
"$ref": "#/definitions/GettableApiReceiver"
}
},
"GetReceiversResponse": {
"description": "",
"schema": {
"items": {
"$ref": "#/definitions/GettableApiReceiver"
},
"type": "array"
}
},
"GettableHistoricUserConfigs": { "GettableHistoricUserConfigs": {
"description": "", "description": "",
"schema": { "schema": {

View File

@ -2007,12 +2007,91 @@
} }
} }
}, },
"/v1/notifications/receivers": {
"get": {
"tags": [
"notifications"
],
"summary": "Get all receivers.",
"operationId": "RouteGetReceivers",
"parameters": [
{
"type": "array",
"items": {
"type": "string"
},
"name": "names",
"in": "query"
},
{
"type": "integer",
"format": "int64",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"format": "int64",
"name": "offset",
"in": "query"
},
{
"type": "boolean",
"name": "decrypt",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/GetReceiversResponse"
},
"403": {
"description": "PermissionDenied",
"schema": {
"$ref": "#/definitions/PermissionDenied"
}
}
}
}
},
"/v1/notifications/receivers/{Name}": {
"get": {
"tags": [
"notifications"
],
"summary": "Get a receiver by name.",
"operationId": "RouteGetReceiver",
"parameters": [
{
"type": "string",
"name": "name",
"in": "path",
"required": true
},
{
"type": "boolean",
"name": "decrypt",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/GetReceiverResponse"
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
}
},
"/v1/notifications/time-intervals": { "/v1/notifications/time-intervals": {
"get": { "get": {
"description": "Get all the time intervals", "description": "Get all the time intervals",
"tags": [ "tags": [
"notifications", "notifications"
"stable"
], ],
"operationId": "RouteNotificationsGetTimeIntervals", "operationId": "RouteNotificationsGetTimeIntervals",
"responses": { "responses": {
@ -2031,8 +2110,7 @@
"/v1/notifications/time-intervals/{name}": { "/v1/notifications/time-intervals/{name}": {
"get": { "get": {
"tags": [ "tags": [
"notifications", "notifications"
"stable"
], ],
"summary": "Get a time interval by name.", "summary": "Get a time interval by name.",
"operationId": "RouteNotificationsGetTimeInterval", "operationId": "RouteNotificationsGetTimeInterval",
@ -7994,7 +8072,6 @@
} }
}, },
"alertGroup": { "alertGroup": {
"description": "AlertGroup alert group",
"type": "object", "type": "object",
"required": [ "required": [
"alerts", "alerts",
@ -8019,6 +8096,7 @@
"$ref": "#/definitions/alertGroup" "$ref": "#/definitions/alertGroup"
}, },
"alertGroups": { "alertGroups": {
"description": "AlertGroups alert groups",
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/alertGroup" "$ref": "#/definitions/alertGroup"
@ -8124,6 +8202,7 @@
} }
}, },
"gettableAlert": { "gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object", "type": "object",
"required": [ "required": [
"labels", "labels",
@ -8187,7 +8266,6 @@
"$ref": "#/definitions/gettableAlerts" "$ref": "#/definitions/gettableAlerts"
}, },
"gettableSilence": { "gettableSilence": {
"description": "GettableSilence gettable silence",
"type": "object", "type": "object",
"required": [ "required": [
"comment", "comment",
@ -8244,6 +8322,7 @@
"$ref": "#/definitions/gettableSilences" "$ref": "#/definitions/gettableSilences"
}, },
"integration": { "integration": {
"description": "Integration integration",
"type": "object", "type": "object",
"required": [ "required": [
"name", "name",
@ -8426,7 +8505,6 @@
"$ref": "#/definitions/postableSilence" "$ref": "#/definitions/postableSilence"
}, },
"receiver": { "receiver": {
"description": "Receiver receiver",
"type": "object", "type": "object",
"required": [ "required": [
"active", "active",
@ -8557,6 +8635,21 @@
"$ref": "#/definitions/GettableTimeIntervals" "$ref": "#/definitions/GettableTimeIntervals"
} }
}, },
"GetReceiverResponse": {
"description": "",
"schema": {
"$ref": "#/definitions/GettableApiReceiver"
}
},
"GetReceiversResponse": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/GettableApiReceiver"
}
}
},
"GettableHistoricUserConfigs": { "GettableHistoricUserConfigs": {
"description": "", "description": "",
"schema": { "schema": {

View File

@ -1,5 +1,12 @@
package models package models
// GetReceiverQuery represents a query for a single receiver.
type GetReceiverQuery struct {
OrgID int64
Name string
Decrypt bool
}
// GetReceiversQuery represents a query for receiver groups. // GetReceiversQuery represents a query for receiver groups.
type GetReceiversQuery struct { type GetReceiversQuery struct {
OrgID int64 OrgID int64

View File

@ -46,19 +46,19 @@ func PostableApiAlertingConfigToApiReceivers(c apimodels.PostableApiAlertingConf
type DecryptFn = func(value string) string type DecryptFn = func(value string) string
func PostableToGettableGrafanaReceiver(r *apimodels.PostableGrafanaReceiver, provenance *models.Provenance, decryptFn DecryptFn) (apimodels.GettableGrafanaReceiver, error) { func PostableToGettableGrafanaReceiver(r *apimodels.PostableGrafanaReceiver, provenance *models.Provenance, decryptFn DecryptFn, listOnly bool) (apimodels.GettableGrafanaReceiver, error) {
out := apimodels.GettableGrafanaReceiver{ out := apimodels.GettableGrafanaReceiver{
UID: r.UID, UID: r.UID,
Name: r.Name, Name: r.Name,
Type: r.Type, Type: r.Type,
DisableResolveMessage: r.DisableResolveMessage,
SecureFields: make(map[string]bool, len(r.SecureSettings)),
} }
if provenance != nil { if provenance != nil {
out.Provenance = apimodels.Provenance(*provenance) out.Provenance = apimodels.Provenance(*provenance)
} }
// if we aren't only listing, include the settings in the output
if !listOnly {
secureFields := make(map[string]bool, len(r.SecureSettings))
settings, err := simplejson.NewJson([]byte(r.Settings)) settings, err := simplejson.NewJson([]byte(r.Settings))
if err != nil { if err != nil {
return apimodels.GettableGrafanaReceiver{}, err return apimodels.GettableGrafanaReceiver{}, err
@ -74,19 +74,23 @@ func PostableToGettableGrafanaReceiver(r *apimodels.PostableGrafanaReceiver, pro
} else { } else {
settings.Set(k, decryptedValue) settings.Set(k, decryptedValue)
} }
out.SecureFields[k] = true secureFields[k] = true
} }
jsonBytes, err := settings.MarshalJSON() jsonBytes, err := settings.MarshalJSON()
if err != nil { if err != nil {
return apimodels.GettableGrafanaReceiver{}, err return apimodels.GettableGrafanaReceiver{}, err
} }
out.Settings = jsonBytes out.Settings = jsonBytes
out.SecureFields = secureFields
out.DisableResolveMessage = r.DisableResolveMessage
}
return out, nil return out, nil
} }
func PostableToGettableApiReceiver(r *apimodels.PostableApiReceiver, provenances map[string]models.Provenance, decryptFn DecryptFn) (apimodels.GettableApiReceiver, error) { func PostableToGettableApiReceiver(r *apimodels.PostableApiReceiver, provenances map[string]models.Provenance, decryptFn DecryptFn, listOnly bool) (apimodels.GettableApiReceiver, error) {
out := apimodels.GettableApiReceiver{ out := apimodels.GettableApiReceiver{
Receiver: config.Receiver{ Receiver: config.Receiver{
Name: r.Receiver.Name, Name: r.Receiver.Name,
@ -99,7 +103,7 @@ func PostableToGettableApiReceiver(r *apimodels.PostableApiReceiver, provenances
prov = &p prov = &p
} }
gettable, err := PostableToGettableGrafanaReceiver(gr, prov, decryptFn) gettable, err := PostableToGettableGrafanaReceiver(gr, prov, decryptFn, listOnly)
if err != nil { if err != nil {
return apimodels.GettableApiReceiver{}, err return apimodels.GettableApiReceiver{}, err
} }

View File

@ -18,6 +18,8 @@ import (
var ( var (
// ErrPermissionDenied is returned when the user does not have permission to perform the requested action. // ErrPermissionDenied is returned when the user does not have permission to perform the requested action.
ErrPermissionDenied = errors.New("permission denied") // TODO: convert to errutil ErrPermissionDenied = errors.New("permission denied") // TODO: convert to errutil
// ErrNotFound is returned when the requested resource does not exist.
ErrNotFound = errors.New("not found") // TODO: convert to errutil
) )
// ReceiverService is the service for managing alertmanager receivers. // ReceiverService is the service for managing alertmanager receivers.
@ -61,19 +63,71 @@ func NewReceiverService(
} }
} }
func (rs *ReceiverService) canDecrypt(ctx context.Context, user identity.Requester, name string) (bool, error) { func (rs *ReceiverService) shouldDecrypt(ctx context.Context, user identity.Requester, name string, reqDecrypt bool) (bool, error) {
receiverAccess := false // TODO: stub, check for read secrets access // TODO: migrate to new permission
eval := accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets) eval := accesscontrol.EvalAny(
provisioningAccess, err := rs.ac.Evaluate(ctx, user, eval) accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversReadSecrets),
accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets),
)
decryptAccess, err := rs.ac.Evaluate(ctx, user, eval)
if err != nil { if err != nil {
return false, err return false, err
} }
return receiverAccess || provisioningAccess, nil
if reqDecrypt && !decryptAccess {
return false, ErrPermissionDenied
}
return decryptAccess && reqDecrypt, nil
}
// GetReceiver returns a receiver by name.
// The receiver's secure settings are decrypted if requested and the user has access to do so.
func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiverQuery, user identity.Requester) (definitions.GettableApiReceiver, error) {
if q.Decrypt && user == nil {
return definitions.GettableApiReceiver{}, ErrPermissionDenied
}
baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, q.OrgID)
if err != nil {
return definitions.GettableApiReceiver{}, err
}
cfg := definitions.PostableUserConfig{}
err = json.Unmarshal([]byte(baseCfg.AlertmanagerConfiguration), &cfg)
if err != nil {
return definitions.GettableApiReceiver{}, err
}
provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, "contactPoint")
if err != nil {
return definitions.GettableApiReceiver{}, err
}
receivers := cfg.AlertmanagerConfig.Receivers
for _, r := range receivers {
if r.Name == q.Name {
decrypt, err := rs.shouldDecrypt(ctx, user, q.Name, q.Decrypt)
if err != nil {
return definitions.GettableApiReceiver{}, err
}
decryptFn := rs.decryptOrRedact(ctx, decrypt, q.Name, "")
return PostableToGettableApiReceiver(r, provenances, decryptFn, false)
}
}
return definitions.GettableApiReceiver{}, ErrNotFound
} }
// GetReceivers returns a list of receivers a user has access to. // GetReceivers returns a list of receivers a user has access to.
// Receivers can be filtered by name, and secure settings are decrypted if requested and the user has access to do so. // Receivers can be filtered by name, and secure settings are decrypted if requested and the user has access to do so.
func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceiversQuery, user identity.Requester) ([]definitions.GettableApiReceiver, error) { func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceiversQuery, user identity.Requester) ([]definitions.GettableApiReceiver, error) {
if q.Decrypt && user == nil {
return nil, ErrPermissionDenied
}
baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, q.OrgID) baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, q.OrgID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -90,7 +144,11 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
return nil, err return nil, err
} }
// TODO: check for list access eval := accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversList)
listAccess, err := rs.ac.Evaluate(ctx, user, eval)
if err != nil {
return nil, err
}
var output []definitions.GettableApiReceiver var output []definitions.GettableApiReceiver
for i := q.Offset; i < len(cfg.AlertmanagerConfig.Receivers); i++ { for i := q.Offset; i < len(cfg.AlertmanagerConfig.Receivers); i++ {
@ -99,24 +157,18 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
continue continue
} }
// TODO: check for scoped read access and continue if not allowed decrypt, err := rs.shouldDecrypt(ctx, user, r.Name, q.Decrypt)
decryptAccess, err := rs.canDecrypt(ctx, user, r.Name)
if err != nil {
return nil, err
}
if q.Decrypt && !decryptAccess {
return nil, ErrPermissionDenied
}
decryptFn := rs.decryptOrRedact(ctx, decryptAccess && q.Decrypt, r.Name, "")
res, err := PostableToGettableApiReceiver(r, provenances, decryptFn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: redact settings if the user only has list access decryptFn := rs.decryptOrRedact(ctx, decrypt, r.Name, "")
listOnly := !decrypt && listAccess
res, err := PostableToGettableApiReceiver(r, provenances, decryptFn, listOnly)
if err != nil {
return nil, err
}
output = append(output, res) output = append(output, res)
// stop if we have reached the limit or we have found all the requested receivers // stop if we have reached the limit or we have found all the requested receivers

View File

@ -3,6 +3,7 @@ package notifier
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"testing" "testing"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
@ -22,24 +23,46 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestReceiverService_GetReceiver(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
t.Run("service gets receiver from AM config", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
Receiver, err := sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), nil)
require.NoError(t, err)
require.Equal(t, "slack receiver", Receiver.Name)
require.Len(t, Receiver.GrafanaManagedReceivers, 1)
require.Equal(t, "UID2", Receiver.GrafanaManagedReceivers[0].UID)
})
t.Run("service returns error when receiver does not exist", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
_, err := sut.GetReceiver(context.Background(), singleQ(1, "nonexistent"), nil)
require.ErrorIs(t, err, ErrNotFound)
})
}
func TestReceiverService_GetReceivers(t *testing.T) { func TestReceiverService_GetReceivers(t *testing.T) {
sqlStore := db.InitTestDB(t) sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
t.Run("service gets receiver groups from AM config", func(t *testing.T) { t.Run("service gets receivers from AM config", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService) sut := createReceiverServiceSut(t, secretsService)
Receivers, err := sut.GetReceivers(context.Background(), rQuery(1), nil) Receivers, err := sut.GetReceivers(context.Background(), multiQ(1), nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, Receivers, 2) require.Len(t, Receivers, 2)
require.Equal(t, "grafana-default-email", Receivers[0].Name) require.Equal(t, "grafana-default-email", Receivers[0].Name)
require.Equal(t, "slack receiver", Receivers[1].Name) require.Equal(t, "slack receiver", Receivers[1].Name)
}) })
t.Run("service filters receiver groups by name", func(t *testing.T) { t.Run("service filters receivers by name", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService) sut := createReceiverServiceSut(t, secretsService)
Receivers, err := sut.GetReceivers(context.Background(), rQuery(1, "slack receiver"), nil) Receivers, err := sut.GetReceivers(context.Background(), multiQ(1, "slack receiver"), nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, Receivers, 1) require.Len(t, Receivers, 1)
require.Equal(t, "slack receiver", Receivers[0].Name) require.Equal(t, "slack receiver", Receivers[0].Name)
@ -51,72 +74,95 @@ func TestReceiverService_DecryptRedact(t *testing.T) {
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
ac := acimpl.ProvideAccessControl(setting.NewCfg()) ac := acimpl.ProvideAccessControl(setting.NewCfg())
t.Run("service redacts receiver groups by default", func(t *testing.T) { getMethods := []string{"single", "multi"}
sut := createReceiverServiceSut(t, secretsService)
Receivers, err := sut.GetReceivers(context.Background(), rQuery(1, "slack receiver"), nil)
require.NoError(t, err)
require.Len(t, Receivers, 1) readUser := &user.SignedInUser{
rGroup := Receivers[0]
require.Equal(t, "slack receiver", rGroup.Name)
require.Len(t, rGroup.GrafanaManagedReceivers, 1)
grafanaReceiver := rGroup.GrafanaManagedReceivers[0]
require.Equal(t, "UID2", grafanaReceiver.UID)
testedSettings, err := simplejson.NewJson([]byte(grafanaReceiver.Settings))
require.NoError(t, err)
require.Equal(t, definitions.RedactedValue, testedSettings.Get("url").MustString())
})
t.Run("service returns error when trying to decrypt with nil user", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
sut.ac = ac
q := rQuery(1)
q.Decrypt = true
_, err := sut.GetReceivers(context.Background(), q, nil)
require.ErrorIs(t, err, ErrPermissionDenied)
})
t.Run("service returns error when trying to decrypt without permission", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
sut.ac = ac
q := rQuery(1)
q.Decrypt = true
_, err := sut.GetReceivers(context.Background(), q, &user.SignedInUser{})
require.ErrorIs(t, err, ErrPermissionDenied)
})
t.Run("service decrypts receiver groups with permission", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
sut.ac = ac
q := rQuery(1, "slack receiver")
q.Decrypt = true
Receivers, err := sut.GetReceivers(context.Background(), q, &user.SignedInUser{
OrgID: 1, OrgID: 1,
Permissions: map[int64]map[string][]string{ Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionAlertingProvisioningReadSecrets: nil}, 1: {accesscontrol.ActionAlertingProvisioningRead: nil},
}, },
}) }
secretUser := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingProvisioningRead: nil,
accesscontrol.ActionAlertingProvisioningReadSecrets: nil,
},
},
}
for _, tc := range []struct {
name string
decrypt bool
user *user.SignedInUser
err error
}{
{
name: "service redacts receivers by default",
decrypt: false,
user: readUser,
err: nil,
},
{
name: "service returns error when trying to decrypt without permission",
decrypt: true,
user: readUser,
err: ErrPermissionDenied,
},
{
name: "service returns error if user is nil and decrypt is true",
decrypt: true,
user: nil,
err: ErrPermissionDenied,
},
{
name: "service decrypts receivers with permission",
decrypt: true,
user: secretUser,
err: nil,
},
} {
for _, method := range getMethods {
t.Run(fmt.Sprintf("%s %s", tc.name, method), func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
sut.ac = ac
var res definitions.GettableApiReceiver
var err error
if method == "single" {
q := singleQ(1, "slack receiver")
q.Decrypt = tc.decrypt
res, err = sut.GetReceiver(context.Background(), q, tc.user)
} else {
q := multiQ(1, "slack receiver")
q.Decrypt = tc.decrypt
var multiRes []definitions.GettableApiReceiver
multiRes, err = sut.GetReceivers(context.Background(), q, tc.user)
if tc.err == nil {
require.Len(t, multiRes, 1)
res = multiRes[0]
}
}
require.ErrorIs(t, err, tc.err)
if tc.err == nil {
require.Equal(t, "slack receiver", res.Name)
require.Len(t, res.GrafanaManagedReceivers, 1)
require.Equal(t, "UID2", res.GrafanaManagedReceivers[0].UID)
testedSettings, err := simplejson.NewJson([]byte(res.GrafanaManagedReceivers[0].Settings))
require.NoError(t, err) require.NoError(t, err)
if tc.decrypt {
require.Len(t, Receivers, 1) require.Equal(t, "secure url", testedSettings.Get("url").MustString())
} else {
rGroup := Receivers[0] require.Equal(t, definitions.RedactedValue, testedSettings.Get("url").MustString())
require.Equal(t, "slack receiver", rGroup.Name) }
require.Len(t, rGroup.GrafanaManagedReceivers, 1) }
grafanaReceiver := rGroup.GrafanaManagedReceivers[0]
require.Equal(t, "UID2", grafanaReceiver.UID)
settings, err := simplejson.NewJson([]byte(grafanaReceiver.Settings))
require.NoError(t, err)
require.Equal(t, "secure url", settings.Get("url").MustString())
}) })
}
}
} }
func createReceiverServiceSut(t *testing.T, encryptSvc secrets.Service) *ReceiverService { func createReceiverServiceSut(t *testing.T, encryptSvc secrets.Service) *ReceiverService {
@ -148,7 +194,14 @@ func createEncryptedConfig(t *testing.T, secretService secrets.Service) string {
return string(bytes) return string(bytes)
} }
func rQuery(orgID int64, names ...string) models.GetReceiversQuery { func singleQ(orgID int64, name string) models.GetReceiverQuery {
return models.GetReceiverQuery{
OrgID: orgID,
Name: name,
}
}
func multiQ(orgID int64, names ...string) models.GetReceiversQuery {
return models.GetReceiversQuery{ return models.GetReceiversQuery{
OrgID: orgID, OrgID: orgID,
Names: names, Names: names,

View File

@ -278,7 +278,7 @@ func TestContactPointServiceDecryptRedact(t *testing.T) {
q := cpsQuery(1) q := cpsQuery(1)
q.Decrypt = true q.Decrypt = true
_, err := sut.GetContactPoints(context.Background(), q, &user.SignedInUser{}) _, err := sut.GetContactPoints(context.Background(), q, nil)
require.ErrorIs(t, err, ErrPermissionDenied) require.ErrorIs(t, err, ErrPermissionDenied)
}) })
t.Run("GetContactPoints errors when Decrypt = true and user is nil", func(t *testing.T) { t.Run("GetContactPoints errors when Decrypt = true and user is nil", func(t *testing.T) {

View File

@ -0,0 +1,60 @@
package fakes
import (
"context"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
type ReceiverServiceMethodCall struct {
Method string
Args []interface{}
}
type FakeReceiverService struct {
MethodCalls []ReceiverServiceMethodCall
GetReceiverFn func(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error)
GetReceiversFn func(ctx context.Context, q models.GetReceiversQuery, u identity.Requester) ([]definitions.GettableApiReceiver, error)
}
func NewFakeReceiverService() *FakeReceiverService {
return &FakeReceiverService{
GetReceiverFn: defaultReceiverFn,
GetReceiversFn: defaultReceiversFn,
}
}
func (f *FakeReceiverService) GetReceiver(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error) {
f.MethodCalls = append(f.MethodCalls, ReceiverServiceMethodCall{Method: "GetReceiver", Args: []interface{}{ctx, q}})
return f.GetReceiverFn(ctx, q, u)
}
func (f *FakeReceiverService) GetReceivers(ctx context.Context, q models.GetReceiversQuery, u identity.Requester) ([]definitions.GettableApiReceiver, error) {
f.MethodCalls = append(f.MethodCalls, ReceiverServiceMethodCall{Method: "GetReceivers", Args: []interface{}{ctx, q}})
return f.GetReceiversFn(ctx, q, u)
}
func (f *FakeReceiverService) PopMethodCall() ReceiverServiceMethodCall {
if len(f.MethodCalls) == 0 {
return ReceiverServiceMethodCall{}
}
call := f.MethodCalls[len(f.MethodCalls)-1]
f.MethodCalls = f.MethodCalls[:len(f.MethodCalls)-1]
return call
}
func (f *FakeReceiverService) Reset() {
f.MethodCalls = nil
f.GetReceiverFn = defaultReceiverFn
f.GetReceiversFn = defaultReceiversFn
}
func defaultReceiverFn(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error) {
return definitions.GettableApiReceiver{}, nil
}
func defaultReceiversFn(ctx context.Context, q models.GetReceiversQuery, u identity.Requester) ([]definitions.GettableApiReceiver, error) {
return nil, nil
}

View File

@ -10355,61 +10355,6 @@
} }
} }
}, },
"/v1/notifications/time-intervals": {
"get": {
"description": "Get all the time intervals",
"tags": [
"notifications"
],
"operationId": "RouteNotificationsGetTimeIntervals",
"responses": {
"200": {
"$ref": "#/responses/GetAllIntervalsResponse"
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
},
"/v1/notifications/time-intervals/{name}": {
"get": {
"tags": [
"notifications"
],
"summary": "Get a time interval by name.",
"operationId": "RouteNotificationsGetTimeInterval",
"parameters": [
{
"type": "string",
"description": "Time interval name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/GetIntervalsByNameResponse"
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
}
},
"/v1/provisioning/alert-rules": { "/v1/provisioning/alert-rules": {
"get": { "get": {
"tags": [ "tags": [
@ -21704,7 +21649,6 @@
} }
}, },
"alertGroup": { "alertGroup": {
"description": "AlertGroup alert group",
"type": "object", "type": "object",
"required": [ "required": [
"alerts", "alerts",
@ -21728,7 +21672,6 @@
} }
}, },
"alertGroups": { "alertGroups": {
"description": "AlertGroups alert groups",
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/alertGroup" "$ref": "#/definitions/alertGroup"
@ -21916,13 +21859,13 @@
} }
}, },
"gettableAlerts": { "gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/gettableAlert" "$ref": "#/definitions/gettableAlert"
} }
}, },
"gettableSilence": { "gettableSilence": {
"description": "GettableSilence gettable silence",
"type": "object", "type": "object",
"required": [ "required": [
"comment", "comment",
@ -21971,6 +21914,7 @@
} }
}, },
"gettableSilences": { "gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/gettableSilence" "$ref": "#/definitions/gettableSilence"
@ -22121,7 +22065,6 @@
} }
}, },
"postableSilence": { "postableSilence": {
"description": "PostableSilence postable silence",
"type": "object", "type": "object",
"required": [ "required": [
"comment", "comment",
@ -22388,6 +22331,21 @@
"$ref": "#/definitions/GettableTimeIntervals" "$ref": "#/definitions/GettableTimeIntervals"
} }
}, },
"GetReceiverResponse": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/GettableApiReceiver"
}
},
"GetReceiversResponse": {
"description": "(empty)",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/GettableApiReceiver"
}
}
},
"GettableHistoricUserConfigs": { "GettableHistoricUserConfigs": {
"description": "(empty)", "description": "(empty)",
"schema": { "schema": {

View File

@ -24,6 +24,29 @@
}, },
"description": "(empty)" "description": "(empty)"
}, },
"GetReceiverResponse": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GettableApiReceiver"
}
}
},
"description": "(empty)"
},
"GetReceiversResponse": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/GettableApiReceiver"
},
"type": "array"
}
}
},
"description": "(empty)"
},
"GettableHistoricUserConfigs": { "GettableHistoricUserConfigs": {
"content": { "content": {
"application/json": { "application/json": {
@ -12194,7 +12217,6 @@
"type": "object" "type": "object"
}, },
"alertGroup": { "alertGroup": {
"description": "AlertGroup alert group",
"properties": { "properties": {
"alerts": { "alerts": {
"description": "alerts", "description": "alerts",
@ -12218,7 +12240,6 @@
"type": "object" "type": "object"
}, },
"alertGroups": { "alertGroups": {
"description": "AlertGroups alert groups",
"items": { "items": {
"$ref": "#/components/schemas/alertGroup" "$ref": "#/components/schemas/alertGroup"
}, },
@ -12406,13 +12427,13 @@
"type": "object" "type": "object"
}, },
"gettableAlerts": { "gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": { "items": {
"$ref": "#/components/schemas/gettableAlert" "$ref": "#/components/schemas/gettableAlert"
}, },
"type": "array" "type": "array"
}, },
"gettableSilence": { "gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": { "properties": {
"comment": { "comment": {
"description": "comment", "description": "comment",
@ -12461,6 +12482,7 @@
"type": "object" "type": "object"
}, },
"gettableSilences": { "gettableSilences": {
"description": "GettableSilences gettable silences",
"items": { "items": {
"$ref": "#/components/schemas/gettableSilence" "$ref": "#/components/schemas/gettableSilence"
}, },
@ -12611,7 +12633,6 @@
"type": "array" "type": "array"
}, },
"postableSilence": { "postableSilence": {
"description": "PostableSilence postable silence",
"properties": { "properties": {
"comment": { "comment": {
"description": "comment", "description": "comment",
@ -23973,75 +23994,6 @@
] ]
} }
}, },
"/v1/notifications/time-intervals": {
"get": {
"description": "Get all the time intervals",
"operationId": "RouteNotificationsGetTimeIntervals",
"responses": {
"200": {
"$ref": "#/components/responses/GetAllIntervalsResponse"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ForbiddenError"
}
}
},
"description": "ForbiddenError"
}
},
"tags": [
"notifications"
]
}
},
"/v1/notifications/time-intervals/{name}": {
"get": {
"operationId": "RouteNotificationsGetTimeInterval",
"parameters": [
{
"description": "Time interval name",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/GetIntervalsByNameResponse"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ForbiddenError"
}
}
},
"description": "ForbiddenError"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFound"
}
}
},
"description": "NotFound"
}
},
"summary": "Get a time interval by name.",
"tags": [
"notifications"
]
}
},
"/v1/provisioning/alert-rules": { "/v1/provisioning/alert-rules": {
"get": { "get": {
"operationId": "RouteGetAlertRules", "operationId": "RouteGetAlertRules",