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 15:51:44 -08:00 committed by GitHub
parent ba2d8cd838
commit eacee08135
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1632 additions and 1198 deletions

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),

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()

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)
}

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),
)
}

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 {

View File

@ -77,6 +77,10 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsettings"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
"github.com/grafana/grafana/pkg/services/preference/prefimpl"
"github.com/grafana/grafana/pkg/services/publicdashboards"
publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database"
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/quota"
@ -275,6 +279,11 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)),
starimpl.ProvideService,
dashverimpl.ProvideService,
publicdashboardsService.ProvideService,
wire.Bind(new(publicdashboards.Service), new(*publicdashboardsService.PublicDashboardServiceImpl)),
publicdashboardsStore.ProvideStore,
wire.Bind(new(publicdashboards.Store), new(*publicdashboardsStore.PublicDashboardStoreImpl)),
publicdashboardsApi.ProvideApi,
userimpl.ProvideService,
orgimpl.ProvideService,
)

View File

@ -3,15 +3,12 @@ package dashboards
import (
"context"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/models"
)
//go:generate mockery --name DashboardService --structname FakeDashboardService --inpackage --filename dashboard_service_mock.go
// DashboardService is a service for operating on dashboards.
type DashboardService interface {
BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error)
BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error)
BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error)
DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error
FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error)
@ -20,14 +17,11 @@ type DashboardService interface {
GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error
GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error
GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error
GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error)
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error)
HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error
HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error
ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error)
MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error
SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error)
SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboard, error)
SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
}
@ -66,18 +60,13 @@ type Store interface {
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error)
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error)
GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error)
GenerateNewPublicDashboardUid(ctx context.Context) (string, error)
HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error
HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error
// SaveAlerts saves dashboard alerts.
SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error
SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error)
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error)
UnprovisionDashboard(ctx context.Context, id int64) error
UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
// ValidateDashboardBeforeSave validates a dashboard before save.
ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error)

View File

