mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			406 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			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":{"basicAuthPassword":"[REDACTED]","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":"[REDACTED]","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{},
 | |
| 	}
 | |
| }
 |