grafana/pkg/services/ngalert/api/api_notifications_test.go
Matthew Jacobson d5fd6aceca
Alerting: Stop redacting receivers by default in receiver_svc (#92631)
* Stop redacting receivers by default in receiver_svc

[REDACTED] is only used in provisioning API since response doesn't include
SecureFields. This is not necessary in k8s or notifications api, instead we do
not include the encrypted settings in Settings at all, leaving it to
SecureFields to specify when a secure field exists.

* Capitalize logs messages
2024-08-29 14:48:59 -04:00

406 lines
16 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"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/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"
)
func TestRouteGetReceiver(t *testing.T) {
fakeReceiverSvc := fakes.NewFakeReceiverService()
t.Run("returns expected model", func(t *testing.T) {
expected := &models.Receiver{
Name: "receiver1",
Integrations: []*models.Integration{
{
UID: "uid1",
Name: "receiver1",
Config: models.IntegrationConfig{Type: "slack"},
},
},
}
fakeReceiverSvc.GetReceiverFn = func(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (*models.Receiver, error) {
return expected, nil
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
resp := handler.handleRouteGetReceiver(&rc, "receiver1")
require.Equal(t, http.StatusOK, resp.Status())
gettables, err := GettableApiReceiverFromReceiver(expected)
require.NoError(t, err)
json, err := json.Marshal(gettables)
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) (*models.Receiver, error) {
return &models.Receiver{}, 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) (*models.Receiver, error) {
return nil, legacy_storage.ErrReceiverNotFound.Errorf("")
}
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) (*models.Receiver, error) {
return nil, ac.ErrAuthorizationBase.Errorf("")
}
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 := []*models.Receiver{
{
Name: "receiver1",
Integrations: []*models.Integration{
{
UID: "uid1",
Name: "receiver1",
Config: models.IntegrationConfig{Type: "slack"},
},
},
},
}
fakeReceiverSvc.ListReceiversFn = func(ctx context.Context, q models.ListReceiversQuery, u identity.Requester) ([]*models.Receiver, 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())
gettables, err := GettableApiReceiversFromReceivers(expected)
require.NoError(t, err)
jsonBody, err := json.Marshal(gettables)
require.NoError(t, err)
require.JSONEq(t, string(jsonBody), string(resp.Body()))
})
t.Run("builds query from request context", func(t *testing.T) {
fakeReceiverSvc.ListReceiversFn = func(ctx context.Context, q models.ListReceiversQuery, u identity.Requester) ([]*models.Receiver, error) {
return nil, 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, "ListReceivers", call.Method)
expectedQ := models.ListReceiversQuery{
Names: []string{"receiver1", "receiver2"},
Limit: 1,
Offset: 2,
OrgID: 1,
}
require.Equal(t, expectedQ, call.Args[1])
})
t.Run("should pass along permission denied response", func(t *testing.T) {
fakeReceiverSvc.ListReceiversFn = func(ctx context.Context, q models.ListReceiversQuery, u identity.Requester) ([]*models.Receiver, error) {
return nil, ac.ErrAuthorizationBase.Errorf("")
}
handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc))
rc := testReqCtx("GET")
resp := handler.handleRouteGetReceivers(&rc)
require.Equal(t, http.StatusForbidden, resp.Status())
})
}
func TestRouteGetReceiversResponses(t *testing.T) {
createTestEnv := func(t *testing.T, testConfig string) testEnvironment {
env := createTestEnv(t, testConfig)
env.ac = &recordingAccessControlFake{
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingNotificationsRead) {
return true, nil
}
if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingReceiversList) {
return true, nil
}
return false, nil
},
}
return env
}
t.Run("list receivers", func(t *testing.T) {
t.Run("GET returns 200", func(t *testing.T) {
env := createTestEnv(t, testConfig)
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
response := sut.RouteGetReceivers(&rc)
require.Equal(t, 200, response.Status())
})
t.Run("json body content is as expected", func(t *testing.T) {
expectedListResponse := `[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"ad95bd8a-49ed-4adc-bf89-1b444fa1aa5b","name":"grafana-default-email","type":"email","disableResolveMessage":false,"secureFields":{}}]},{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":false,"secureFields":{}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"secureFields":{}}]},{"name":"pagerduty test","grafana_managed_receiver_configs":[{"uid":"b9bf06f8-bde2-4438-9d4a-bba0522dcd4d","name":"pagerduty test","type":"pagerduty","disableResolveMessage":false,"secureFields":{}}]},{"name":"slack test","grafana_managed_receiver_configs":[{"uid":"cbfd0976-8228-4126-b672-4419f30a9e50","name":"slack test","type":"slack","disableResolveMessage":false,"secureFields":{}}]}]`
t.Run("limit offset", func(t *testing.T) {
env := createTestEnv(t, testContactPointConfig)
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rc.Context.Req.Header.Add("Accept", "application/json")
rc.Context.Req.Form.Set("decrypt", "false")
var expected []definitions.GettableApiReceiver
err := json.Unmarshal([]byte(expectedListResponse), &expected)
require.NoError(t, err)
type testcase struct {
limit int
offset int
expected []definitions.GettableApiReceiver
}
testcases := []testcase{
{limit: 1, offset: 0, expected: expected[:1]},
{limit: 2, offset: 0, expected: expected[:2]},
{limit: 4, offset: 0, expected: expected[:4]},
{limit: 1, offset: 1, expected: expected[1:2]},
{limit: 2, offset: 2, expected: expected[2:4]},
{limit: 2, offset: 99, expected: []definitions.GettableApiReceiver{}},
{limit: 0, offset: 0, expected: expected},
{limit: 0, offset: 1, expected: expected[1:]},
}
for _, tc := range testcases {
t.Run(fmt.Sprintf("limit %d offset %d", tc.limit, tc.offset), func(t *testing.T) {
rc.Context.Req.Form.Set("limit", strconv.Itoa(tc.limit))
rc.Context.Req.Form.Set("offset", strconv.Itoa(tc.offset))
response := sut.RouteGetReceivers(&rc)
require.Equal(t, 200, response.Status())
var configs []definitions.GettableApiReceiver
err := json.Unmarshal(response.Body(), &configs)
require.NoError(t, err)
require.Equal(t, tc.expected, configs)
})
}
})
t.Run("decrypt false with read permissions, does not have settings", func(t *testing.T) {
env := createTestEnv(t, testContactPointConfig)
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rc.Context.Req.Header.Add("Accept", "application/json")
rc.Context.Req.Form.Set("decrypt", "false")
response := sut.RouteGetReceivers(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, expectedListResponse, string(response.Body()))
})
t.Run("decrypt false with only list permissions, does not have settings", func(t *testing.T) {
env := createTestEnv(t, testContactPointConfig)
env.ac = &recordingAccessControlFake{
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingReceiversList) {
return true, nil
}
return false, nil
},
}
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rc.Context.Req.Header.Add("Accept", "application/json")
rc.Context.Req.Form.Set("decrypt", "false")
response := sut.RouteGetReceivers(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, expectedListResponse, string(response.Body()))
})
t.Run("decrypt true with all permissions, does not have settings", func(t *testing.T) {
env := createTestEnv(t, testContactPointConfig)
env.ac = &recordingAccessControlFake{
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
return true, nil
},
}
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rc.Context.Req.Header.Add("Accept", "application/json")
rc.Context.Req.Form.Set("decrypt", "true")
response := sut.RouteGetReceivers(&rc)
require.Equal(t, 200, response.Status())
require.Equal(t, expectedListResponse, string(response.Body()))
})
})
})
t.Run("get receiver", func(t *testing.T) {
t.Run("GET returns 200", func(t *testing.T) {
env := createTestEnv(t, testConfig)
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
response := sut.RouteGetReceiver(&rc, "grafana-default-email")
require.Equal(t, 200, response.Status())
})
t.Run("decrypt true without secrets:read permissions returns 403", func(t *testing.T) {
recPermCheck := false
env := createTestEnv(t, testConfig)
env.ac = &recordingAccessControlFake{
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingReceiversReadSecrets) {
recPermCheck = true
}
return false, nil
},
}
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rc.Context.Req.Form.Set("decrypt", "true")
response := sut.RouteGetReceiver(&rc, "grafana-default-email")
require.True(t, recPermCheck)
require.Equal(t, 403, response.Status())
})
t.Run("json body content is as expected", func(t *testing.T) {
expectedRedactedResponse := `{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","use_discord_username":true},"secureFields":{"url":true}}]}`
expectedDecryptedResponse := `{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthPassword":"testpass","basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","url":"some url","use_discord_username":true},"secureFields":{"url":true}}]}`
t.Run("decrypt false", func(t *testing.T) {
env := createTestEnv(t, testContactPointConfig)
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rc.Context.Req.Header.Add("Accept", "application/json")
rc.Context.Req.Form.Set("decrypt", "false")
response := sut.RouteGetReceiver(&rc, "multiple integrations")
require.Equal(t, 200, response.Status())
require.Equal(t, expectedRedactedResponse, string(response.Body()))
})
t.Run("decrypt true", func(t *testing.T) {
env := createTestEnv(t, testContactPointConfig)
env.ac = &recordingAccessControlFake{
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
return true, nil
},
}
sut := createNotificationSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rc.Context.Req.Header.Add("Accept", "application/json")
rc.Context.Req.Form.Set("decrypt", "true")
response := sut.RouteGetReceiver(&rc, "multiple integrations")
require.Equal(t, 200, response.Status())
require.Equal(t, expectedDecryptedResponse, string(response.Body()))
})
})
})
}
func createNotificationSrvSutFromEnv(t *testing.T, env *testEnvironment) NotificationSrv {
t.Helper()
receiverSvc := notifier.NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](env.ac, false),
legacy_storage.NewAlertmanagerConfigStore(env.configs),
env.prov,
env.store,
env.secrets,
env.xact,
env.log,
)
return NotificationSrv{
logger: env.log,
receiverService: receiverSvc,
}
}
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{},
}
}