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:
co-authored by
Jesse Weaver
Owen Smallwood
parent
ba2d8cd838
commit
eacee08135
+6
-13
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user