@ -1,14 +1,12 @@
// Code generated by mockery v2.12.1. DO NOT EDIT.
// Code generated by mockery v2.12.2. DO NOT EDIT.
package dashboards
import (
context "context"
dtos "github.com/grafana/grafana/pkg/api/dtos"
mock "github.com/stretchr/testify/mock"
models "github.com/grafana/grafana/pkg/models"
mock "github.com/stretchr/testify/mock"
testing "testing"
)
@ -18,50 +16,6 @@ type FakeDashboardService struct {
mock.Mock
}
// BuildAnonymousUser provides a mock function with given fields: ctx, dashboard
func (_m *FakeDashboardService) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) {
ret := _m.Called(ctx, dashboard)
var r0 *models.SignedInUser
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard) *models.SignedInUser); ok {
r0 = rf(ctx, dashboard)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.SignedInUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard) error); ok {
r1 = rf(ctx, dashboard)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId
func (_m *FakeDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) {
ret := _m.Called(ctx, dashboard, publicDashboard, panelId)
var r0 dtos.MetricRequest
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *models.PublicDashboard, int64) dtos.MetricRequest); ok {
r0 = rf(ctx, dashboard, publicDashboard, panelId)
} else {
r0 = ret.Get(0).(dtos.MetricRequest)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *models.PublicDashboard, int64) error); ok {
r1 = rf(ctx, dashboard, publicDashboard, panelId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// BuildSaveDashboardCommand provides a mock function with given fields: ctx, dto, shouldValidateAlerts, validateProvisionedDashboard
func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
ret := _m.Called(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard)
@ -192,52 +146,6 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *models
return r0
}
// GetPublicDashboard provides a mock function with given fields: ctx, accessToken
func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) {
ret := _m.Called(ctx, accessToken)
var r0 *models.Dashboard
if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok {
r0 = rf(ctx, accessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Dashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, accessToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, orgId, dashboardUid)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok {
r0 = rf(ctx, orgId, dashboardUid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgId, dashboardUid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasAdminPermissionInDashboardsOrFolders provides a mock function with given fields: ctx, query
func (_m *FakeDashboardService) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error {
ret := _m.Called(ctx, query)
@ -326,29 +234,6 @@ func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDash
return r0, r1
}
// SavePublicDashboardConfig provides a mock function with given fields: ctx, dto
func (_m *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, dto)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, *SavePublicDashboardConfigDTO) *models.PublicDashboard); ok {
r0 = rf(ctx, dto)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *SavePublicDashboardConfigDTO) error); ok {
r1 = rf(ctx, dto)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchDashboards provides a mock function with given fields: ctx, query
func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error {
ret := _m.Called(ctx, query)

View File

@ -15,13 +15,6 @@ type SaveDashboardDTO struct {
Dashboard *models.Dashboard
}
type SavePublicDashboardConfigDTO struct {
DashboardUid string
OrgId int64
UserId int64
PublicDashboard *models.PublicDashboard
}
type DashboardSearchProjection struct {
ID int64 `xorm:"id"`
UID string `xorm:"uid"`

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.12.1. DO NOT EDIT.
// Code generated by mockery v2.12.2. DO NOT EDIT.
package dashboards
@ -67,27 +67,6 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models.
return r0, r1
}
// GenerateNewPublicDashboardUid provides a mock function with given fields: ctx
func (_m *FakeDashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) {
ret := _m.Called(ctx)
var r0 string
if rf, ok := ret.Get(0).(func(context.Context) string); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDashboard provides a mock function with given fields: ctx, query
func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) {
ret := _m.Called(ctx, query)
@ -319,61 +298,6 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dash
return r0, r1
}
// GetPublicDashboard provides a mock function with given fields: ctx, accessToken
func (_m *FakeDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) {
ret := _m.Called(ctx, accessToken)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok {
r0 = rf(ctx, accessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 *models.Dashboard
if rf, ok := ret.Get(1).(func(context.Context, string) *models.Dashboard); ok {
r1 = rf(ctx, accessToken)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*models.Dashboard)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, string) error); ok {
r2 = rf(ctx, accessToken)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
func (_m *FakeDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, orgId, dashboardUid)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok {
r0 = rf(ctx, orgId, dashboardUid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgId, dashboardUid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasAdminPermissionInDashboardsOrFolders provides a mock function with given fields: ctx, query
func (_m *FakeDashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error {
ret := _m.Called(ctx, query)
@ -462,29 +386,6 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(cmd models.SaveDashboardC
return r0, r1
}
// SavePublicDashboardConfig provides a mock function with given fields: ctx, cmd
func (_m *FakeDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, cmd)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) *models.PublicDashboard); ok {
r0 = rf(ctx, cmd)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok {
r1 = rf(ctx, cmd)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UnprovisionDashboard provides a mock function with given fields: ctx, id
func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
@ -513,20 +414,6 @@ func (_m *FakeDashboardStore) UpdateDashboardACL(ctx context.Context, uid int64,
return r0
}
// UpdatePublicDashboardConfig provides a mock function with given fields: ctx, cmd
func (_m *FakeDashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error {
ret := _m.Called(ctx, cmd)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok {
r0 = rf(ctx, cmd)
} else {
r0 = ret.Error(0)
}
return r0
}
// ValidateDashboardBeforeSave provides a mock function with given fields: dashboard, overwrite
func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) {
ret := _m.Called(dashboard, overwrite)

View File

@ -0,0 +1,234 @@
package api
import (
"errors"
"net/http"
"strconv"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/publicdashboards"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
type Api struct {
PublicDashboardService publicdashboards.Service
RouteRegister routing.RouteRegister
AccessControl accesscontrol.AccessControl
QueryDataService *query.Service
Features *featuremgmt.FeatureManager
}
func ProvideApi(
pd publicdashboards.Service,
rr routing.RouteRegister,
ac accesscontrol.AccessControl,
qds *query.Service,
features *featuremgmt.FeatureManager,
) *Api {
api := &Api{
PublicDashboardService: pd,
RouteRegister: rr,
AccessControl: ac,
QueryDataService: qds,
Features: features,
}
// attach api if PublicDashboards feature flag is enabled
if features.IsEnabled(featuremgmt.FlagPublicDashboards) {
api.RegisterAPIEndpoints()
}
return api
}
func (api *Api) RegisterAPIEndpoints() {
auth := accesscontrol.Middleware(api.AccessControl)
reqSignedIn := middleware.ReqSignedIn
// Anonymous access to public dashboard route is configured in pkg/api/api.go
// because it is deeply dependent on the HTTPServer.Index() method and would result in a
// circular dependency
api.RouteRegister.Get("/api/public/dashboards/:accessToken", routing.Wrap(api.GetPublicDashboard))
api.RouteRegister.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard))
// Create/Update Public Dashboard
api.RouteRegister.Get("/api/dashboards/uid/:uid/public-config", auth(reqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(api.GetPublicDashboardConfig))
api.RouteRegister.Post("/api/dashboards/uid/:uid/public-config", auth(reqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(api.SavePublicDashboardConfig))
}
// gets public dashboard
func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
accessToken := web.Params(c.Req)[":accessToken"]
dash, err := api.PublicDashboardService.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 (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
pdc, err := api.PublicDashboardService.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 (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Response {
pubdash := &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 := SavePublicDashboardConfigDTO{
OrgId: c.OrgId,
DashboardUid: web.Params(c.Req)[":uid"],
UserId: c.UserId,
PublicDashboard: pubdash,
}
pubdash, err := api.PublicDashboardService.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 (api *Api) 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 := api.PublicDashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"])
if err != nil {
return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err)
}
publicDashboard, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), dashboard.OrgId, dashboard.Uid)
if err != nil {
return response.Error(http.StatusInternalServerError, "could not fetch public dashboard", err)
}
reqDTO, err := api.PublicDashboardService.BuildPublicDashboardMetricRequest(
c.Req.Context(),
dashboard,
publicDashboard,
panelId,
)
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err)
}
anonymousUser, err := api.PublicDashboardService.BuildAnonymousUser(c.Req.Context(), dashboard)
if err != nil {
return response.Error(http.StatusInternalServerError, "could not create anonymous user", err)
}
resp, err := api.QueryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true)
if err != nil {
return handleQueryMetricsError(err)
}
return toJsonStreamingResponse(api.Features, resp)
}
// util to help us unpack dashboard and publicdashboard errors or use default http code and message
// we should look to do some future refactoring of these errors as publicdashboard err is the same as a dashboarderr, just defined in a
// different package.
func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response {
var publicDashboardErr PublicDashboardErr
// handle public dashboard er
if ok := errors.As(err, &publicDashboardErr); ok {
return response.Error(publicDashboardErr.StatusCode, publicDashboardErr.Error(), publicDashboardErr)
}
// handle dashboard errors as well
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)
}
// Copied from pkg/api/metrics.go
func handleQueryMetricsError(err error) *response.NormalResponse {
if errors.Is(err, datasources.ErrDataSourceAccessDenied) {
return response.Error(http.StatusForbidden, "Access denied to data source", err)
}
if errors.Is(err, datasources.ErrDataSourceNotFound) {
return response.Error(http.StatusNotFound, "Data source not found", err)
}
var badQuery *query.ErrBadQuery
if errors.As(err, &badQuery) {
return response.Error(http.StatusBadRequest, util.Capitalize(badQuery.Message), err)
}
if errors.Is(err, backendplugin.ErrPluginNotRegistered) {
return response.Error(http.StatusNotFound, "Plugin not found", err)
}
return response.Error(http.StatusInternalServerError, "Query data error", err)
}
// Copied from pkg/api/metrics.go
func toJsonStreamingResponse(features *featuremgmt.FeatureManager, qdr *backend.QueryDataResponse) response.Response {
statusWhenError := http.StatusBadRequest
if features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) {
statusWhenError = http.StatusMultiStatus
}
statusCode := http.StatusOK
for _, res := range qdr.Responses {
if res.Error != nil {
statusCode = statusWhenError
}
}
return response.JSONStreaming(statusCode, qdr)
}

View File

@ -0,0 +1,589 @@
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"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/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"
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
"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/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/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
func TestAPIGetPublicDashboard(t *testing.T) {
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
cfg := setting.NewCfg()
qs := buildQueryDataService(t, nil, nil, nil)
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(&models.Dashboard{}, nil).Maybe()
testServer := setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(), service, nil)
response := callAPI(testServer, http.MethodGet, "/api/public/dashboards", nil, t)
assert.Equal(t, http.StatusNotFound, response.Code)
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
assert.Equal(t, http.StatusNotFound, response.Code)
// control set. make sure routes are mounted
testServer = setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil)
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
assert.NotEqual(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 no public dashboard",
AccessToken: accessToken,
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardErr: ErrPublicDashboardNotFound,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardErr).Maybe()
testServer := setupTestServer(
t,
setting.NewCfg(),
buildQueryDataService(t, nil, nil, nil),
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
)
response := callAPI(testServer, 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 := &PublicDashboard{IsEnabled: true}
testCases := []struct {
Name string
DashboardUid string
ExpectedHttpResponse int
PublicDashboardResult *PublicDashboard
PublicDashboardErr error
}{
{
Name: "retrieves public dashboard config when dashboard is found",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
},
{
Name: "returns 404 when dashboard not found",
DashboardUid: "77777",
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardErr: dashboards.ErrDashboardNotFound,
},
{
Name: "returns 500 when internal server error",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusInternalServerError,
PublicDashboardResult: nil,
PublicDashboardErr: errors.New("database broken"),
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardErr)
testServer := setupTestServer(
t,
setting.NewCfg(),
buildQueryDataService(t, nil, nil, nil),
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
)
response := callAPI(
testServer,
http.MethodGet,
"/api/dashboards/uid/1/public-config",
nil,
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if response.Code == http.StatusOK {
var pdcResp 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 *PublicDashboard
ExpectedHttpResponse int
SaveDashboardErr error
}{
{
Name: "returns 200 when update persists",
DashboardUid: "1",
publicDashboardConfig: &PublicDashboard{IsEnabled: true},
ExpectedHttpResponse: http.StatusOK,
SaveDashboardErr: nil,
},
{
Name: "returns 500 when not persisted",
ExpectedHttpResponse: http.StatusInternalServerError,
publicDashboardConfig: &PublicDashboard{},
SaveDashboardErr: errors.New("backend failed to save"),
},
{
Name: "returns 404 when dashboard not found",
ExpectedHttpResponse: http.StatusNotFound,
publicDashboardConfig: &PublicDashboard{},
SaveDashboardErr: dashboards.ErrDashboardNotFound,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardConfigDTO")).
Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr)
testServer := setupTestServer(
t,
setting.NewCfg(),
buildQueryDataService(t, nil, nil, nil),
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
)
response := callAPI(
testServer,
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) {
cacheService := &fakeDatasources.FakeCacheService{
DataSources: []*datasources.DataSource{
{Uid: "mysqlds"},
{Uid: "promds"},
{Uid: "promds2"},
},
}
// used to determine whether fakePluginClient returns an error
queryReturnsError := false
fakePluginClient := &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
},
}
qds := buildQueryDataService(t, cacheService, fakePluginClient, nil)
setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) {
service := publicdashboards.NewFakePublicDashboardService(t)
testServer := setupTestServer(
t,
setting.NewCfg(),
qds,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled),
service,
nil,
)
return testServer, service
}
t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) {
server, _ := setup(false)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
require.Equal(t, http.StatusNotFound, resp.Code)
})
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) {
server, _ := setup(true)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/notanumber/query", strings.NewReader("{}"), t)
require.Equal(t, http.StatusBadRequest, resp.Code)
})
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(&PublicDashboard{}, nil)
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, 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)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
require.JSONEq(
t,
`{
"results": {
"A": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": [],
"refId": "A",
"name": "query-A"
}
}
]
}
}
}`,
resp.Body.String(),
)
require.Equal(t, http.StatusOK, resp.Code)
})
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(&PublicDashboard{}, nil)
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, 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)
queryReturnsError = true
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
require.Equal(t, http.StatusInternalServerError, resp.Code)
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(&PublicDashboard{}, nil)
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, 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)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
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"
}
}
]
}
}
}`,
resp.Body.String(),
)
require.Equal(t, http.StatusOK, resp.Code)
})
}
func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) {
db := sqlstore.InitTestDB(t)
cacheService := service.ProvideCacheService(localcache.ProvideService(), db)
qds := buildQueryDataService(t, cacheService, nil, db)
_ = 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",
},
},
},
},
}),
}
// create dashboard
dashboardStore := dashboardStore.ProvideDashboardStore(db)
dashboard, err := dashboardStore.SaveDashboard(saveDashboardCmd)
require.NoError(t, err)
// Create public dashboard
savePubDashboardCmd := &SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
},
}
// create public dashboard
store := publicdashboardsStore.ProvideStore(db)
service := publicdashboardsService.ProvideService(setting.NewCfg(), store)
pubdash, err := service.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd)
require.NoError(t, err)
// setup test server
server := setupTestServer(t,
setting.NewCfg(),
qds,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
db,
)
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": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": []
}
}
]
}
}
}`,
resp.Body.String(),
)
}

