diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go index d022408d82f..c7847135380 100644 --- a/pkg/services/publicdashboards/api/common_test.go +++ b/pkg/services/publicdashboards/api/common_test.go @@ -1,7 +1,6 @@ package api import ( - "context" "io" "net/http" "net/http/httptest" @@ -9,32 +8,15 @@ import ( "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/datasources" - fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" - "github.com/grafana/grafana/pkg/services/datasources/guardian" - datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" - "github.com/grafana/grafana/pkg/services/mtdsclient" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" - "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" - pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/publicdashboards" publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" - "github.com/grafana/grafana/pkg/services/query" - fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" @@ -108,77 +90,3 @@ func callAPI(server *web.Mux, method, path string, body io.Reader, t *testing.T) server.ServeHTTP(recorder, req) return recorder } - -// helper to query.Service -// allows us to stub the cache and plugin clients -func buildQueryDataService(t *testing.T, cs datasources.CacheService, fpc *fakePluginClient, store db.DB) *query.ServiceImpl { - // build database if we need one - if store == nil { - store = db.InitTestDB(t) - } - - // default cache service - if cs == nil { - cs = datasourceService.ProvideCacheService(localcache.ProvideService(), store, guardian.ProvideGuardian()) - } - - // default fakePluginClient - if fpc == nil { - fpc = &fakePluginClient{ - QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - resp := backend.Responses{ - "A": backend.DataResponse{ - Frames: []*data.Frame{{}}, - }, - } - return &backend.QueryDataResponse{Responses: resp}, nil - }, - } - } - - ds := &fakeDatasources.FakeDataSourceService{} - pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), - localcache.ProvideService(), &pluginstore.FakePluginStore{ - PluginList: []pluginstore.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "mysql", - }, - }, - }, - }, &fakeDatasources.FakeCacheService{}, ds, - pluginSettings.ProvideService(store, fakeSecrets.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider()) - - return query.ProvideService( - setting.NewCfg(), - cs, - nil, - &fakeDataSourceRequestValidator{}, - fpc, - pCtxProvider, - mtdsclient.NewNullMTDatasourceClientBuilder(), - ) -} - -// copied from pkg/api/metrics_test.go -type fakeDataSourceRequestValidator struct { - err error -} - -func (rv *fakeDataSourceRequestValidator) Validate(ds *datasources.DataSource, req *http.Request) error { - return rv.err -} - -// copied from pkg/api/plugins_test.go -type fakePluginClient struct { - plugins.Client - backend.QueryDataHandlerFunc -} - -func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if c.QueryDataHandlerFunc != nil { - return c.QueryDataHandlerFunc.QueryData(ctx, req) - } - - return backend.NewQueryDataResponse(), nil -} diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index c3b508d5627..02367824fbd 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -1,7 +1,6 @@ package api import ( - "context" "encoding/json" "errors" "fmt" @@ -20,35 +19,9 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/serverlock" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/annotations/annotationstest" - "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" - dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" - "github.com/grafana/grafana/pkg/services/dashboards/service" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/datasources/guardian" - datasourcesService "github.com/grafana/grafana/pkg/services/datasources/service" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/folder/foldertest" - "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/publicdashboards" - publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" - publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service" - "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/search/sort" - "github.com/grafana/grafana/pkg/services/tag/tagimpl" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/web" ) @@ -258,126 +231,6 @@ func getValidQueryPath(accessToken string) string { return fmt.Sprintf("/api/public/dashboards/%s/panels/2/query", accessToken) } -func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - db, cfg := db.InitTestDBWithCfg(t) - - cacheService := datasourcesService.ProvideCacheService(localcache.ProvideService(), db, guardian.ProvideGuardian()) - qds := buildQueryDataService(t, cacheService, nil, db) - dsStore := datasourcesService.CreateStore(db, log.New("publicdashboards.test")) - _, _ = dsStore.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ - UID: "ds1", - OrgID: 1, - Name: "laban", - Type: datasources.DS_MYSQL, - Access: datasources.DS_ACCESS_DIRECT, - URL: "http://test", - Database: "site", - ReadOnly: true, - }) - - // Create Dashboard - saveDashboardCmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - FolderUID: "", - IsFolder: false, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": "test", - "panels": []map[string]any{ - { - "id": 1, - "targets": []map[string]any{ - { - "datasource": map[string]string{ - "type": "mysql", - "uid": "ds1", - }, - "refId": "A", - }, - }, - }, - }, - }), - } - - // create dashboard - dashboardStoreService, err := dashboardStore.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db)) - require.NoError(t, err) - dashboard, err := dashboardStoreService.SaveDashboard(context.Background(), saveDashboardCmd) - require.NoError(t, err) - - // Create public dashboard - isEnabled := true - savePubDashboardCmd := &SavePublicDashboardDTO{ - DashboardUid: dashboard.UID, - OrgID: dashboard.OrgID, - PublicDashboard: &PublicDashboardDTO{ - IsEnabled: &isEnabled, - }, - } - - annotationsService := annotationstest.NewFakeAnnotationsRepo() - - // create public dashboard - store := publicdashboardsStore.ProvideStore(db, cfg, featuremgmt.WithFeatures()) - cfg.PublicDashboardsEnabled = true - ac := actest.FakeAccessControl{} - ws := publicdashboardsService.ProvideServiceWrapper(store) - folderStore := folderimpl.ProvideDashboardFolderStore(db) - dashPermissionService := acmock.NewMockedPermissionsService() - dashService, err := service.ProvideDashboardServiceImpl( - cfg, dashboardStoreService, folderStore, - featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), ac, actest.FakeService{}, - foldertest.NewFakeService(), nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, - nil, dualwrite.ProvideTestService(), sort.ProvideService(), - serverlock.ProvideService(db, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - require.NoError(t, err) - dashService.RegisterDashboardPermissions(dashPermissionService) - - license := licensingtest.NewFakeLicensing() - license.On("FeatureEnabled", FeaturePublicDashboardsEmailSharing).Return(false) - pds := publicdashboardsService.ProvideService(cfg, featuremgmt.WithFeatures(), store, qds, annotationsService, ac, ws, dashService, license) - pubdash, err := pds.Create(context.Background(), &user.SignedInUser{}, savePubDashboardCmd) - require.NoError(t, err) - - // setup test server - server := setupTestServer(t, cfg, pds, anonymousUser) - - resp := callAPI(server, http.MethodPost, - fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken), - strings.NewReader(`{}`), - t, - ) - require.Equal(t, http.StatusOK, resp.Code) - require.NoError(t, err) - require.JSONEq( - t, - `{ - "results": { - "A": { - "status": 200, - "frames": [ - { - "data": { - "values": [] - }, - "schema": { - "fields": [] - } - } - ] - } - } - }`, - resp.Body.String(), - ) -} - func TestAPIGetAnnotations(t *testing.T) { testCases := []struct { Name string diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index f79ada33f83..d10eb8641dc 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -16,33 +16,16 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/errutil" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/serverlock" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" - dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/org" . "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" - "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/search/sort" - "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) @@ -1407,548 +1390,6 @@ func TestDashboardEnabledChanged(t *testing.T) { }) } -func TestIntegrationPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } - features := featuremgmt.WithFeatures() - testDB, cfg := db.InitTestDBWithCfg(t) - dashStore, err := dashboardsDB.ProvideDashboardStore(testDB, cfg, features, tagimpl.ProvideService(testDB)) - require.NoError(t, err) - ac := actest.FakeAccessControl{ExpectedEvaluate: true} - - fStore := folderimpl.ProvideStore(testDB) - folderPermissions := acmock.NewMockedPermissionsService() - folderStore := folderimpl.ProvideDashboardFolderStore(testDB) - folderSvc := folderimpl.ProvideService( - fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, - nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - - dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), - serverlock.ProvideService(testDB, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore()) - require.NoError(t, err) - dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{}) - - // insert in test data so we can check that permissions are working properly through the dashboard service - // this will create 4 dashboards and 3 users - // user1 has access to all dashboards ("*") - // user2 has access to solely one dashboard - // user3 has access to all created dashboards through specific permissions - creatingUser := &user.SignedInUser{ - UserID: 1, - OrgID: 1, - OrgRole: org.RoleAdmin, - } - dashboardsToSave := []dashboards.SaveDashboardDTO{ - { - OrgID: 1, - User: creatingUser, - Dashboard: &dashboards.Dashboard{ - OrgID: 1, - UID: "9S6TmO67z", - Title: "test", - Slug: "test", - Data: simplejson.New(), - }, - }, - { - OrgID: 1, - User: creatingUser, - Dashboard: &dashboards.Dashboard{ - OrgID: 1, - UID: "1S6TmO67z", - Title: "my first dashboard", - Slug: "my-first-dashboard", - Data: simplejson.New(), - }, - }, - { - OrgID: 1, - User: creatingUser, - Dashboard: &dashboards.Dashboard{ - OrgID: 1, - UID: "2S6TmO67z", - Title: "my second dashboard", - Slug: "my-second-dashboard", - Data: simplejson.New(), - }, - }, - { - OrgID: 1, - User: creatingUser, - Dashboard: &dashboards.Dashboard{ - OrgID: 1, - UID: "0S6TmO67z", - Title: "my zero dashboard", - Slug: "my-zero-dashboard", - Data: simplejson.New(), - }, - }, - } - for _, dash := range dashboardsToSave { - _, err = dashboardService.SaveDashboard(context.Background(), &dash, true) - require.NoError(t, err) - } - - users := []user.User{ - { - ID: 1, - UID: "user1", - Email: "test1@gmail.com", - Login: "user1", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 2, - UID: "user2", - Login: "user2", - Email: "test2@gmail.com", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 3, - UID: "user3", - Login: "user3", - Email: "test3@gmail.com", - Created: time.Now(), - Updated: time.Now(), - }, - } - roles := []accesscontrol.Role{ - { - ID: 1, - UID: "role1", - Name: "forUser1", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 2, - UID: "role2", - Name: "forUser2", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 3, - UID: "role3", - Name: "forUser3", - Created: time.Now(), - Updated: time.Now(), - }, - } - - userRoles := []accesscontrol.UserRole{ - { - ID: 1, - OrgID: 1, - UserID: 1, - RoleID: 1, - Created: time.Now(), - }, - { - ID: 2, - OrgID: 1, - UserID: 2, - RoleID: 2, - Created: time.Now(), - }, - { - ID: 3, - OrgID: 1, - UserID: 3, - RoleID: 3, - Created: time.Now(), - }, - } - - permissions := []accesscontrol.Permission{ - { - ID: 1, - RoleID: 1, - Action: dashboards.ActionDashboardsRead, - Scope: "*", - Kind: "dashboards", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 2, - RoleID: 2, - Action: dashboards.ActionDashboardsRead, - Scope: "dashboards:uid:1S6TmO67z", - Attribute: "uid", - Identifier: "1S6TmO67z", - Kind: "dashboards", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 3, - RoleID: 3, - Action: dashboards.ActionDashboardsRead, - Scope: "dashboards:uid:0S6TmO67z", - Identifier: "0S6TmO67z", - Attribute: "uid", - Kind: "dashboards", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 4, - RoleID: 3, - Action: dashboards.ActionDashboardsRead, - Scope: "dashboards:uid:1S6TmO67z", - Identifier: "1S6TmO67z", - Kind: "dashboards", - Attribute: "uid", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 5, - RoleID: 3, - Action: dashboards.ActionDashboardsRead, - Scope: "dashboards:uid:2S6TmO67z", - Identifier: "2S6TmO67z", - Kind: "dashboards", - Attribute: "uid", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 6, - RoleID: 3, - Action: dashboards.ActionDashboardsRead, - Scope: "dashboards:uid:9S6TmO67z", - Identifier: "9S6TmO67z", - Kind: "dashboards", - Attribute: "uid", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 7, - RoleID: 1, - Action: dashboards.ActionFoldersRead, - Scope: "*", - Kind: "folders", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 8, - RoleID: 2, - Action: dashboards.ActionFoldersRead, - Scope: "*", - Kind: "folders", - Created: time.Now(), - Updated: time.Now(), - }, - { - ID: 9, - RoleID: 3, - Action: dashboards.ActionFoldersRead, - Scope: "*", - Kind: "folders", - Created: time.Now(), - Updated: time.Now(), - }, - } - - err = testDB.WithDbSession(context.Background(), func(sess *db.Session) error { - if _, err := sess.Insert(users); err != nil { - return err - } - if _, err := sess.Insert(roles); err != nil { - return err - } - - if _, err := sess.Insert(userRoles); err != nil { - return err - } - _, err := sess.Insert(permissions) - return err - }) - require.NoError(t, err) - - type args struct { - ctx context.Context - query *PublicDashboardListQuery - } - type mockResponse struct { - PublicDashboardListResponseWithPagination *PublicDashboardListResponseWithPagination - Err error - DashboardResponse []dashboards.DashboardSearchProjection - DashboardErr error - } - - expectedFinalResponse := []*PublicDashboardListResponse{ - { - Uid: "1GwW7mgVk", - AccessToken: "1b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "1S6TmO67z", - Title: "my first dashboard", - Slug: "my-first-dashboard", - IsEnabled: true, - }, - { - Uid: "2GwW7mgVk", - AccessToken: "2b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "2S6TmO67z", - Title: "my second dashboard", - Slug: "my-second-dashboard", - IsEnabled: false, - }, - { - Uid: "0GwW7mgVk", - AccessToken: "0b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "0S6TmO67z", - Title: "my zero dashboard", - Slug: "my-zero-dashboard", - IsEnabled: true, - }, - { - Uid: "9GwW7mgVk", - AccessToken: "deletedashboardaccesstoken", - DashboardUid: "9S6TmO67z", - Title: "test", - Slug: "test", - IsEnabled: true, - }, - } - mockedStoreResponse := []*PublicDashboardListResponse{ - { - Uid: "0GwW7mgVk", - AccessToken: "0b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "0S6TmO67z", - IsEnabled: true, - }, - { - Uid: "1GwW7mgVk", - AccessToken: "1b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "1S6TmO67z", - IsEnabled: true, - }, - { - Uid: "2GwW7mgVk", - AccessToken: "2b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "2S6TmO67z", - IsEnabled: false, - }, - { - Uid: "9GwW7mgVk", - AccessToken: "deletedashboardaccesstoken", - DashboardUid: "9S6TmO67z", - IsEnabled: true, - }, - } - - testCases := []struct { - name string - args args - want *PublicDashboardListResponseWithPagination - mockResponse *mockResponse - wantErr assert.ErrorAssertionFunc - }{ - { - name: "should return full response when user has access to all dashboards", - args: args{ - ctx: context.Background(), - query: &PublicDashboardListQuery{ - User: &user.SignedInUser{OrgID: 1, UserID: 1, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"*"}, "folders:read": {"*"}}}}, - OrgID: 1, - Page: 1, - Limit: 50, - }, - }, - mockResponse: &mockResponse{ - PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ - TotalCount: int64(len(mockedStoreResponse)), - PublicDashboards: mockedStoreResponse, - }, - Err: nil, - }, - want: &PublicDashboardListResponseWithPagination{ - Page: 1, - PerPage: 50, - TotalCount: int64(len(expectedFinalResponse)), - PublicDashboards: expectedFinalResponse, - }, - wantErr: assert.NoError, - }, - { - name: "should only return the one dashboard user 2 has access to", - args: args{ - ctx: context.Background(), - query: &PublicDashboardListQuery{ - User: &user.SignedInUser{OrgID: 1, UserID: 2, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:1S6TmO67z"}, "folders:read": {"*"}}}}, - OrgID: 1, - Page: 1, - Limit: 50, - }, - }, - mockResponse: &mockResponse{ - PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ - TotalCount: int64(len(mockedStoreResponse)), - PublicDashboards: mockedStoreResponse, - }, - Err: nil, - }, - want: &PublicDashboardListResponseWithPagination{ - Page: 1, - PerPage: 50, - TotalCount: 1, - PublicDashboards: []*PublicDashboardListResponse{expectedFinalResponse[0]}, - }, - wantErr: assert.NoError, - }, - { - name: "should return full response when user 3 has specific access to all dashboards", - args: args{ - ctx: context.Background(), - query: &PublicDashboardListQuery{ - User: &user.SignedInUser{OrgID: 1, UserID: 3, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:0S6TmO67z", "dashboards:uid:1S6TmO67z", "dashboards:uid:2S6TmO67z", "dashboards:uid:9S6TmO67z"}, "folders:read": {"*"}}}}, - OrgID: 1, - Page: 1, - Limit: 50, - }, - }, - mockResponse: &mockResponse{ - PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ - TotalCount: int64(len(mockedStoreResponse)), - PublicDashboards: mockedStoreResponse, - }, - Err: nil, - }, - want: &PublicDashboardListResponseWithPagination{ - Page: 1, - PerPage: 50, - TotalCount: int64(len(expectedFinalResponse)), - PublicDashboards: expectedFinalResponse, - }, - wantErr: assert.NoError, - }, - { - name: "should an empty response for a user with no access", - args: args{ - ctx: context.Background(), - query: &PublicDashboardListQuery{ - User: &user.SignedInUser{OrgID: 1, UserID: 4, Permissions: map[int64]map[string][]string{}}, - OrgID: 1, - Page: 1, - Limit: 50, - }, - }, - mockResponse: &mockResponse{ - PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ - TotalCount: int64(len(mockedStoreResponse)), - PublicDashboards: mockedStoreResponse, - }, - Err: nil, - }, - want: &PublicDashboardListResponseWithPagination{ - Page: 1, - PerPage: 50, - TotalCount: 0, - PublicDashboards: []*PublicDashboardListResponse{}, - }, - wantErr: assert.NoError, - }, - { - name: "should return correct pagination response if limited", - args: args{ - ctx: context.Background(), - query: &PublicDashboardListQuery{ - User: &user.SignedInUser{OrgID: 1, UserID: 1, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"*"}, "folders:read": {"*"}}}}, - OrgID: 1, - Page: 1, - Limit: 2, - }, - }, - mockResponse: &mockResponse{ - PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ - TotalCount: int64(len(mockedStoreResponse)), - PublicDashboards: mockedStoreResponse, - }, - Err: nil, - }, - want: &PublicDashboardListResponseWithPagination{ - Page: 1, - PerPage: 2, - TotalCount: 4, - PublicDashboards: expectedFinalResponse[:2], - }, - wantErr: assert.NoError, - }, - { - name: "should return correct page", - args: args{ - ctx: context.Background(), - query: &PublicDashboardListQuery{ - User: &user.SignedInUser{OrgID: 1, UserID: 1, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"*"}, "folders:read": {"*"}}}}, - OrgID: 1, - Page: 2, - Limit: 2, - }, - }, - mockResponse: &mockResponse{ - PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ - TotalCount: int64(len(mockedStoreResponse)), - PublicDashboards: mockedStoreResponse, - }, - Err: nil, - }, - want: &PublicDashboardListResponseWithPagination{ - Page: 2, - PerPage: 2, - TotalCount: 4, - PublicDashboards: expectedFinalResponse[2:], - }, - wantErr: assert.NoError, - }, - { - name: "should return error when store returns error", - args: args{ - ctx: context.Background(), - query: &PublicDashboardListQuery{ - User: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ - 1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}}, - }, - OrgID: 1, - Page: 1, - Limit: 50, - }, - }, - mockResponse: &mockResponse{ - PublicDashboardListResponseWithPagination: nil, - Err: errors.New("an err"), - }, - want: nil, - wantErr: assert.Error, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - store := NewFakePublicDashboardStore(t) - store.On("FindAll", mock.Anything, mock.Anything). - Return(tt.mockResponse.PublicDashboardListResponseWithPagination, tt.mockResponse.Err) - pd, _, _ := newPublicDashboardServiceImpl(t, testDB, cfg, store, dashboardService, nil) - pd.ac = ac - - got, err := pd.FindAllWithPagination(tt.args.ctx, tt.args.query) - if !tt.wantErr(t, err, fmt.Sprintf("FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query)) { - return - } - assert.Equalf(t, tt.want, got, "FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query) - }) - } -} - func TestPublicDashboardServiceImpl_NewPublicDashboardUid(t *testing.T) { mockedDashboard := &PublicDashboard{ IsEnabled: true, diff --git a/pkg/tests/api/publicdashboards/public_dashboard_query_test.go b/pkg/tests/api/publicdashboards/public_dashboard_query_test.go new file mode 100644 index 00000000000..7f12940c705 --- /dev/null +++ b/pkg/tests/api/publicdashboards/public_dashboard_query_test.go @@ -0,0 +1,195 @@ +package publicdashboards + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests" + "github.com/grafana/grafana/pkg/tests/testinfra" +) + +func TestPublicDashboardQueryAPI(t *testing.T) { + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + EnableFeatureToggles: []string{ + featuremgmt.FlagPublicDashboardsEmailSharing, + }, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + adminUsername := fmt.Sprintf("testadmin-%d", time.Now().UnixNano()) + tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Login: adminUsername, + Password: "admin", + IsAdmin: true, + }) + adminClient := createHTTPClient(grafanaListedAddr, adminUsername, "admin") + + datasourcePayload := map[string]interface{}{ + "name": "Test Data Source", + "type": "prometheus", + "uid": "prometheus", + "url": "http://localhost:9090", + "access": "proxy", + } + datasourceBytes, err := json.Marshal(datasourcePayload) + require.NoError(t, err) + var datasourceResult map[string]interface{} + createDatasourceResp := doRequest(t, adminClient, "POST", "/api/datasources", datasourceBytes, &datasourceResult) + require.Equal(t, 200, createDatasourceResp.StatusCode) + + t.Run("unauthenticated user can query public dashboard panel", func(t *testing.T) { + // create dashboard first + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Test Dashboard for Query", + "time": map[string]interface{}{ + "from": "now-1h", + "to": "now", + }, + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + "targets": []map[string]interface{}{ + { + "refId": "A", + "expr": "up", + "datasource": map[string]interface{}{ + "type": "prometheus", + "uid": "prometheus", + }, + }, + }, + }, + }, + }, + "folderUid": "", + "overwrite": false, + } + payloadBytes, err := json.Marshal(dashboardPayload) + require.NoError(t, err) + var dashboardResult map[string]interface{} + createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult) + require.Equal(t, 200, createDashboardResp.StatusCode) + + // make it public + dashboardUID := dashboardResult["uid"].(string) + publicDashboardPayload := map[string]interface{}{ + "isEnabled": true, + "annotationsEnabled": false, + "timeSelectionEnabled": false, + "share": "public", + } + payloadBytes, err = json.Marshal(publicDashboardPayload) + require.NoError(t, err) + createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID) + var publicDashboard map[string]interface{} + createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard) + require.Equal(t, 200, createResp.StatusCode) + assert.Equal(t, true, publicDashboard["isEnabled"]) + assert.NotEmpty(t, publicDashboard["accessToken"]) + + // test unauthenticated query to the public dashboard panel + accessToken := publicDashboard["accessToken"].(string) + queryPayload := map[string]interface{}{} + queryBytes, err := json.Marshal(queryPayload) + require.NoError(t, err) + queryURL := fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", accessToken) + unauthenticatedClient := createUnauthenticatedClient(grafanaListedAddr) + + var queryResult map[string]interface{} + doRequest(t, unauthenticatedClient, "POST", queryURL, queryBytes, &queryResult) + assert.NotNil(t, queryResult["results"]) + results := queryResult["results"].(map[string]interface{}) + assert.NotNil(t, results["A"]) + }) + + t.Run("unauthenticated user cannot query disabled public dashboard", func(t *testing.T) { + // create the dashboard + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Test Disabled Dashboard", + "time": map[string]interface{}{ + "from": "now-1h", + "to": "now", + }, + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + }, + }, + }, + "folderUid": "", + "overwrite": false, + } + payloadBytes, err := json.Marshal(dashboardPayload) + require.NoError(t, err) + var dashboardResult map[string]interface{} + createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult) + require.Equal(t, 200, createDashboardResp.StatusCode) + + // make it a disabled public dashboard + dashboardUID := dashboardResult["uid"].(string) + publicDashboardPayload := map[string]interface{}{ + "isEnabled": false, + "annotationsEnabled": false, + "timeSelectionEnabled": true, + "share": "public", + } + payloadBytes, err = json.Marshal(publicDashboardPayload) + require.NoError(t, err) + createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID) + var publicDashboard map[string]interface{} + createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard) + require.Equal(t, 200, createResp.StatusCode) + assert.Equal(t, false, publicDashboard["isEnabled"]) + assert.NotEmpty(t, publicDashboard["accessToken"]) + + accessToken := publicDashboard["accessToken"].(string) + + queryPayload := map[string]interface{}{ + "intervalMs": 1000, + "maxDataPoints": 100, + "timeRange": map[string]interface{}{ + "from": "now-1h", + "to": "now", + }, + } + queryBytes, err := json.Marshal(queryPayload) + require.NoError(t, err) + + // should not be able to query anymore + queryURL := fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", accessToken) + unauthenticatedClient := createUnauthenticatedClient(grafanaListedAddr) + var queryResult map[string]interface{} + queryResp := doRequest(t, unauthenticatedClient, "POST", queryURL, queryBytes, &queryResult) + require.Equal(t, 403, queryResp.StatusCode) + require.Nil(t, queryResult["results"]) + }) +} + +func createUnauthenticatedClient(host string) *httpClient { + baseURL := fmt.Sprintf("http://%s", host) + return &httpClient{ + baseURL: baseURL, + client: &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + } +} diff --git a/pkg/tests/api/publicdashboards/public_dashboards_api_test.go b/pkg/tests/api/publicdashboards/public_dashboards_api_test.go new file mode 100644 index 00000000000..d4a97e068a7 --- /dev/null +++ b/pkg/tests/api/publicdashboards/public_dashboards_api_test.go @@ -0,0 +1,439 @@ +package publicdashboards + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestPublicDashboardsAPI(t *testing.T) { + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + EnableFeatureToggles: []string{ + featuremgmt.FlagPublicDashboardsEmailSharing, + }, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + adminUsername := fmt.Sprintf("testadmin-%d", time.Now().UnixNano()) + tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Login: adminUsername, + Password: "admin", + IsAdmin: true, + }) + adminClient := createHTTPClient(grafanaListedAddr, adminUsername, "admin") + + t.Run("should create, get, update, and delete public dashboard", func(t *testing.T) { + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Test Dashboard", + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + }, + }, + }, + "folderUid": "", + "overwrite": false, + } + + payloadBytes, err := json.Marshal(dashboardPayload) + require.NoError(t, err) + + var dashboardResult map[string]interface{} + createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult) + require.Equal(t, 200, createDashboardResp.StatusCode) + + dashboardUID := dashboardResult["uid"].(string) + + var listResult map[string]interface{} + doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards", nil, &listResult) + publicDashboardPayload := map[string]interface{}{ + "isEnabled": true, + "annotationsEnabled": false, + "timeSelectionEnabled": true, + "share": "public", + } + + payloadBytes, err = json.Marshal(publicDashboardPayload) + require.NoError(t, err) + + createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID) + var publicDashboard map[string]interface{} + createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard) + require.Equal(t, 200, createResp.StatusCode) + assert.Equal(t, true, publicDashboard["isEnabled"]) + assert.Equal(t, false, publicDashboard["annotationsEnabled"]) + assert.Equal(t, true, publicDashboard["timeSelectionEnabled"]) + assert.Equal(t, "public", publicDashboard["share"]) + assert.NotEmpty(t, publicDashboard["accessToken"]) + assert.NotEmpty(t, publicDashboard["uid"]) + + accessToken := publicDashboard["accessToken"].(string) + publicDashboardUID := publicDashboard["uid"].(string) + + // get the public dashboard + getURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID) + var retrievedPD map[string]interface{} + getResp := doRequest(t, adminClient, "GET", getURL, nil, &retrievedPD) + require.Equal(t, 200, getResp.StatusCode) + + // view the public dashboard + viewURL := fmt.Sprintf("/api/public/dashboards/%s", accessToken) + var dashboardData map[string]interface{} + viewResp := doRequest(t, adminClient, "GET", viewURL, nil, &dashboardData) + require.Equal(t, 200, viewResp.StatusCode) + assert.Equal(t, "Test Dashboard", dashboardData["dashboard"].(map[string]interface{})["title"]) + assert.Equal(t, "Test Panel", dashboardData["dashboard"].(map[string]interface{})["panels"].([]interface{})[0].(map[string]interface{})["title"]) + + updatePayload := map[string]interface{}{ + "isEnabled": false, + "annotationsEnabled": true, + "timeSelectionEnabled": false, + "share": "email", + } + updateBytes, err := json.Marshal(updatePayload) + require.NoError(t, err) + updateURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", dashboardUID, publicDashboardUID) + var updatedPD map[string]interface{} + updateResp := doRequest(t, adminClient, "PATCH", updateURL, updateBytes, &updatedPD) + require.Equal(t, 200, updateResp.StatusCode) + assert.Equal(t, false, updatedPD["isEnabled"]) + assert.Equal(t, true, updatedPD["annotationsEnabled"]) + assert.Equal(t, false, updatedPD["timeSelectionEnabled"]) + assert.Equal(t, "email", updatedPD["share"]) + + deleteURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", dashboardUID, publicDashboardUID) + var deleteResult map[string]interface{} + deleteResp := doRequest(t, adminClient, "DELETE", deleteURL, nil, &deleteResult) + require.Equal(t, 200, deleteResp.StatusCode) + var getAfterDeleteResult map[string]interface{} + getAfterDeleteResp := doRequest(t, adminClient, "GET", getURL, nil, &getAfterDeleteResult) + require.Equal(t, 404, getAfterDeleteResp.StatusCode) + }) + + t.Run("should list public dashboards", func(t *testing.T) { + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Test Dashboard for List", + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + }, + }, + }, + "folderUid": "", + "overwrite": false, + } + + payloadBytes, err := json.Marshal(dashboardPayload) + require.NoError(t, err) + + var dashboardResult map[string]interface{} + createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult) + require.Equal(t, 200, createDashboardResp.StatusCode) + dashboardUID := dashboardResult["uid"].(string) + + publicDashboardPayload := map[string]interface{}{ + "isEnabled": true, + "annotationsEnabled": false, + "timeSelectionEnabled": true, + "share": "public", + } + + payloadBytes, err = json.Marshal(publicDashboardPayload) + require.NoError(t, err) + + createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID) + var createResult map[string]interface{} + createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &createResult) + require.Equal(t, 200, createResp.StatusCode) + + var listData map[string]interface{} + listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards", nil, &listData) + require.Equal(t, 200, listResp.StatusCode) + assert.NotEmpty(t, listData["publicDashboards"]) + publicDashboards := listData["publicDashboards"].([]interface{}) + assert.GreaterOrEqual(t, len(publicDashboards), 1) + }) + + t.Run("should handle invalid access token", func(t *testing.T) { + var viewResult map[string]interface{} + viewResp := doRequest(t, adminClient, "GET", "/api/public/dashboards/invalid-token", nil, &viewResult) + require.Equal(t, 400, viewResp.StatusCode) + }) + + t.Run("should handle disabled public dashboard", func(t *testing.T) { + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Test Dashboard Disabled", + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + }, + }, + }, + "folderUid": "", + "overwrite": false, + } + + payloadBytes, err := json.Marshal(dashboardPayload) + require.NoError(t, err) + + var dashboardResult map[string]interface{} + createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult) + require.Equal(t, 200, createDashboardResp.StatusCode) + + dashboardUID := dashboardResult["uid"].(string) + publicDashboardPayload := map[string]interface{}{ + "isEnabled": false, + "annotationsEnabled": false, + "timeSelectionEnabled": true, + "share": "public", + } + + payloadBytes, err = json.Marshal(publicDashboardPayload) + require.NoError(t, err) + + createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID) + var publicDashboard map[string]interface{} + createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard) + require.Equal(t, 200, createResp.StatusCode) + accessToken := publicDashboard["accessToken"].(string) + + var viewResult map[string]interface{} + viewResp := doRequest(t, adminClient, "GET", fmt.Sprintf("/api/public/dashboards/%s", accessToken), nil, &viewResult) + require.Equal(t, 403, viewResp.StatusCode) + }) + + t.Run("permission test", func(t *testing.T) { + dashboards := []map[string]interface{}{ + { + "dashboard": map[string]interface{}{ + "title": "test", + "uid": "9S6TmO67z", + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + }, + }, + }, + "folderUid": "", + "overwrite": false, + }, + { + "dashboard": map[string]interface{}{ + "title": "my first dashboard", + "uid": "1S6TmO67z", + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + }, + }, + }, + "folderUid": "", + "overwrite": false, + }, + { + "dashboard": map[string]interface{}{ + "title": "my second dashboard", + "uid": "2S6TmO67z", + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + }, + }, + }, + "folderUid": "", + "overwrite": false, + }, + { + "dashboard": map[string]interface{}{ + "title": "my zero dashboard", + "uid": "0S6TmO67z", + "panels": []map[string]interface{}{ + { + "id": 1, + "type": "stat", + "title": "Test Panel", + }, + }, + }, + "folderUid": "", + "overwrite": false, + }, + } + + dashboardUIDs := make([]string, len(dashboards)) + publicDashboardUIDs := make([]string, len(dashboards)) + + for i, dashboardPayload := range dashboards { + payloadBytes, err := json.Marshal(dashboardPayload) + require.NoError(t, err) + + var dashboardResult map[string]interface{} + createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult) + require.Equal(t, 200, createDashboardResp.StatusCode) + dashboardUIDs[i] = dashboardResult["uid"].(string) + + isEnabled := i != 1 + publicDashboardPayload := map[string]interface{}{ + "isEnabled": isEnabled, + "annotationsEnabled": false, + "timeSelectionEnabled": true, + "share": "public", + } + + payloadBytes, err = json.Marshal(publicDashboardPayload) + require.NoError(t, err) + + createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUIDs[i]) + var publicDashboard map[string]interface{} + createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard) + require.Equal(t, 200, createResp.StatusCode) + publicDashboardUIDs[i] = publicDashboard["uid"].(string) + } + + t.Run("admin user should see all dashboards", func(t *testing.T) { + var listData map[string]interface{} + listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=50", nil, &listData) + require.Equal(t, 200, listResp.StatusCode) + + totalCount := int64(listData["totalCount"].(float64)) + assert.GreaterOrEqual(t, totalCount, int64(4)) + }) + + t.Run("user with access to just one dashboard should see only that dashboard", func(t *testing.T) { + limitedUserUsername := fmt.Sprintf("limiteduser-%d", time.Now().UnixNano()) + limitedUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleNone), + Login: limitedUserUsername, + Password: "password", + IsAdmin: false, + }) + limitedUserClient := createHTTPClient(grafanaListedAddr, limitedUserUsername, "password") + permissionPayload := map[string]interface{}{ + "permission": "View", + } + permissionBytes, err := json.Marshal(permissionPayload) + require.NoError(t, err) + + permissionURL := fmt.Sprintf("/api/access-control/dashboards/9S6TmO67z/users/%d", limitedUserID) + var permissionResult map[string]interface{} + permissionResp := doRequest(t, adminClient, "POST", permissionURL, permissionBytes, &permissionResult) + require.Equal(t, 200, permissionResp.StatusCode) + + var listData map[string]interface{} + listResp := doRequest(t, limitedUserClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=50", nil, &listData) + require.Equal(t, 200, listResp.StatusCode) + + totalCount := int64(listData["totalCount"].(float64)) + assert.Equal(t, int64(1), totalCount) + }) + + t.Run("pagination should work correctly", func(t *testing.T) { + var listData map[string]interface{} + listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=2", nil, &listData) + require.Equal(t, 200, listResp.StatusCode) + assert.NotEmpty(t, listData["publicDashboards"]) + publicDashboards := listData["publicDashboards"].([]interface{}) + assert.Equal(t, 2, len(publicDashboards)) + totalCount := int64(listData["totalCount"].(float64)) + assert.GreaterOrEqual(t, totalCount, int64(4)) + + var listDataPage2 map[string]interface{} + listRespPage2 := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=2&perpage=2", nil, &listDataPage2) + require.Equal(t, 200, listRespPage2.StatusCode) + publicDashboardsPage2 := listDataPage2["publicDashboards"].([]interface{}) + assert.Equal(t, 2, len(publicDashboardsPage2)) + }) + }) +} + +type httpClient struct { + baseURL string + client *http.Client +} + +func createHTTPClient(host, username, password string) *httpClient { + baseURL := fmt.Sprintf("http://%s:%s@%s", username, password, host) + return &httpClient{ + baseURL: baseURL, + client: &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + } +} + +type httpResponse struct { + StatusCode int + Body []byte +} + +func doRequest(t *testing.T, client *httpClient, method, path string, body []byte, result interface{}) httpResponse { + t.Helper() + + var req *http.Request + var err error + + url := client.baseURL + path + if body != nil { + req, err = http.NewRequest(method, url, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + } else { + req, err = http.NewRequest(method, url, nil) + } + require.NoError(t, err) + + resp, err := client.client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() // nolint:errcheck + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + response := httpResponse{ + StatusCode: resp.StatusCode, + Body: respBody, + } + + if result != nil && len(respBody) > 0 { + err = json.Unmarshal(respBody, result) + require.NoError(t, err) + } + + return response +}