public dashboards: move into into its own service (#51358)

This PR moves public dashboards into its own self contained service including API, Service, Database, and Models. Routes are mounted on the Grafana HTTPServer by the API service at injection time with wire.go. The main  route that loads the frontend for public dashboards is still handled by the API package.

Co-authored-by: Jesse Weaver <jesse.weaver@grafana.com>
Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com>
This commit is contained in:
Jeff Levin
2022-07-06 17:51:44 -06:00
committed by GitHub
co-authored by Jesse Weaver Owen Smallwood
parent ba2d8cd838
commit eacee08135
23 changed files with 1632 additions and 1198 deletions
+6 -13
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/web"
)
@@ -103,6 +104,10 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/dashboards/*", reqSignedIn, hs.Index)
r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index)
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
r.Get("/public-dashboards/:accessToken", publicdashboardsapi.SetPublicDashboardFlag(), hs.Index)
}
r.Get("/explore", authorize(func(c *models.ReqContext) {
if f, ok := reqSignedIn.(func(c *models.ReqContext)); ok {
f(c)
@@ -391,11 +396,6 @@ func (hs *HTTPServer) registerRoutes() {
})
dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) {
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
dashUidRoute.Get("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetPublicDashboardConfig))
dashUidRoute.Post("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.SavePublicDashboardConfig))
}
if hs.ThumbService != nil {
dashUidRoute.Get("/img/:kind/:theme", hs.ThumbService.GetImage)
if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
@@ -598,7 +598,7 @@ func (hs *HTTPServer) registerRoutes() {
// grafana.net proxy
r.Any("/api/gnet/*", reqSignedIn, hs.ProxyGnetRequest)
// Gravatar service.
// Gravatar service
r.Get("/avatar/:hash", hs.AvatarCacheServer.Handler)
// Snapshots
@@ -608,13 +608,6 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey))
r.Delete("/api/snapshots/:key", reqEditorRole, routing.Wrap(hs.DeleteDashboardSnapshot))
// Public API
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
r.Get("/public-dashboards/:accessToken", middleware.SetPublicDashboardFlag(), hs.Index)
r.Get("/api/public/dashboards/:accessToken", routing.Wrap(hs.GetPublicDashboard))
r.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(hs.QueryPublicDashboard))
}
// Frontend logs
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS)
r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now),
-10
View File
@@ -43,7 +43,6 @@ import (
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/searchusers/filters"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/grafana/grafana/pkg/web/webtest"
@@ -341,15 +340,6 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, db, featuremgmt.WithFeatures())
}
func setupHTTPServerWithMockDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, features *featuremgmt.FeatureManager) accessControlScenarioContext {
// Use a new conf
cfg := setting.NewCfg()
db := sqlstore.InitTestDB(t)
db.Cfg = setting.NewCfg()
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, mockstore.NewSQLStoreMock(), features)
}
func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore, store sqlstore.Store, features *featuremgmt.FeatureManager) accessControlScenarioContext {
t.Helper()
-141
View File
@@ -1,141 +0,0 @@
package api
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/web"
)
// gets public dashboard
func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response {
accessToken := web.Params(c.Req)[":accessToken"]
dash, err := hs.DashboardService.GetPublicDashboard(c.Req.Context(), accessToken)
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err)
}
meta := dtos.DashboardMeta{
Slug: dash.Slug,
Type: models.DashTypeDB,
CanStar: false,
CanSave: false,
CanEdit: false,
CanAdmin: false,
CanDelete: false,
Created: dash.Created,
Updated: dash.Updated,
Version: dash.Version,
IsFolder: false,
FolderId: dash.FolderId,
PublicDashboardAccessToken: accessToken,
}
dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data}
return response.JSON(http.StatusOK, dto)
}
// gets public dashboard configuration for dashboard
func (hs *HTTPServer) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
pdc, err := hs.DashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"])
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err)
}
return response.JSON(http.StatusOK, pdc)
}
// sets public dashboard configuration for dashboard
func (hs *HTTPServer) SavePublicDashboardConfig(c *models.ReqContext) response.Response {
pubdash := &models.PublicDashboard{}
if err := web.Bind(c.Req, pubdash); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
// Always set the org id to the current auth session orgId
pubdash.OrgId = c.OrgId
dto := dashboards.SavePublicDashboardConfigDTO{
OrgId: c.OrgId,
DashboardUid: web.Params(c.Req)[":uid"],
UserId: c.UserId,
PublicDashboard: pubdash,
}
pubdash, err := hs.DashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto)
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err)
}
return response.JSON(http.StatusOK, pubdash)
}
// QueryPublicDashboard returns all results for a given panel on a public dashboard
// POST /api/public/dashboard/:accessToken/panels/:panelId/query
func (hs *HTTPServer) QueryPublicDashboard(c *models.ReqContext) response.Response {
panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "invalid panel ID", err)
}
dashboard, err := hs.DashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"])
if err != nil {
return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err)
}
publicDashboard, err := hs.DashboardService.GetPublicDashboardConfig(c.Req.Context(), dashboard.OrgId, dashboard.Uid)
if err != nil {
return response.Error(http.StatusInternalServerError, "could not fetch public dashboard", err)
}
reqDTO, err := hs.DashboardService.BuildPublicDashboardMetricRequest(
c.Req.Context(),
dashboard,
publicDashboard,
panelId,
)
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err)
}
// Get all needed datasource UIDs from queries
var uids []string
for _, query := range reqDTO.Queries {
uids = append(uids, query.Get("datasource").Get("uid").MustString())
}
// Create a temp user with read-only datasource permissions
anonymousUser := &models.SignedInUser{OrgId: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)}
permissions := make(map[string][]string)
datasourceScope := fmt.Sprintf("datasources:uid:%s", strings.Join(uids, ","))
permissions[datasources.ActionQuery] = []string{datasourceScope}
permissions[datasources.ActionRead] = []string{datasourceScope}
anonymousUser.Permissions[dashboard.OrgId] = permissions
resp, err := hs.queryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true)
if err != nil {
return hs.handleQueryMetricsError(err)
}
return hs.toJsonStreamingResponse(resp)
}
// util to help us unpack a dashboard err or use default http code and message
func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response {
var dashboardErr dashboards.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr)
}
return response.Error(defaultCode, defaultMsg, err)
}
-630
View File
@@ -1,630 +0,0 @@
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/google/uuid"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
func TestAPIGetPublicDashboard(t *testing.T) {
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures())
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(&models.Dashboard{}, nil).Maybe()
sc.hs.DashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
response := callAPI(
sc.server,
http.MethodGet,
"/api/public/dashboards",
nil,
t,
)
assert.Equal(t, http.StatusNotFound, response.Code)
response = callAPI(
sc.server,
http.MethodGet,
"/api/public/dashboards/asdf",
nil,
t,
)
assert.Equal(t, http.StatusNotFound, response.Code)
})
DashboardUid := "dashboard-abcd1234"
token, err := uuid.NewRandom()
require.NoError(t, err)
accessToken := fmt.Sprintf("%x", token)
testCases := []struct {
Name string
AccessToken string
ExpectedHttpResponse int
publicDashboardResult *models.Dashboard
publicDashboardErr error
}{
{
Name: "It gets a public dashboard",
AccessToken: accessToken,
ExpectedHttpResponse: http.StatusOK,
publicDashboardResult: &models.Dashboard{
Data: simplejson.NewFromAny(map[string]interface{}{
"Uid": DashboardUid,
}),
},
publicDashboardErr: nil,
},
{
Name: "It should return 404 if isPublicDashboard is false",
AccessToken: accessToken,
ExpectedHttpResponse: http.StatusNotFound,
publicDashboardResult: nil,
publicDashboardErr: dashboards.ErrPublicDashboardNotFound,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(test.publicDashboardResult, test.publicDashboardErr)
sc.hs.DashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
response := callAPI(
sc.server,
http.MethodGet,
fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken),
nil,
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if test.publicDashboardErr == nil {
var dashResp dtos.DashboardFullWithMeta
err := json.Unmarshal(response.Body.Bytes(), &dashResp)
require.NoError(t, err)
assert.Equal(t, DashboardUid, dashResp.Dashboard.Get("Uid").MustString())
assert.Equal(t, false, dashResp.Meta.CanEdit)
assert.Equal(t, false, dashResp.Meta.CanDelete)
assert.Equal(t, false, dashResp.Meta.CanSave)
} else {
var errResp struct {
Error string `json:"error"`
}
err := json.Unmarshal(response.Body.Bytes(), &errResp)
require.NoError(t, err)
assert.Equal(t, test.publicDashboardErr.Error(), errResp.Error)
}
})
}
}
func TestAPIGetPublicDashboardConfig(t *testing.T) {
pubdash := &models.PublicDashboard{IsEnabled: true}
testCases := []struct {
Name string
DashboardUid string
ExpectedHttpResponse int
PublicDashboardResult *models.PublicDashboard
PublicDashboardError error
}{
{
Name: "retrieves public dashboard config when dashboard is found",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: pubdash,
PublicDashboardError: nil,
},
{
Name: "returns 404 when dashboard not found",
DashboardUid: "77777",
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardError: dashboards.ErrDashboardNotFound,
},
{
Name: "returns 500 when internal server error",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusInternalServerError,
PublicDashboardResult: nil,
PublicDashboardError: errors.New("database broken"),
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardError)
sc.hs.DashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
response := callAPI(
sc.server,
http.MethodGet,
"/api/dashboards/uid/1/public-config",
nil,
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if response.Code == http.StatusOK {
var pdcResp models.PublicDashboard
err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
require.NoError(t, err)
assert.Equal(t, test.PublicDashboardResult, &pdcResp)
}
})
}
}
func TestApiSavePublicDashboardConfig(t *testing.T) {
testCases := []struct {
Name string
DashboardUid string
publicDashboardConfig *models.PublicDashboard
ExpectedHttpResponse int
saveDashboardError error
}{
{
Name: "returns 200 when update persists",
DashboardUid: "1",
publicDashboardConfig: &models.PublicDashboard{IsEnabled: true},
ExpectedHttpResponse: http.StatusOK,
saveDashboardError: nil,
},
{
Name: "returns 500 when not persisted",
ExpectedHttpResponse: http.StatusInternalServerError,
publicDashboardConfig: &models.PublicDashboard{},
saveDashboardError: errors.New("backend failed to save"),
},
{
Name: "returns 404 when dashboard not found",
ExpectedHttpResponse: http.StatusNotFound,
publicDashboardConfig: &models.PublicDashboard{},
saveDashboardError: dashboards.ErrDashboardNotFound,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")).
Return(&models.PublicDashboard{IsEnabled: true}, test.saveDashboardError)
sc.hs.DashboardService = dashSvc
setInitCtxSignedInViewer(sc.initCtx)
response := callAPI(
sc.server,
http.MethodPost,
"/api/dashboards/uid/1/public-config",
strings.NewReader(`{ "isPublic": true }`),
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
// check the result if it's a 200
if response.Code == http.StatusOK {
val, err := json.Marshal(test.publicDashboardConfig)
require.NoError(t, err)
assert.Equal(t, string(val), response.Body.String())
}
})
}
}
// `/public/dashboards/:uid/query`` endpoint test
func TestAPIQueryPublicDashboard(t *testing.T) {
queryReturnsError := false
qds := query.ProvideService(
nil,
&fakeDatasources.FakeCacheService{
DataSources: []*datasources.DataSource{
{Uid: "mysqlds"},
{Uid: "promds"},
{Uid: "promds2"},
},
},
nil,
&fakePluginRequestValidator{},
&fakeDatasources.FakeDataSourceService{},
&fakePluginClient{
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
if queryReturnsError {
return nil, errors.New("error")
}
resp := backend.Responses{}
for _, query := range req.Queries {
resp[query.RefID] = backend.DataResponse{
Frames: []*data.Frame{
{
RefID: query.RefID,
Name: "query-" + query.RefID,
},
},
}
}
return &backend.QueryDataResponse{Responses: resp}, nil
},
},
&fakeOAuthTokenService{},
)
setup := func(enabled bool) (*webtest.Server, *dashboards.FakeDashboardService) {
fakeDashboardService := &dashboards.FakeDashboardService{}
return SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled)
hs.DashboardService = fakeDashboardService
}), fakeDashboardService
}
t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) {
server, _ := setup(false)
req := server.NewPostRequest(
"/api/public/dashboards/abc123/panels/2/query",
strings.NewReader("{}"),
)
resp, err := server.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) {
server, _ := setup(true)
req := server.NewPostRequest(
"/api/public/dashboards/abc123/panels/notanumber/query",
strings.NewReader("{}"),
)
resp, err := server.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil)
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil)
fakeDashboardService.On(
"BuildPublicDashboardMetricRequest",
mock.Anything,
mock.Anything,
mock.Anything,
int64(2),
).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
simplejson.MustJson([]byte(`
{
"datasource": {
"type": "prometheus",
"uid": "promds"
},
"exemplar": true,
"expr": "query_2_A",
"interval": "",
"legendFormat": "",
"refId": "A"
}
`)),
},
}, nil)
req := server.NewPostRequest(
"/api/public/dashboards/abc123/panels/2/query",
strings.NewReader("{}"),
)
resp, err := server.SendJSON(req)
require.NoError(t, err)
bodyBytes, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.JSONEq(
t,
`{
"results": {
"A": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": [],
"refId": "A",
"name": "query-A"
}
}
]
}
}
}`,
string(bodyBytes),
)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("Status code is 500 when the query fails", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil)
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil)
fakeDashboardService.On(
"BuildPublicDashboardMetricRequest",
mock.Anything,
mock.Anything,
mock.Anything,
int64(2),
).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
simplejson.MustJson([]byte(`
{
"datasource": {
"type": "prometheus",
"uid": "promds"
},
"exemplar": true,
"expr": "query_2_A",
"interval": "",
"legendFormat": "",
"refId": "A"
}
`)),
},
}, nil)
req := server.NewPostRequest(
"/api/public/dashboards/abc123/panels/2/query",
strings.NewReader("{}"),
)
queryReturnsError = true
resp, err := server.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
queryReturnsError = false
})
t.Run("Status code is 200 when a panel has queries from multiple datasources", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil)
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil)
fakeDashboardService.On(
"BuildPublicDashboardMetricRequest",
mock.Anything,
mock.Anything,
mock.Anything,
int64(2),
).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
simplejson.MustJson([]byte(`
{
"datasource": {
"type": "prometheus",
"uid": "promds"
},
"exemplar": true,
"expr": "query_2_A",
"interval": "",
"legendFormat": "",
"refId": "A"
}
`)),
simplejson.MustJson([]byte(`
{
"datasource": {
"type": "prometheus",
"uid": "promds2"
},
"exemplar": true,
"expr": "query_2_B",
"interval": "",
"legendFormat": "",
"refId": "B"
}
`)),
},
}, nil)
req := server.NewPostRequest(
"/api/public/dashboards/abc123/panels/2/query",
strings.NewReader("{}"),
)
resp, err := server.SendJSON(req)
require.NoError(t, err)
bodyBytes, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.JSONEq(
t,
`{
"results": {
"A": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": [],
"refId": "A",
"name": "query-A"
}
}
]
},
"B": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": [],
"refId": "B",
"name": "query-B"
}
}
]
}
}
}`,
string(bodyBytes),
)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
})
}
func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) {
config := setting.NewCfg()
db := sqlstore.InitTestDB(t)
scenario := setupHTTPServerWithCfgDb(t, false, false, config, db, db, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
scenario.initCtx.SkipCache = true
cacheService := service.ProvideCacheService(localcache.ProvideService(), db)
qds := query.ProvideService(
nil,
cacheService,
nil,
&fakePluginRequestValidator{},
&fakeDatasources.FakeDataSourceService{},
&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
},
},
&fakeOAuthTokenService{},
)
scenario.hs.queryDataService = qds
_ = db.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 := models.SaveDashboardCommand{
OrgId: 1,
FolderId: 1,
IsFolder: false,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test",
"panels": []map[string]interface{}{
{
"id": 1,
"targets": []map[string]interface{}{
{
"datasource": map[string]string{
"type": "mysql",
"uid": "ds1",
},
"refId": "A",
},
},
},
},
}),
}
dashboard, _ := scenario.dashboardsStore.SaveDashboard(saveDashboardCmd)
// Create public dashboard
savePubDashboardCmd := &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
PublicDashboard: &models.PublicDashboard{
IsEnabled: true,
},
}
pubdash, err := scenario.hs.DashboardService.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd)
require.NoError(t, err)
response := callAPI(
scenario.server,
http.MethodPost,
fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken),
strings.NewReader(`{}`),
t,
)
require.Equal(t, http.StatusOK, response.Code)
bodyBytes, err := ioutil.ReadAll(response.Body)
require.NoError(t, err)
require.JSONEq(
t,
`{
"results": {
"A": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": []
}
}
]
}
}
}`,
string(bodyBytes),
)
}
+5 -1
View File
@@ -62,6 +62,8 @@ import (
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/provisioning"
publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/quota"
@@ -163,6 +165,7 @@ type HTTPServer struct {
folderPermissionsService accesscontrol.FolderPermissionsService
dashboardPermissionsService accesscontrol.DashboardPermissionsService
dashboardVersionService dashver.Service
PublicDashboardsApi *publicdashboardsApi.Api
starService star.Service
CoremodelRegistry *registry.Generic
CoremodelStaticRegistry *registry.Static
@@ -202,7 +205,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static,
kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck,
kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck, publicDashboardsApi *publicdashboardsApi.Api,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@@ -287,6 +290,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
CoremodelRegistry: coremodelRegistry,
CoremodelStaticRegistry: coremodelStaticRegistry,
kvStore: kvStore,
PublicDashboardsApi: publicDashboardsApi,
secretsMigrator: secretsMigrator,
}
if hs.Listener != nil {