View File

@ -0,0 +1,174 @@
package api
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"golang.org/x/oauth2"
"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/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/publicdashboards"
"github.com/grafana/grafana/pkg/services/sqlstore"
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
datasourceService "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"
)
type Server struct {
Mux *web.Mux
RouteRegister routing.RouteRegister
TestServer *httptest.Server
}
func setupTestServer(
t *testing.T,
cfg *setting.Cfg,
qs *query.Service,
features *featuremgmt.FeatureManager,
service publicdashboards.Service,
db *sqlstore.SQLStore,
) *web.Mux {
// build router to register routes
rr := routing.NewRouteRegister()
// build access control - FIXME we should be able to mock this, but to get
// tests going, we're going to instantiate full accesscontrol
//ac := accesscontrolmock.New()
//ac.WithDisabled()
// create a sqlstore for access control.
if db == nil {
db = sqlstore.InitTestDB(t)
}
var err error
ac, err := ossaccesscontrol.ProvideService(features, cfg, database.ProvideService(db), rr)
require.NoError(t, err)
// build mux
m := web.New()
// set initial context
m.Use(func(c *web.Context) {
ctx := &models.ReqContext{
Context: c,
IsSignedIn: true, // FIXME need to be able to change this for tests
SkipCache: true, // hardcoded to make sure query service doesnt hit the cache
Logger: log.New("publicdashboards-test"),
// Set signed in user. We might not actually need to do this.
SignedInUser: &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: "testUser"},
}
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), ctx))
})
// build api, this will mount the routes at the same time if
// featuremgmt.FlagPublicDashboard is enabled
ProvideApi(service, rr, ac, qs, features)
// connect routes to mux
rr.Register(m.Router)
return m
}
func callAPI(server *web.Mux, method, path string, body io.Reader, t *testing.T) *httptest.ResponseRecorder {
req, err := http.NewRequest(method, path, body)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
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 *sqlstore.SQLStore) *query.Service {
// build database if we need one
if store == nil {
store = sqlstore.InitTestDB(t)
}
// default cache service
if cs == nil {
cs = datasourceService.ProvideCacheService(localcache.ProvideService(), store)
}
// 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
},
}
}
return query.ProvideService(
nil,
cs,
nil,
&fakePluginRequestValidator{},
&fakeDatasources.FakeDataSourceService{},
fpc,
&fakeOAuthTokenService{},
)
}
//copied from pkg/api/metrics_test.go
type fakePluginRequestValidator struct {
err error
}
func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error {
return rv.err
}
type fakeOAuthTokenService struct {
passThruEnabled bool
token *oauth2.Token
}
func (ts *fakeOAuthTokenService) GetCurrentOAuthToken(context.Context, *models.SignedInUser) *oauth2.Token {
return ts.token
}
func (ts *fakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource) bool {
return ts.passThruEnabled
}
// 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
}

