diff --git a/pkg/api/api.go b/pkg/api/api.go index f0d04909396..7bb024a17b0 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -273,17 +273,17 @@ func (hs *HTTPServer) registerRoutes() { // Data sources apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) { datasourceRoute.Get("/", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesRead)), routing.Wrap(hs.GetDataSources)) - datasourceRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesCreate)), quota("data_source"), routing.Wrap(AddDataSource)) + datasourceRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesCreate)), quota("data_source"), routing.Wrap(hs.AddDataSource)) datasourceRoute.Put("/:id", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesWrite, ScopeDatasourceID)), routing.Wrap(hs.UpdateDataSource)) datasourceRoute.Delete("/:id", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesDelete, ScopeDatasourceID)), routing.Wrap(hs.DeleteDataSourceById)) datasourceRoute.Delete("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesDelete, ScopeDatasourceUID)), routing.Wrap(hs.DeleteDataSourceByUID)) datasourceRoute.Delete("/name/:name", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesDelete, ScopeDatasourceName)), routing.Wrap(hs.DeleteDataSourceByName)) datasourceRoute.Get("/:id", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesRead)), routing.Wrap(hs.GetDataSourceById)) datasourceRoute.Get("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesRead)), routing.Wrap(hs.GetDataSourceByUID)) - datasourceRoute.Get("/name/:name", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesRead)), routing.Wrap(GetDataSourceByName)) + datasourceRoute.Get("/name/:name", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesRead)), routing.Wrap(hs.GetDataSourceByName)) }) - apiRoute.Get("/datasources/id/:name", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesIDRead, ScopeDatasourceName)), routing.Wrap(GetDataSourceIdByName)) + apiRoute.Get("/datasources/id/:name", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesIDRead, ScopeDatasourceName)), routing.Wrap(hs.GetDataSourceIdByName)) apiRoute.Get("/plugins", routing.Wrap(hs.GetPluginList)) apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID)) diff --git a/pkg/api/datasource_permissions.go b/pkg/api/datasource_permissions.go new file mode 100644 index 00000000000..5035b8da6c9 --- /dev/null +++ b/pkg/api/datasource_permissions.go @@ -0,0 +1,22 @@ +package api + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) + +type DatasourcePermissionsService interface { + FilterDatasourcesBasedOnQueryPermissions(ctx context.Context, cmd *models.DatasourcesPermissionFilterQuery) error +} + +// dummy method +func (hs *OSSDatasourcePermissionsService) FilterDatasourcesBasedOnQueryPermissions(ctx context.Context, cmd *models.DatasourcesPermissionFilterQuery) error { + return nil +} + +type OSSDatasourcePermissionsService struct{} + +func ProvideDatasourcePermissionsService() *OSSDatasourcePermissionsService { + return &OSSDatasourcePermissionsService{} +} diff --git a/pkg/api/datasource_permissions_mocks.go b/pkg/api/datasource_permissions_mocks.go new file mode 100644 index 00000000000..1fd921fa6ac --- /dev/null +++ b/pkg/api/datasource_permissions_mocks.go @@ -0,0 +1,20 @@ +package api + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) + +type mockDatasourcePermissionService struct { + dsResult []*models.DataSource +} + +func (m *mockDatasourcePermissionService) FilterDatasourcesBasedOnQueryPermissions(ctx context.Context, cmd *models.DatasourcesPermissionFilterQuery) error { + cmd.Result = m.dsResult + return nil +} + +func newMockDatasourcePermissionService() *mockDatasourcePermissionService { + return &mockDatasourcePermissionService{} +} diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index b24612f763f..cb54321e91b 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -27,11 +27,11 @@ var datasourcesLogger = log.New("datasources") func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response { query := models.GetDataSourcesQuery{OrgId: c.OrgId, DataSourceLimit: hs.Cfg.DataSourceLimit} - if err := bus.Dispatch(c.Req.Context(), &query); err != nil { + if err := hs.SQLStore.GetDataSources(c.Req.Context(), &query); err != nil { return response.Error(500, "Failed to query datasources", err) } - filtered, err := filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, query.Result) + filtered, err := hs.filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, query.Result) if err != nil { return response.Error(500, "Failed to query datasources", err) } @@ -98,7 +98,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response OrgId: c.OrgId, } - if err := bus.Dispatch(c.Req.Context(), &query); err != nil { + if err := hs.SQLStore.GetDataSource(c.Req.Context(), &query); err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { return response.Error(404, "Data source not found", nil) } @@ -108,7 +108,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response return response.Error(500, "Failed to query datasources", err) } - filtered, err := filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{query.Result}) + filtered, err := hs.filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{query.Result}) if err != nil || len(filtered) != 1 { return response.Error(404, "Data source not found", err) } @@ -135,7 +135,7 @@ func (hs *HTTPServer) DeleteDataSourceById(c *models.ReqContext) response.Respon return response.Error(400, "Missing valid datasource id", nil) } - ds, err := getRawDataSourceById(c.Req.Context(), id, c.OrgId) + ds, err := hs.getRawDataSourceById(c.Req.Context(), id, c.OrgId) if err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { return response.Error(404, "Data source not found", nil) @@ -149,7 +149,7 @@ func (hs *HTTPServer) DeleteDataSourceById(c *models.ReqContext) response.Respon cmd := &models.DeleteDataSourceCommand{ID: id, OrgID: c.OrgId} - err = bus.Dispatch(c.Req.Context(), cmd) + err = hs.SQLStore.DeleteDataSource(c.Req.Context(), cmd) if err != nil { return response.Error(500, "Failed to delete datasource", err) } @@ -161,7 +161,7 @@ func (hs *HTTPServer) DeleteDataSourceById(c *models.ReqContext) response.Respon // GET /api/datasources/uid/:uid func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response { - ds, err := getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.OrgId) + ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.OrgId) if err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { @@ -170,7 +170,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response return response.Error(http.StatusInternalServerError, "Failed to query datasource", err) } - filtered, err := filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{ds}) + filtered, err := hs.filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{ds}) if err != nil || len(filtered) != 1 { return response.Error(404, "Data source not found", err) } @@ -195,7 +195,7 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *models.ReqContext) response.Respo return response.Error(400, "Missing datasource uid", nil) } - ds, err := getRawDataSourceByUID(c.Req.Context(), uid, c.OrgId) + ds, err := hs.getRawDataSourceByUID(c.Req.Context(), uid, c.OrgId) if err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { return response.Error(404, "Data source not found", nil) @@ -209,7 +209,7 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *models.ReqContext) response.Respo cmd := &models.DeleteDataSourceCommand{UID: uid, OrgID: c.OrgId} - err = bus.Dispatch(c.Req.Context(), cmd) + err = hs.SQLStore.DeleteDataSource(c.Req.Context(), cmd) if err != nil { return response.Error(500, "Failed to delete datasource", err) } @@ -231,7 +231,7 @@ func (hs *HTTPServer) DeleteDataSourceByName(c *models.ReqContext) response.Resp } getCmd := &models.GetDataSourceQuery{Name: name, OrgId: c.OrgId} - if err := bus.Dispatch(c.Req.Context(), getCmd); err != nil { + if err := hs.SQLStore.GetDataSource(c.Req.Context(), getCmd); err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { return response.Error(404, "Data source not found", nil) } @@ -243,7 +243,7 @@ func (hs *HTTPServer) DeleteDataSourceByName(c *models.ReqContext) response.Resp } cmd := &models.DeleteDataSourceCommand{Name: name, OrgID: c.OrgId} - err := bus.Dispatch(c.Req.Context(), cmd) + err := hs.SQLStore.DeleteDataSource(c.Req.Context(), cmd) if err != nil { return response.Error(500, "Failed to delete datasource", err) } @@ -265,7 +265,7 @@ func validateURL(cmdType string, url string) response.Response { } // POST /api/datasources/ -func AddDataSource(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response { cmd := models.AddDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -279,7 +279,7 @@ func AddDataSource(c *models.ReqContext) response.Response { } } - if err := bus.Dispatch(c.Req.Context(), &cmd); err != nil { + if err := hs.SQLStore.AddDataSource(c.Req.Context(), &cmd); err != nil { if errors.Is(err, models.ErrDataSourceNameExists) || errors.Is(err, models.ErrDataSourceUidExists) { return response.Error(409, err.Error(), err) } @@ -312,7 +312,7 @@ func (hs *HTTPServer) UpdateDataSource(c *models.ReqContext) response.Response { return resp } - ds, err := getRawDataSourceById(c.Req.Context(), cmd.Id, cmd.OrgId) + ds, err := hs.getRawDataSourceById(c.Req.Context(), cmd.Id, cmd.OrgId) if err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { return response.Error(404, "Data source not found", nil) @@ -329,7 +329,7 @@ func (hs *HTTPServer) UpdateDataSource(c *models.ReqContext) response.Response { return response.Error(500, "Failed to update datasource", err) } - err = bus.Dispatch(c.Req.Context(), &cmd) + err = hs.SQLStore.UpdateDataSource(c.Req.Context(), &cmd) if err != nil { if errors.Is(err, models.ErrDataSourceUpdatingOldVersion) { return response.Error(409, "Datasource has already been updated by someone else. Please reload and try again", err) @@ -342,7 +342,7 @@ func (hs *HTTPServer) UpdateDataSource(c *models.ReqContext) response.Response { OrgId: c.OrgId, } - if err := bus.Dispatch(c.Req.Context(), &query); err != nil { + if err := hs.SQLStore.GetDataSource(c.Req.Context(), &query); err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { return response.Error(404, "Data source not found", nil) } @@ -366,7 +366,7 @@ func (hs *HTTPServer) fillWithSecureJSONData(ctx context.Context, cmd *models.Up return nil } - ds, err := getRawDataSourceById(ctx, cmd.Id, cmd.OrgId) + ds, err := hs.getRawDataSourceById(ctx, cmd.Id, cmd.OrgId) if err != nil { return err } @@ -388,26 +388,26 @@ func (hs *HTTPServer) fillWithSecureJSONData(ctx context.Context, cmd *models.Up return nil } -func getRawDataSourceById(ctx context.Context, id int64, orgID int64) (*models.DataSource, error) { +func (hs *HTTPServer) getRawDataSourceById(ctx context.Context, id int64, orgID int64) (*models.DataSource, error) { query := models.GetDataSourceQuery{ Id: id, OrgId: orgID, } - if err := bus.Dispatch(ctx, &query); err != nil { + if err := hs.SQLStore.GetDataSource(ctx, &query); err != nil { return nil, err } return query.Result, nil } -func getRawDataSourceByUID(ctx context.Context, uid string, orgID int64) (*models.DataSource, error) { +func (hs *HTTPServer) getRawDataSourceByUID(ctx context.Context, uid string, orgID int64) (*models.DataSource, error) { query := models.GetDataSourceQuery{ Uid: uid, OrgId: orgID, } - if err := bus.Dispatch(ctx, &query); err != nil { + if err := hs.SQLStore.GetDataSource(ctx, &query); err != nil { return nil, err } @@ -415,17 +415,17 @@ func getRawDataSourceByUID(ctx context.Context, uid string, orgID int64) (*model } // Get /api/datasources/name/:name -func GetDataSourceByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceByName(c *models.ReqContext) response.Response { query := models.GetDataSourceQuery{Name: web.Params(c.Req)[":name"], OrgId: c.OrgId} - if err := bus.Dispatch(c.Req.Context(), &query); err != nil { + if err := hs.SQLStore.GetDataSource(c.Req.Context(), &query); err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { return response.Error(404, "Data source not found", nil) } return response.Error(500, "Failed to query datasources", err) } - filtered, err := filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{query.Result}) + filtered, err := hs.filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{query.Result}) if err != nil || len(filtered) != 1 { return response.Error(404, "Data source not found", err) } @@ -435,10 +435,10 @@ func GetDataSourceByName(c *models.ReqContext) response.Response { } // Get /api/datasources/id/:name -func GetDataSourceIdByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceIdByName(c *models.ReqContext) response.Response { query := models.GetDataSourceQuery{Name: web.Params(c.Req)[":name"], OrgId: c.OrgId} - if err := bus.Dispatch(c.Req.Context(), &query); err != nil { + if err := hs.SQLStore.GetDataSource(c.Req.Context(), &query); err != nil { if errors.Is(err, models.ErrDataSourceNotFound) { return response.Error(404, "Data source not found", nil) } @@ -593,13 +593,14 @@ func (hs *HTTPServer) decryptSecureJsonDataFn() func(map[string][]byte) map[stri } } -func filterDatasourcesByQueryPermission(ctx context.Context, user *models.SignedInUser, datasources []*models.DataSource) ([]*models.DataSource, error) { +func (hs *HTTPServer) filterDatasourcesByQueryPermission(ctx context.Context, user *models.SignedInUser, datasources []*models.DataSource) ([]*models.DataSource, error) { query := models.DatasourcesPermissionFilterQuery{ User: user, Datasources: datasources, } + query.Result = datasources - if err := bus.Dispatch(ctx, &query); err != nil { + if err := hs.DatasourcePermissionsService.FilterDatasourcesBasedOnQueryPermissions(ctx, &query); err != nil { if !errors.Is(err, bus.ErrHandlerNotFound) { return nil, err } diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 4e95153c6ec..c92cad413bb 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -2,7 +2,6 @@ package api import ( "bytes" - "context" "encoding/json" "fmt" "io" @@ -12,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" @@ -28,26 +26,25 @@ const ( ) func TestDataSourcesProxy_userLoggedIn(t *testing.T) { - mock := mockstore.NewSQLStoreMock() + mockSQLStore := mockstore.NewSQLStoreMock() + mockDatasourcePermissionService := newMockDatasourcePermissionService() loggedInUserScenario(t, "When calling GET on", "/api/datasources/", "/api/datasources/", func(sc *scenarioContext) { // Stubs the database query - bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourcesQuery) error { - assert.Equal(t, testOrgID, query.OrgId) - query.Result = []*models.DataSource{ - {Name: "mmm"}, - {Name: "ZZZ"}, - {Name: "BBB"}, - {Name: "aaa"}, - } - return nil - }) + ds := []*models.DataSource{ + {Name: "mmm"}, + {Name: "ZZZ"}, + {Name: "BBB"}, + {Name: "aaa"}, + } + mockSQLStore.ExpectedDatasources = ds + mockDatasourcePermissionService.dsResult = ds // handler func being tested hs := &HTTPServer{ - Bus: bus.GetBus(), - Cfg: setting.NewCfg(), - pluginStore: &fakePluginStore{}, - SQLStore: mock, + Cfg: setting.NewCfg(), + pluginStore: &fakePluginStore{}, + SQLStore: mockSQLStore, + DatasourcePermissionsService: mockDatasourcePermissionService, } sc.handlerFunc = hs.GetDataSources sc.fakeReq("GET", "/api/datasources").exec() @@ -60,27 +57,27 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) { assert.Equal(t, "BBB", respJSON[1]["name"]) assert.Equal(t, "mmm", respJSON[2]["name"]) assert.Equal(t, "ZZZ", respJSON[3]["name"]) - }, mock) + }, mockSQLStore) loggedInUserScenario(t, "Should be able to save a data source when calling DELETE on non-existing", "/api/datasources/name/12345", "/api/datasources/name/:name", func(sc *scenarioContext) { // handler func being tested hs := &HTTPServer{ - Bus: bus.GetBus(), Cfg: setting.NewCfg(), pluginStore: &fakePluginStore{}, } sc.handlerFunc = hs.DeleteDataSourceByName sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() assert.Equal(t, 404, sc.resp.Code) - }, mock) + }, mockSQLStore) } // Adding data sources with invalid URLs should lead to an error. func TestAddDataSource_InvalidURL(t *testing.T) { - defer bus.ClearBusHandlers() - sc := setupScenarioContext(t, "/api/datasources") + hs := &HTTPServer{ + SQLStore: mockstore.NewSQLStoreMock(), + } sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { c.Req.Body = mockRequestBody(models.AddDataSourceCommand{ @@ -89,7 +86,7 @@ func TestAddDataSource_InvalidURL(t *testing.T) { Access: "direct", Type: "test", }) - return AddDataSource(c) + return hs.AddDataSource(c) })) sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() @@ -99,19 +96,14 @@ func TestAddDataSource_InvalidURL(t *testing.T) { // Adding data sources with URLs not specifying protocol should work. func TestAddDataSource_URLWithoutProtocol(t *testing.T) { - defer bus.ClearBusHandlers() - const name = "Test" const url = "localhost:5432" - // Stub handler - bus.AddHandler("sql", func(ctx context.Context, cmd *models.AddDataSourceCommand) error { - assert.Equal(t, name, cmd.Name) - assert.Equal(t, url, cmd.Url) - - cmd.Result = &models.DataSource{} - return nil - }) + mockSQLStore := mockstore.NewSQLStoreMock() + mockSQLStore.ExpectedDatasource = &models.DataSource{} + hs := &HTTPServer{ + SQLStore: mockSQLStore, + } sc := setupScenarioContext(t, "/api/datasources") @@ -122,7 +114,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) { Access: "direct", Type: "test", }) - return AddDataSource(c) + return hs.AddDataSource(c) })) sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() @@ -132,8 +124,9 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) { // Updating data sources with invalid URLs should lead to an error. func TestUpdateDataSource_InvalidURL(t *testing.T) { - defer bus.ClearBusHandlers() - + hs := &HTTPServer{ + SQLStore: mockstore.NewSQLStoreMock(), + } sc := setupScenarioContext(t, "/api/datasources/1234") sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { @@ -143,7 +136,7 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) { Access: "direct", Type: "test", }) - return AddDataSource(c) + return hs.AddDataSource(c) })) sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() @@ -153,19 +146,15 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) { // Updating data sources with URLs not specifying protocol should work. func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) { - defer bus.ClearBusHandlers() - const name = "Test" const url = "localhost:5432" + mockSQLStore := mockstore.NewSQLStoreMock() + hs := &HTTPServer{ + SQLStore: mockSQLStore, + } // Stub handler - bus.AddHandler("sql", func(ctx context.Context, cmd *models.AddDataSourceCommand) error { - assert.Equal(t, name, cmd.Name) - assert.Equal(t, url, cmd.Url) - - cmd.Result = &models.DataSource{} - return nil - }) + mockSQLStore.ExpectedDatasource = &models.DataSource{} sc := setupScenarioContext(t, "/api/datasources/1234") @@ -176,7 +165,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) { Access: "direct", Type: "test", }) - return AddDataSource(c) + return hs.AddDataSource(c) })) sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() @@ -204,44 +193,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { Access: "Proxy", ReadOnly: true, } - getDatasourceStub := func(ctx context.Context, query *models.GetDataSourceQuery) error { - result := testDatasource - result.Id = query.Id - result.OrgId = query.OrgId - query.Result = &result - return nil - } - getDatasourcesStub := func(ctx context.Context, cmd *models.GetDataSourcesQuery) error { - cmd.Result = []*models.DataSource{} - return nil - } - addDatasourceStub := func(ctx context.Context, cmd *models.AddDataSourceCommand) error { - cmd.Result = &testDatasource - return nil - } - updateDatasourceStub := func(ctx context.Context, cmd *models.UpdateDataSourceCommand) error { - cmd.Result = &testDatasource - return nil - } - updateDatasourceReadOnlyStub := func(ctx context.Context, cmd *models.UpdateDataSourceCommand) error { - cmd.Result = &testDatasourceReadOnly - return nil - } - getDatasourceNotFoundStub := func(ctx context.Context, cmd *models.GetDataSourceQuery) error { - cmd.Result = nil - return models.ErrDataSourceNotFound - } - - getDatasourceReadOnlyStub := func(ctx context.Context, query *models.GetDataSourceQuery) error { - query.Result = &testDatasourceReadOnly - return nil - } - - deleteDatasourceStub := func(ctx context.Context, cmd *models.DeleteDataSourceCommand) error { - cmd.DeletedDatasourcesCount = 1 - return nil - } addDatasourceBody := func() io.Reader { s, _ := json.Marshal(models.AddDataSourceCommand{ Name: "test", @@ -251,6 +203,14 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }) return bytes.NewReader(s) } + + sqlStore := mockstore.NewSQLStoreMock() + sqlStore.ExpectedDatasource = &testDatasource + dsPermissionService := newMockDatasourcePermissionService() + dsPermissionService.dsResult = []*models.DataSource{ + &testDatasource, + } + updateDatasourceBody := func() io.Reader { s, _ := json.Marshal(models.UpdateDataSourceCommand{ Name: "test", @@ -261,14 +221,14 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { return bytes.NewReader(s) } type acTestCaseWithHandler struct { - busStubs []bus.HandlerFunc - body func() io.Reader + body func() io.Reader accessControlTestCase + expectedDS *models.DataSource + expectedSQLError error } tests := []acTestCaseWithHandler{ { - busStubs: []bus.HandlerFunc{getDatasourceNotFoundStub, updateDatasourceStub}, - body: updateDatasourceBody, + body: updateDatasourceBody, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusNotFound, desc: "DatasourcesPut should return 404 if datasource not found", @@ -281,9 +241,9 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedSQLError: models.ErrDataSourceNotFound, }, { - busStubs: []bus.HandlerFunc{getDatasourcesStub}, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesGet should return 200 for user with correct permissions", @@ -302,8 +262,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, { - busStubs: []bus.HandlerFunc{addDatasourceStub}, - body: addDatasourceBody, + body: addDatasourceBody, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesPost should return 200 for user with correct permissions", @@ -311,6 +270,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { method: http.MethodPost, permissions: []*accesscontrol.Permission{{Action: ActionDatasourcesCreate}}, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -322,8 +282,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, { - busStubs: []bus.HandlerFunc{getDatasourceStub, updateDatasourceStub}, - body: updateDatasourceBody, + body: updateDatasourceBody, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesPut should return 200 for user with correct permissions", @@ -336,6 +295,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -347,8 +307,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, { - busStubs: []bus.HandlerFunc{getDatasourceReadOnlyStub, updateDatasourceReadOnlyStub}, - body: updateDatasourceBody, + body: updateDatasourceBody, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusForbidden, desc: "DatasourcesPut should return 403 for read only datasource", @@ -361,9 +320,9 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasourceReadOnly, }, { - busStubs: []bus.HandlerFunc{getDatasourceStub, deleteDatasourceStub}, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesDeleteByID should return 200 for user with correct permissions", @@ -376,6 +335,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -387,7 +347,6 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, { - busStubs: []bus.HandlerFunc{getDatasourceStub, deleteDatasourceStub}, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesDeleteByUID should return 200 for user with correct permissions", @@ -400,6 +359,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -411,7 +371,6 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, { - busStubs: []bus.HandlerFunc{getDatasourceStub, deleteDatasourceStub}, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesDeleteByName should return 200 for user with correct permissions", @@ -424,6 +383,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -435,7 +395,6 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, { - busStubs: []bus.HandlerFunc{getDatasourceStub}, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesGetByID should return 200 for user with correct permissions", @@ -448,6 +407,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -459,7 +419,6 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, { - busStubs: []bus.HandlerFunc{getDatasourceStub}, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesGetByUID should return 200 for user with correct permissions", @@ -472,6 +431,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -483,7 +443,6 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, { - busStubs: []bus.HandlerFunc{getDatasourceStub}, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesGetByName should return 200 for user with correct permissions", @@ -496,6 +455,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -505,9 +465,9 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { method: http.MethodGet, permissions: []*accesscontrol.Permission{{Action: "wrong"}}, }, + expectedDS: &testDatasource, }, { - busStubs: []bus.HandlerFunc{getDatasourceStub}, accessControlTestCase: accessControlTestCase{ expectedCode: http.StatusOK, desc: "DatasourcesGetIdByName should return 200 for user with correct permissions", @@ -520,6 +480,7 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { }, }, }, + expectedDS: &testDatasource, }, { accessControlTestCase: accessControlTestCase{ @@ -529,19 +490,26 @@ func TestAPI_Datasources_AccessControl(t *testing.T) { method: http.MethodGet, permissions: []*accesscontrol.Permission{{Action: "wrong"}}, }, + expectedDS: &testDatasource, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - t.Cleanup(bus.ClearBusHandlers) - for i, handler := range test.busStubs { - bus.AddHandler(fmt.Sprintf("test_handler_%v", i), handler) - } - cfg := setting.NewCfg() sc, hs := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions) + // mock sqlStore and datasource permission service + sqlStore.ExpectedError = test.expectedSQLError + sqlStore.ExpectedDatasource = test.expectedDS + dsPermissionService.dsResult = []*models.DataSource{test.expectedDS} + if test.expectedDS == nil { + dsPermissionService.dsResult = nil + } + sc.sqlStore = sqlStore + hs.SQLStore = sqlStore + hs.DatasourcePermissionsService = dsPermissionService + // Create a middleware to pretend user is logged in pretendSignInMiddleware := func(c *models.ReqContext) { sc.context = c diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3fe6353de38..f1ea4edaef1 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -23,7 +23,7 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins Enab return nil, err } - filtered, err := filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, query.Result) + filtered, err := hs.filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, query.Result) if err != nil { return nil, err } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 5c666b98022..3da4a233ce1 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -76,60 +76,61 @@ type HTTPServer struct { middlewares []web.Handler namedMiddlewares []routing.RegisterNamedMiddleware - PluginContextProvider *plugincontext.Provider - RouteRegister routing.RouteRegister - Bus bus.Bus - RenderService rendering.Service - Cfg *setting.Cfg - Features *featuremgmt.FeatureManager - SettingsProvider setting.Provider - HooksService *hooks.HooksService - CacheService *localcache.CacheService - DataSourceCache datasources.CacheService - AuthTokenService models.UserTokenService - QuotaService *quota.QuotaService - RemoteCacheService *remotecache.RemoteCache - ProvisioningService provisioning.ProvisioningService - Login login.Service - License models.Licensing - AccessControl accesscontrol.AccessControl - DataProxy *datasourceproxy.DataSourceProxyService - PluginRequestValidator models.PluginRequestValidator - pluginClient plugins.Client - pluginStore plugins.Store - pluginDashboardManager plugins.PluginDashboardManager - pluginStaticRouteResolver plugins.StaticRouteResolver - pluginErrorResolver plugins.ErrorResolver - SearchService search.Service - ShortURLService shorturls.Service - QueryHistoryService queryhistory.Service - Live *live.GrafanaLive - LivePushGateway *pushhttp.Gateway - ThumbService thumbs.Service - ContextHandler *contexthandler.ContextHandler - SQLStore sqlstore.Store - AlertEngine *alerting.AlertEngine - LoadSchemaService *schemaloader.SchemaLoaderService - AlertNG *ngalert.AlertNG - LibraryPanelService librarypanels.Service - LibraryElementService libraryelements.Service - SocialService social.Service - Listener net.Listener - EncryptionService encryption.Internal - SecretsService secrets.Service - DataSourcesService *datasources.Service - cleanUpService *cleanup.CleanUpService - tracer tracing.Tracer - grafanaUpdateChecker *updatechecker.GrafanaService - pluginsUpdateChecker *updatechecker.PluginsService - searchUsersService searchusers.Service - ldapGroups ldap.Groups - teamGuardian teamguardian.TeamGuardian - queryDataService *query.Service - serviceAccountsService serviceaccounts.Service - authInfoService login.AuthInfoService - TeamPermissionsService *resourcepermissions.Service - NotificationService *notifications.NotificationService + PluginContextProvider *plugincontext.Provider + RouteRegister routing.RouteRegister + Bus bus.Bus + RenderService rendering.Service + Cfg *setting.Cfg + Features *featuremgmt.FeatureManager + SettingsProvider setting.Provider + HooksService *hooks.HooksService + CacheService *localcache.CacheService + DataSourceCache datasources.CacheService + AuthTokenService models.UserTokenService + QuotaService *quota.QuotaService + RemoteCacheService *remotecache.RemoteCache + ProvisioningService provisioning.ProvisioningService + Login login.Service + License models.Licensing + AccessControl accesscontrol.AccessControl + DataProxy *datasourceproxy.DataSourceProxyService + PluginRequestValidator models.PluginRequestValidator + pluginClient plugins.Client + pluginStore plugins.Store + pluginDashboardManager plugins.PluginDashboardManager + pluginStaticRouteResolver plugins.StaticRouteResolver + pluginErrorResolver plugins.ErrorResolver + SearchService search.Service + ShortURLService shorturls.Service + QueryHistoryService queryhistory.Service + Live *live.GrafanaLive + LivePushGateway *pushhttp.Gateway + ThumbService thumbs.Service + ContextHandler *contexthandler.ContextHandler + SQLStore sqlstore.Store + AlertEngine *alerting.AlertEngine + LoadSchemaService *schemaloader.SchemaLoaderService + AlertNG *ngalert.AlertNG + LibraryPanelService librarypanels.Service + LibraryElementService libraryelements.Service + SocialService social.Service + Listener net.Listener + EncryptionService encryption.Internal + SecretsService secrets.Service + DataSourcesService *datasources.Service + cleanUpService *cleanup.CleanUpService + tracer tracing.Tracer + grafanaUpdateChecker *updatechecker.GrafanaService + pluginsUpdateChecker *updatechecker.PluginsService + searchUsersService searchusers.Service + ldapGroups ldap.Groups + teamGuardian teamguardian.TeamGuardian + queryDataService *query.Service + serviceAccountsService serviceaccounts.Service + authInfoService login.AuthInfoService + TeamPermissionsService *resourcepermissions.Service + NotificationService *notifications.NotificationService + DatasourcePermissionsService DatasourcePermissionsService } type ServerOptions struct { @@ -157,67 +158,68 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi dataSourcesService *datasources.Service, secretsService secrets.Service, queryDataService *query.Service, ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service, authInfoService login.AuthInfoService, resourcePermissionServices *resourceservices.ResourceServices, - notificationService *notifications.NotificationService) (*HTTPServer, error) { + notificationService *notifications.NotificationService, datasourcePermissionsService DatasourcePermissionsService) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() hs := &HTTPServer{ - Cfg: cfg, - RouteRegister: routeRegister, - Bus: bus, - RenderService: renderService, - License: licensing, - HooksService: hooksService, - CacheService: cacheService, - SQLStore: sqlStore, - AlertEngine: alertEngine, - PluginRequestValidator: pluginRequestValidator, - pluginClient: pluginClient, - pluginStore: pluginStore, - pluginStaticRouteResolver: pluginStaticRouteResolver, - pluginDashboardManager: pluginDashboardManager, - pluginErrorResolver: pluginErrorResolver, - grafanaUpdateChecker: grafanaUpdateChecker, - pluginsUpdateChecker: pluginsUpdateChecker, - SettingsProvider: settingsProvider, - DataSourceCache: dataSourceCache, - AuthTokenService: userTokenService, - cleanUpService: cleanUpService, - ShortURLService: shortURLService, - QueryHistoryService: queryHistoryService, - Features: features, - ThumbService: thumbService, - RemoteCacheService: remoteCache, - ProvisioningService: provisioningService, - Login: loginService, - AccessControl: accessControl, - DataProxy: dataSourceProxy, - SearchService: searchService, - Live: live, - LivePushGateway: livePushGateway, - PluginContextProvider: plugCtxProvider, - ContextHandler: contextHandler, - LoadSchemaService: schemaService, - AlertNG: alertNG, - LibraryPanelService: libraryPanelService, - LibraryElementService: libraryElementService, - QuotaService: quotaService, - tracer: tracer, - log: log.New("http.server"), - web: m, - Listener: opts.Listener, - SocialService: socialService, - EncryptionService: encryptionService, - SecretsService: secretsService, - DataSourcesService: dataSourcesService, - searchUsersService: searchUsersService, - ldapGroups: ldapGroups, - teamGuardian: teamGuardian, - queryDataService: queryDataService, - serviceAccountsService: serviceaccountsService, - authInfoService: authInfoService, - TeamPermissionsService: resourcePermissionServices.GetTeamService(), - NotificationService: notificationService, + Cfg: cfg, + RouteRegister: routeRegister, + Bus: bus, + RenderService: renderService, + License: licensing, + HooksService: hooksService, + CacheService: cacheService, + SQLStore: sqlStore, + AlertEngine: alertEngine, + PluginRequestValidator: pluginRequestValidator, + pluginClient: pluginClient, + pluginStore: pluginStore, + pluginStaticRouteResolver: pluginStaticRouteResolver, + pluginDashboardManager: pluginDashboardManager, + pluginErrorResolver: pluginErrorResolver, + grafanaUpdateChecker: grafanaUpdateChecker, + pluginsUpdateChecker: pluginsUpdateChecker, + SettingsProvider: settingsProvider, + DataSourceCache: dataSourceCache, + AuthTokenService: userTokenService, + cleanUpService: cleanUpService, + ShortURLService: shortURLService, + QueryHistoryService: queryHistoryService, + Features: features, + ThumbService: thumbService, + RemoteCacheService: remoteCache, + ProvisioningService: provisioningService, + Login: loginService, + AccessControl: accessControl, + DataProxy: dataSourceProxy, + SearchService: searchService, + Live: live, + LivePushGateway: livePushGateway, + PluginContextProvider: plugCtxProvider, + ContextHandler: contextHandler, + LoadSchemaService: schemaService, + AlertNG: alertNG, + LibraryPanelService: libraryPanelService, + LibraryElementService: libraryElementService, + QuotaService: quotaService, + tracer: tracer, + log: log.New("http.server"), + web: m, + Listener: opts.Listener, + SocialService: socialService, + EncryptionService: encryptionService, + SecretsService: secretsService, + DataSourcesService: dataSourcesService, + searchUsersService: searchUsersService, + ldapGroups: ldapGroups, + teamGuardian: teamGuardian, + queryDataService: queryDataService, + serviceAccountsService: serviceaccountsService, + authInfoService: authInfoService, + TeamPermissionsService: resourcePermissionServices.GetTeamService(), + NotificationService: notificationService, + DatasourcePermissionsService: datasourcePermissionsService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 620c1b57107..b91578d4456 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -5,6 +5,7 @@ package server import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" @@ -73,6 +74,8 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)), ldap.ProvideGroupsService, wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)), + api.ProvideDatasourcePermissionsService, + wire.Bind(new(api.DatasourcePermissionsService), new(*api.OSSDatasourcePermissionsService)), ) var wireExtsSet = wire.NewSet( diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 940eb842249..8120fef050c 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -28,7 +28,9 @@ type SQLStoreMock struct { ExpectedDashboardSnapshot *models.DashboardSnapshot ExpectedTeamsByUser []*models.TeamDTO ExpectedSearchOrgList []*models.OrgDTO - ExpectedError error + ExpectedDatasources []*models.DataSource + + ExpectedError error } func NewSQLStoreMock() *SQLStoreMock { @@ -483,6 +485,7 @@ func (m *SQLStoreMock) GetDataSource(ctx context.Context, query *models.GetDataS } func (m *SQLStoreMock) GetDataSources(ctx context.Context, query *models.GetDataSourcesQuery) error { + query.Result = m.ExpectedDatasources return m.ExpectedError } @@ -499,10 +502,12 @@ func (m *SQLStoreMock) DeleteDataSource(ctx context.Context, cmd *models.DeleteD } func (m *SQLStoreMock) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error { + cmd.Result = m.ExpectedDatasource return m.ExpectedError } func (m *SQLStoreMock) UpdateDataSource(ctx context.Context, cmd *models.UpdateDataSourceCommand) error { + cmd.Result = m.ExpectedDatasource return m.ExpectedError }