View File

@ -1,8 +1,6 @@
package middleware
package api
import (
"github.com/grafana/grafana/pkg/models"
)
import "github.com/grafana/grafana/pkg/models"
func SetPublicDashboardFlag() func(c *models.ReqContext) {
return func(c *models.ReqContext) {

View File

@ -3,27 +3,52 @@ package database
import (
"context"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/publicdashboards"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/util"
)
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) {
// Define the storage implementation. We're generating the mock implementation
// automatically
type PublicDashboardStoreImpl struct {
sqlStore *sqlstore.SQLStore
log log.Logger
dialect migrator.Dialect
}
// Gives us a compile time error if our database does not adhere to contract of
// the interface
var _ publicdashboards.Store = (*PublicDashboardStoreImpl)(nil)
// Factory used by wire to dependency injection
func ProvideStore(sqlStore *sqlstore.SQLStore) *PublicDashboardStoreImpl {
return &PublicDashboardStoreImpl{
sqlStore: sqlStore,
log: log.New("publicdashboards.store"),
dialect: sqlStore.Dialect,
}
}
// Retrieves public dashboard configuration
func (d *PublicDashboardStoreImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) {
if accessToken == "" {
return nil, nil, dashboards.ErrPublicDashboardIdentifierNotSet
return nil, nil, ErrPublicDashboardIdentifierNotSet
}
// get public dashboard
pdRes := &models.PublicDashboard{AccessToken: accessToken}
pdRes := &PublicDashboard{AccessToken: accessToken}
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
has, err := sess.Get(pdRes)
if err != nil {
return err
}
if !has {
return dashboards.ErrPublicDashboardNotFound
return ErrPublicDashboardNotFound
}
return nil
})
@ -40,7 +65,7 @@ func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken str
return err
}
if !has {
return dashboards.ErrPublicDashboardNotFound
return ErrPublicDashboardNotFound
}
return nil
})
@ -52,15 +77,15 @@ func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken str
return pdRes, dashRes, err
}
// generates a new unique uid to retrieve a public dashboard
func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) {
// Generates a new unique uid to retrieve a public dashboard
func (d *PublicDashboardStoreImpl) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) {
var uid string
err := d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
for i := 0; i < 3; i++ {
uid = util.GenerateShortUID()
exists, err := sess.Get(&models.PublicDashboard{Uid: uid})
exists, err := sess.Get(&PublicDashboard{Uid: uid})
if err != nil {
return err
}
@ -70,7 +95,7 @@ func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (str
}
}
return dashboards.ErrPublicDashboardFailedGenerateUniqueUid
return ErrPublicDashboardFailedGenerateUniqueUid
})
if err != nil {
@ -80,13 +105,13 @@ func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (str
return uid, nil
}
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
// Retrieves public dashboard configuration
func (d *PublicDashboardStoreImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) {
if dashboardUid == "" {
return nil, dashboards.ErrDashboardIdentifierNotSet
}
pdRes := &models.PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid}
pdRes := &PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid}
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// publicDashboard
_, err := sess.Get(pdRes)
@ -104,8 +129,8 @@ func (d *DashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int
return pdRes, err
}
// persists public dashboard configuration
func (d *DashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) {
// Persists public dashboard configuration
func (d *PublicDashboardStoreImpl) SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) (*PublicDashboard, error) {
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard)
if err != nil {
@ -123,7 +148,7 @@ func (d *DashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd mode
}
// updates existing public dashboard configuration
func (d *DashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error {
func (d *PublicDashboardStoreImpl) UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error {
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
timeSettingsJSON, err := cmd.PublicDashboard.TimeSettings.MarshalJSON()
if err != nil {

View File

@ -10,8 +10,10 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboards "github.com/grafana/grafana/pkg/services/dashboards"
dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
@ -25,19 +27,21 @@ var DefaultTime = time.Now().UTC().Round(time.Second)
// GetPublicDashboard
func TestIntegrationGetPublicDashboard(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var dashboardStore *dashboardsDB.DashboardStore
var publicdashboardStore *PublicDashboardStoreImpl
var savedDashboard *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t)
dashboardStore = ProvideDashboardStore(sqlStore)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("returns PublicDashboard and Dashboard", func(t *testing.T) {
setup()
pubdash, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
PublicDashboard: models.PublicDashboard{
pubdash, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "abc1234",
DashboardUid: savedDashboard.Uid,
@ -50,7 +54,7 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
})
require.NoError(t, err)
pd, d, err := dashboardStore.GetPublicDashboard(context.Background(), "NOTAREALUUID")
pd, d, err := publicdashboardStore.GetPublicDashboard(context.Background(), "NOTAREALUUID")
require.NoError(t, err)
assert.Equal(t, pd, pubdash)
@ -59,22 +63,22 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
t.Run("returns ErrPublicDashboardNotFound with empty uid", func(t *testing.T) {
setup()
_, _, err := dashboardStore.GetPublicDashboard(context.Background(), "")
require.Error(t, dashboards.ErrPublicDashboardIdentifierNotSet, err)
_, _, err := publicdashboardStore.GetPublicDashboard(context.Background(), "")
require.Error(t, ErrPublicDashboardIdentifierNotSet, err)
})
t.Run("returns ErrPublicDashboardNotFound when PublicDashboard not found", func(t *testing.T) {
setup()
_, _, err := dashboardStore.GetPublicDashboard(context.Background(), "zzzzzz")
require.Error(t, dashboards.ErrPublicDashboardNotFound, err)
_, _, err := publicdashboardStore.GetPublicDashboard(context.Background(), "zzzzzz")
require.Error(t, ErrPublicDashboardNotFound, err)
})
t.Run("returns ErrDashboardNotFound when Dashboard not found", func(t *testing.T) {
setup()
_, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
_, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: models.PublicDashboard{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "abc1234",
DashboardUid: "nevergonnafindme",
@ -84,7 +88,7 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
},
})
require.NoError(t, err)
_, _, err = dashboardStore.GetPublicDashboard(context.Background(), "abc1234")
_, _, err = publicdashboardStore.GetPublicDashboard(context.Background(), "abc1234")
require.Error(t, dashboards.ErrDashboardNotFound, err)
})
}
@ -92,35 +96,37 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
// GetPublicDashboardConfig
func TestIntegrationGetPublicDashboardConfig(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var dashboardStore *dashboardsDB.DashboardStore
var publicdashboardStore *PublicDashboardStoreImpl
var savedDashboard *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t)
dashboardStore = ProvideDashboardStore(sqlStore)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("returns isPublic and set dashboardUid and orgId", func(t *testing.T) {
setup()
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, &models.PublicDashboard{IsEnabled: false, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}, pubdash)
assert.Equal(t, &PublicDashboard{IsEnabled: false, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}, pubdash)
})
t.Run("returns dashboard errDashboardIdentifierNotSet", func(t *testing.T) {
setup()
_, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, "")
_, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, "")
require.Error(t, dashboards.ErrDashboardIdentifierNotSet, err)
})
t.Run("returns isPublic along with public dashboard when exists", func(t *testing.T) {
setup()
// insert test public dashboard
resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
resp, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: models.PublicDashboard{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
@ -132,7 +138,7 @@ func TestIntegrationGetPublicDashboardConfig(t *testing.T) {
})
require.NoError(t, err)
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.True(t, assert.ObjectsAreEqualValues(resp, pubdash))
@ -143,23 +149,25 @@ func TestIntegrationGetPublicDashboardConfig(t *testing.T) {
// SavePublicDashboardConfig
func TestIntegrationSavePublicDashboardConfig(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var dashboardStore *dashboardsDB.DashboardStore
var publicdashboardStore *PublicDashboardStoreImpl
var savedDashboard *models.Dashboard
var savedDashboard2 *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore = ProvideDashboardStore(sqlStore)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true)
}
t.Run("saves new public dashboard", func(t *testing.T) {
setup()
resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
resp, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: models.PublicDashboard{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
@ -172,7 +180,7 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) {
})
require.NoError(t, err)
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
//verify saved response and queried response are the same
@ -182,7 +190,7 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) {
assert.True(t, util.IsValidShortUID(pubdash.Uid))
// verify we didn't update all dashboards
pubdash2, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid)
pubdash2, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid)
require.NoError(t, err)
assert.False(t, pubdash2.IsEnabled)
})
@ -190,13 +198,15 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) {
func TestIntegrationUpdatePublicDashboard(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var dashboardStore *dashboardsDB.DashboardStore
var publicdashboardStore *PublicDashboardStoreImpl
var savedDashboard *models.Dashboard
var anotherSavedDashboard *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore = ProvideDashboardStore(sqlStore)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
anotherSavedDashboard = insertTestDashboard(t, dashboardStore, "test another Dashie", 1, 0, true)
}
@ -204,28 +214,11 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
t.Run("updates an existing dashboard", func(t *testing.T) {
setup()
// inserting two different public dashboards to test update works and only affect the desired pd by uid
anotherPdUid := "anotherUid"
_, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: anotherSavedDashboard.Uid,
OrgId: anotherSavedDashboard.OrgId,
PublicDashboard: models.PublicDashboard{
Uid: anotherPdUid,
DashboardUid: anotherSavedDashboard.Uid,
OrgId: anotherSavedDashboard.OrgId,
IsEnabled: true,
CreatedAt: DefaultTime,
CreatedBy: 7,
AccessToken: "fakeaccesstoken",
},
})
require.NoError(t, err)
pdUid := "asdf1234"
_, err = dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
_, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: models.PublicDashboard{
PublicDashboard: PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
@ -237,7 +230,24 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
})
require.NoError(t, err)
updatedPublicDashboard := models.PublicDashboard{
// inserting two different public dashboards to test update works and only affect the desired pd by uid
anotherPdUid := "anotherUid"
_, err = publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
DashboardUid: anotherSavedDashboard.Uid,
OrgId: anotherSavedDashboard.OrgId,
PublicDashboard: PublicDashboard{
Uid: anotherPdUid,
DashboardUid: anotherSavedDashboard.Uid,
OrgId: anotherSavedDashboard.OrgId,
IsEnabled: true,
CreatedAt: DefaultTime,
CreatedBy: 7,
AccessToken: "fakeaccesstoken",
},
})
require.NoError(t, err)
updatedPublicDashboard := PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
@ -247,7 +257,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
UpdatedBy: 8,
}
// update initial record
err = dashboardStore.UpdatePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
err = publicdashboardStore.UpdatePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: updatedPublicDashboard,
@ -255,7 +265,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
require.NoError(t, err)
// updated dashboard should have changed
pdRetrieved, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
pdRetrieved, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, updatedPublicDashboard.UpdatedAt, pdRetrieved.UpdatedAt)
@ -264,10 +274,30 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
assert.Equal(t, updatedPublicDashboard.IsEnabled, pdRetrieved.IsEnabled)
// not updated dashboard shouldn't have changed
pdNotUpdatedRetrieved, err := dashboardStore.GetPublicDashboardConfig(context.Background(), anotherSavedDashboard.OrgId, anotherSavedDashboard.Uid)
pdNotUpdatedRetrieved, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), anotherSavedDashboard.OrgId, anotherSavedDashboard.Uid)
require.NoError(t, err)
assert.NotEqual(t, updatedPublicDashboard.UpdatedAt, pdNotUpdatedRetrieved.UpdatedAt)
assert.NotEqual(t, updatedPublicDashboard.IsEnabled, pdNotUpdatedRetrieved.IsEnabled)
})
}
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
t.Helper()
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: folderId,
IsFolder: isFolder,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
dash, err := dashboardStore.SaveDashboard(cmd)
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.Id)
dash.Data.Set("uid", dash.Uid)
return dash
}

View File

@ -4,6 +4,47 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
)
// PublicDashboardErr represents a dashboard error.
type PublicDashboardErr struct {
StatusCode int
Status string
Reason string
}
// Error returns the error message.
func (e PublicDashboardErr) Error() string {
if e.Reason != "" {
return e.Reason
}
return "Dashboard Error"
}
var (
ErrPublicDashboardFailedGenerateUniqueUid = PublicDashboardErr{
Reason: "Failed to generate unique public dashboard id",
StatusCode: 500,
}
ErrPublicDashboardFailedGenerateAccesstoken = PublicDashboardErr{
Reason: "Failed to public dashboard access token",
StatusCode: 500,
}
ErrPublicDashboardNotFound = PublicDashboardErr{
Reason: "Public dashboard not found",
StatusCode: 404,
Status: "not-found",
}
ErrPublicDashboardPanelNotFound = PublicDashboardErr{
Reason: "Panel not found in dashboard",
StatusCode: 404,
Status: "not-found",
}
ErrPublicDashboardIdentifierNotSet = PublicDashboardErr{
Reason: "No Uid for public dashboard specified",
StatusCode: 400,
}
)
type PublicDashboard struct {
@ -32,7 +73,7 @@ type TimeSettings struct {
// build time settings object from json on public dashboard. If empty, use
// defaults on the dashboard
func (pd PublicDashboard) BuildTimeSettings(dashboard *Dashboard) *TimeSettings {
func (pd PublicDashboard) BuildTimeSettings(dashboard *models.Dashboard) *TimeSettings {
ts := &TimeSettings{
From: dashboard.Data.GetPath("time", "from").MustString(),
To: dashboard.Data.GetPath("time", "to").MustString(),
@ -53,6 +94,16 @@ func (pd PublicDashboard) BuildTimeSettings(dashboard *Dashboard) *TimeSettings
return ts
}
//
// DTO for transforming user input in the api
//
type SavePublicDashboardConfigDTO struct {
DashboardUid string
OrgId int64
UserId int64
PublicDashboard *PublicDashboard
}
//
// COMMANDS
//

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/assert"
)
@ -15,13 +16,13 @@ func TestBuildTimeSettings(t *testing.T) {
var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}})
testCases := []struct {
name string
dashboard *Dashboard
dashboard *models.Dashboard
pubdash *PublicDashboard
timeResult *TimeSettings
}{
{
name: "should use dashboard time if pubdash time empty",
dashboard: &Dashboard{Data: dashboardData},
dashboard: &models.Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{},
timeResult: &TimeSettings{
From: "now-8",
@ -30,7 +31,7 @@ func TestBuildTimeSettings(t *testing.T) {
},
{
name: "should use dashboard time if pubdash to/from empty",
dashboard: &Dashboard{Data: dashboardData},
dashboard: &models.Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{},
timeResult: &TimeSettings{
From: "now-8",
@ -39,7 +40,7 @@ func TestBuildTimeSettings(t *testing.T) {
},
{
name: "should use pubdash time",
dashboard: &Dashboard{Data: dashboardData},
dashboard: &models.Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{TimeSettings: simplejson.NewFromAny(map[string]interface{}{"from": "now-12", "to": "now"})},
timeResult: &TimeSettings{
From: "now-12",

View File

@ -0,0 +1,144 @@
// Code generated by mockery v2.12.2. DO NOT EDIT.
package publicdashboards
import (
context "context"
dtos "github.com/grafana/grafana/pkg/api/dtos"
mock "github.com/stretchr/testify/mock"
models "github.com/grafana/grafana/pkg/models"
publicdashboardsmodels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
testing "testing"
)
// FakePublicDashboardService is an autogenerated mock type for the Service type
type FakePublicDashboardService struct {
mock.Mock
}
// BuildAnonymousUser provides a mock function with given fields: ctx, dashboard
func (_m *FakePublicDashboardService) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) {
ret := _m.Called(ctx, dashboard)
var r0 *models.SignedInUser
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard) *models.SignedInUser); ok {
r0 = rf(ctx, dashboard)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.SignedInUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard) error); ok {
r1 = rf(ctx, dashboard)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId
func (_m *FakePublicDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *publicdashboardsmodels.PublicDashboard, panelId int64) (dtos.MetricRequest, error) {
ret := _m.Called(ctx, dashboard, publicDashboard, panelId)
var r0 dtos.MetricRequest
if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64) dtos.MetricRequest); ok {
r0 = rf(ctx, dashboard, publicDashboard, panelId)
} else {
r0 = ret.Get(0).(dtos.MetricRequest)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64) error); ok {
r1 = rf(ctx, dashboard, publicDashboard, panelId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicDashboard provides a mock function with given fields: ctx, accessToken
func (_m *FakePublicDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) {
ret := _m.Called(ctx, accessToken)
var r0 *models.Dashboard
if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok {
r0 = rf(ctx, accessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Dashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, accessToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
func (_m *FakePublicDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*publicdashboardsmodels.PublicDashboard, error) {
ret := _m.Called(ctx, orgId, dashboardUid)
var r0 *publicdashboardsmodels.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *publicdashboardsmodels.PublicDashboard); ok {
r0 = rf(ctx, orgId, dashboardUid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgId, dashboardUid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SavePublicDashboardConfig provides a mock function with given fields: ctx, dto
func (_m *FakePublicDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *publicdashboardsmodels.SavePublicDashboardConfigDTO) (*publicdashboardsmodels.PublicDashboard, error) {
ret := _m.Called(ctx, dto)
var r0 *publicdashboardsmodels.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, *publicdashboardsmodels.SavePublicDashboardConfigDTO) *publicdashboardsmodels.PublicDashboard); ok {
r0 = rf(ctx, dto)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *publicdashboardsmodels.SavePublicDashboardConfigDTO) error); ok {
r1 = rf(ctx, dto)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewFakePublicDashboardService creates a new instance of FakePublicDashboardService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewFakePublicDashboardService(t testing.TB) *FakePublicDashboardService {
mock := &FakePublicDashboardService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,142 @@
// Code generated by mockery v2.12.2. DO NOT EDIT.
package publicdashboards
import (
context "context"
models "github.com/grafana/grafana/pkg/services/publicdashboards/models"
mock "github.com/stretchr/testify/mock"
pkgmodels "github.com/grafana/grafana/pkg/models"
testing "testing"
)
// FakePublicDashboardStore is an autogenerated mock type for the Store type
type FakePublicDashboardStore struct {
mock.Mock
}
// GenerateNewPublicDashboardUid provides a mock function with given fields: ctx
func (_m *FakePublicDashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) {
ret := _m.Called(ctx)
var r0 string
if rf, ok := ret.Get(0).(func(context.Context) string); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicDashboard provides a mock function with given fields: ctx, accessToken
func (_m *FakePublicDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *pkgmodels.Dashboard, error) {
ret := _m.Called(ctx, accessToken)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok {
r0 = rf(ctx, accessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 *pkgmodels.Dashboard
if rf, ok := ret.Get(1).(func(context.Context, string) *pkgmodels.Dashboard); ok {
r1 = rf(ctx, accessToken)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*pkgmodels.Dashboard)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, string) error); ok {
r2 = rf(ctx, accessToken)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
func (_m *FakePublicDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, orgId, dashboardUid)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok {
r0 = rf(ctx, orgId, dashboardUid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgId, dashboardUid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SavePublicDashboardConfig provides a mock function with given fields: ctx, cmd
func (_m *FakePublicDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, cmd)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) *models.PublicDashboard); ok {
r0 = rf(ctx, cmd)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok {
r1 = rf(ctx, cmd)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdatePublicDashboardConfig provides a mock function with given fields: ctx, cmd
func (_m *FakePublicDashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error {
ret := _m.Called(ctx, cmd)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok {
r0 = rf(ctx, cmd)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewFakePublicDashboardStore(t testing.TB) *FakePublicDashboardStore {
mock := &FakePublicDashboardStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,29 @@
package publicdashboards
import (
"context"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/models"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
)
// These are the api contracts. The API should match the underlying service and store
//go:generate mockery --name Service --structname FakePublicDashboardService --inpackage --filename public_dashboard_service_mock.go
type Service interface {
BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error)
GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error)
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error)
BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error)
}
//go:generate mockery --name Store --structname FakePublicDashboardStore --inpackage --filename public_dashboard_store_mock.go
type Store interface {
GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error)
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
GenerateNewPublicDashboardUid(ctx context.Context) (string, error)
SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) (*PublicDashboard, error)
UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error
}

View File

@ -8,25 +8,54 @@ import (
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"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/services/publicdashboards"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/setting"
)
// Define the Service Implementation. We're generating mock implementation
// automatically
type PublicDashboardServiceImpl struct {
log log.Logger
cfg *setting.Cfg
store publicdashboards.Store
}
// Gives us compile time error if the service does not adhere to the contract of
// the interface
var _ publicdashboards.Service = (*PublicDashboardServiceImpl)(nil)
// Factory for method used by wire to inject dependencies.
// builds the service, and api, and configures routes
func ProvideService(
cfg *setting.Cfg,
store publicdashboards.Store,
) *PublicDashboardServiceImpl {
return &PublicDashboardServiceImpl{
log: log.New("publicdashboards"),
cfg: cfg,
store: store,
}
}
// Gets public dashboard via access token
func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) {
pubdash, d, err := dr.dashboardStore.GetPublicDashboard(ctx, accessToken)
func (pd *PublicDashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) {
pubdash, d, err := pd.store.GetPublicDashboard(ctx, accessToken)
if err != nil {
return nil, err
}
if pubdash == nil || d == nil {
return nil, dashboards.ErrPublicDashboardNotFound
return nil, ErrPublicDashboardNotFound
}
if !pubdash.IsEnabled {
return nil, dashboards.ErrPublicDashboardNotFound
return nil, ErrPublicDashboardNotFound
}
ts := pubdash.BuildTimeSettings(d)
@ -37,8 +66,8 @@ func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessTo
}
// GetPublicDashboardConfig is a helper method to retrieve the public dashboard configuration for a given dashboard from the database
func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
pdc, err := dr.dashboardStore.GetPublicDashboardConfig(ctx, orgId, dashboardUid)
func (pd *PublicDashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) {
pdc, err := pd.store.GetPublicDashboardConfig(ctx, orgId, dashboardUid)
if err != nil {
return nil, err
}
@ -48,7 +77,7 @@ func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, or
// SavePublicDashboardConfig is a helper method to persist the sharing config
// to the database. It handles validations for sharing config and persistence
func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) {
func (pd *PublicDashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) {
if len(dto.DashboardUid) == 0 {
return nil, dashboards.ErrDashboardIdentifierNotSet
}
@ -59,14 +88,14 @@ func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, d
}
if dto.PublicDashboard.Uid == "" {
return dr.savePublicDashboardConfig(ctx, dto)
return pd.savePublicDashboardConfig(ctx, dto)
}
return dr.updatePublicDashboardConfig(ctx, dto)
return pd.updatePublicDashboardConfig(ctx, dto)
}
func (dr *DashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) {
uid, err := dr.dashboardStore.GenerateNewPublicDashboardUid(ctx)
func (pd *PublicDashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) {
uid, err := pd.store.GenerateNewPublicDashboardUid(ctx)
if err != nil {
return nil, err
}
@ -76,10 +105,10 @@ func (dr *DashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, d
return nil, err
}
cmd := models.SavePublicDashboardConfigCommand{
cmd := SavePublicDashboardConfigCommand{
DashboardUid: dto.DashboardUid,
OrgId: dto.OrgId,
PublicDashboard: models.PublicDashboard{
PublicDashboard: PublicDashboard{
Uid: uid,
DashboardUid: dto.DashboardUid,
OrgId: dto.OrgId,
@ -91,12 +120,12 @@ func (dr *DashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, d
},
}
return dr.dashboardStore.SavePublicDashboardConfig(ctx, cmd)
return pd.store.SavePublicDashboardConfig(ctx, cmd)
}
func (dr *DashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) {
cmd := models.SavePublicDashboardConfigCommand{
PublicDashboard: models.PublicDashboard{
func (pd *PublicDashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) {
cmd := SavePublicDashboardConfigCommand{
PublicDashboard: PublicDashboard{
Uid: dto.PublicDashboard.Uid,
IsEnabled: dto.PublicDashboard.IsEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
@ -105,12 +134,12 @@ func (dr *DashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context,
},
}
err := dr.dashboardStore.UpdatePublicDashboardConfig(ctx, cmd)
err := pd.store.UpdatePublicDashboardConfig(ctx, cmd)
if err != nil {
return nil, err
}
publicDashboard, err := dr.dashboardStore.GetPublicDashboardConfig(ctx, dto.OrgId, dto.DashboardUid)
publicDashboard, err := pd.store.GetPublicDashboardConfig(ctx, dto.OrgId, dto.DashboardUid)
if err != nil {
return nil, err
}
@ -120,15 +149,15 @@ func (dr *DashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context,
// BuildPublicDashboardMetricRequest merges public dashboard parameters with
// dashboard and returns a metrics request to be sent to query backend
func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) {
func (pd *PublicDashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error) {
if !publicDashboard.IsEnabled {
return dtos.MetricRequest{}, dashboards.ErrPublicDashboardNotFound
return dtos.MetricRequest{}, ErrPublicDashboardNotFound
}
queriesByPanel := models.GroupQueriesByPanelId(dashboard.Data)
if _, ok := queriesByPanel[panelId]; !ok {
return dtos.MetricRequest{}, dashboards.ErrPublicDashboardPanelNotFound
return dtos.MetricRequest{}, ErrPublicDashboardPanelNotFound
}
ts := publicDashboard.BuildTimeSettings(dashboard)
@ -141,7 +170,7 @@ func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Co
}
// BuildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard
func (dr *DashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) {
func (pd *PublicDashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) {
datasourceUids := models.GetUniqueDashboardDatasourceUids(dashboard.Data)
// Create a temp user with read-only datasource permissions

View File

@ -13,8 +13,10 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database"
. "github.com/grafana/grafana/pkg/services/publicdashboards"
database "github.com/grafana/grafana/pkg/services/publicdashboards/database"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
@ -25,7 +27,7 @@ var mergedDashboardData = simplejson.NewFromAny(map[string]interface{}{"time": m
func TestGetPublicDashboard(t *testing.T) {
type storeResp struct {
pd *models.PublicDashboard
pd *PublicDashboard
d *models.Dashboard
err error
}
@ -41,7 +43,7 @@ func TestGetPublicDashboard(t *testing.T) {
Name: "returns a dashboard",
AccessToken: "abc123",
StoreResp: &storeResp{
pd: &models.PublicDashboard{IsEnabled: true},
pd: &PublicDashboard{IsEnabled: true},
d: &models.Dashboard{Uid: "mydashboard", Data: dashboardData},
err: nil,
},
@ -52,7 +54,7 @@ func TestGetPublicDashboard(t *testing.T) {
Name: "puts pubdash time settings into dashboard",
AccessToken: "abc123",
StoreResp: &storeResp{
pd: &models.PublicDashboard{IsEnabled: true, TimeSettings: timeSettings},
pd: &PublicDashboard{IsEnabled: true, TimeSettings: timeSettings},
d: &models.Dashboard{Data: dashboardData},
err: nil,
},
@ -63,35 +65,35 @@ func TestGetPublicDashboard(t *testing.T) {
Name: "returns ErrPublicDashboardNotFound when isEnabled is false",
AccessToken: "abc123",
StoreResp: &storeResp{
pd: &models.PublicDashboard{IsEnabled: false},
pd: &PublicDashboard{IsEnabled: false},
d: &models.Dashboard{Uid: "mydashboard"},
err: nil,
},
ErrResp: dashboards.ErrPublicDashboardNotFound,
ErrResp: ErrPublicDashboardNotFound,
DashResp: nil,
},
{
Name: "returns ErrPublicDashboardNotFound if PublicDashboard missing",
AccessToken: "abc123",
StoreResp: &storeResp{pd: nil, d: nil, err: nil},
ErrResp: dashboards.ErrPublicDashboardNotFound,
ErrResp: ErrPublicDashboardNotFound,
DashResp: nil,
},
{
Name: "returns ErrPublicDashboardNotFound if Dashboard missing",
AccessToken: "abc123",
StoreResp: &storeResp{pd: nil, d: nil, err: nil},
ErrResp: dashboards.ErrPublicDashboardNotFound,
ErrResp: ErrPublicDashboardNotFound,
DashResp: nil,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
fakeStore := dashboards.FakeDashboardStore{}
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: &fakeStore,
fakeStore := FakePublicDashboardStore{}
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: &fakeStore,
}
fakeStore.On("GetPublicDashboard", mock.Anything, mock.Anything).
@ -116,19 +118,20 @@ func TestGetPublicDashboard(t *testing.T) {
func TestSavePublicDashboard(t *testing.T) {
t.Run("Saving public dashboard", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
}
dto := &dashboards.SavePublicDashboardConfigDTO{
dto := &SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 7,
PublicDashboard: &models.PublicDashboard{
PublicDashboard: &PublicDashboard{
IsEnabled: true,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
@ -159,19 +162,20 @@ func TestSavePublicDashboard(t *testing.T) {
t.Run("Validate pubdash has default time setting value", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
}
dto := &dashboards.SavePublicDashboardConfigDTO{
dto := &SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 7,
PublicDashboard: &models.PublicDashboard{
PublicDashboard: &PublicDashboard{
IsEnabled: true,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
@ -192,19 +196,20 @@ func TestSavePublicDashboard(t *testing.T) {
func TestUpdatePublicDashboard(t *testing.T) {
t.Run("Updating public dashboard", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
}
dto := &dashboards.SavePublicDashboardConfigDTO{
dto := &SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 7,
PublicDashboard: &models.PublicDashboard{
PublicDashboard: &PublicDashboard{
IsEnabled: true,
TimeSettings: timeSettings,
},
@ -217,11 +222,11 @@ func TestUpdatePublicDashboard(t *testing.T) {
require.NoError(t, err)
// attempt to overwrite settings
dto = &dashboards.SavePublicDashboardConfigDTO{
dto = &SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 8,
PublicDashboard: &models.PublicDashboard{
PublicDashboard: &PublicDashboard{
Uid: savedPubdash.Uid,
OrgId: 9,
DashboardUid: "abc1234",
@ -258,19 +263,20 @@ func TestUpdatePublicDashboard(t *testing.T) {
t.Run("Updating set empty time settings", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
}
dto := &dashboards.SavePublicDashboardConfigDTO{
dto := &SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 7,
PublicDashboard: &models.PublicDashboard{
PublicDashboard: &PublicDashboard{
IsEnabled: true,
TimeSettings: timeSettings,
},
@ -285,11 +291,11 @@ func TestUpdatePublicDashboard(t *testing.T) {
require.NoError(t, err)
// attempt to overwrite settings
dto = &dashboards.SavePublicDashboardConfigDTO{
dto = &SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 8,
PublicDashboard: &models.PublicDashboard{
PublicDashboard: &PublicDashboard{
Uid: savedPubdash.Uid,
OrgId: 9,
DashboardUid: "abc1234",
@ -316,11 +322,12 @@ func TestUpdatePublicDashboard(t *testing.T) {
func TestBuildAnonymousUser(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
publicdashboardStore := database.ProvideStore(sqlStore)
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
}
t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) {
@ -336,19 +343,21 @@ func TestBuildAnonymousUser(t *testing.T) {
func TestBuildPublicDashboardMetricRequest(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore)
publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
}
dto := &dashboards.SavePublicDashboardConfigDTO{
dto := &SavePublicDashboardConfigDTO{
DashboardUid: publicDashboard.Uid,
OrgId: publicDashboard.OrgId,
PublicDashboard: &models.PublicDashboard{
PublicDashboard: &PublicDashboard{
IsEnabled: true,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
@ -359,10 +368,10 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
publicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
nonPublicDto := &dashboards.SavePublicDashboardConfigDTO{
nonPublicDto := &SavePublicDashboardConfigDTO{
DashboardUid: nonPublicDashboard.Uid,
OrgId: nonPublicDashboard.OrgId,
PublicDashboard: &models.PublicDashboard{
PublicDashboard: &PublicDashboard{
IsEnabled: false,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
@ -431,7 +440,7 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) {
})
}
func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, title string, orgId int64,
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
t.Helper()
cmd := models.SaveDashboardCommand{