refactor public dashboards middleware testing (#55706)
This PR refactors how we add the orgId to the context on a public dashboard paths. We also split out accessToken handling into its own package and rework status code for "RequiresValidAccessToken". We will be modeling all endpoints to use these status codes going forward. Additionally, it includes a scaffold for better middleware testing and refactors existing tests to table drive tests.
This commit is contained in:
+6
-1
@@ -141,7 +141,12 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
}
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
|
||||
r.Get("/public-dashboards/:accessToken", publicdashboardsapi.SetPublicDashboardFlag(), publicdashboardsapi.CountPublicDashboardRequest(), hs.Index)
|
||||
r.Get("/public-dashboards/:accessToken",
|
||||
publicdashboardsapi.SetPublicDashboardFlag,
|
||||
publicdashboardsapi.SetPublicDashboardOrgIdOnContext(hs.PublicDashboardsApi.PublicDashboardService),
|
||||
publicdashboardsapi.CountPublicDashboardRequest(),
|
||||
hs.Index,
|
||||
)
|
||||
}
|
||||
|
||||
r.Get("/explore", authorize(func(c *models.ReqContext) {
|
||||
|
||||
@@ -2,46 +2,60 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func SetPublicDashboardFlag() func(c *models.ReqContext) {
|
||||
// Adds orgId to context based on org of public dashboard
|
||||
func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) {
|
||||
return func(c *models.ReqContext) {
|
||||
// TODO: Find a better place to set this, or rename this function
|
||||
orgIDValue := c.Req.URL.Query().Get("orgId")
|
||||
orgID, err := strconv.ParseInt(orgIDValue, 10, 64)
|
||||
if err == nil && orgID > 0 && orgID != c.OrgID {
|
||||
c.OrgID = orgID
|
||||
accessToken, ok := web.Params(c.Req)[":accessToken"]
|
||||
if !ok || !tokens.IsValidAccessToken(accessToken) {
|
||||
return
|
||||
}
|
||||
c.IsPublicDashboardView = true
|
||||
|
||||
// Get public dashboard
|
||||
orgId, err := publicDashboardService.GetPublicDashboardOrgId(c.Req.Context(), accessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.OrgID = orgId
|
||||
}
|
||||
}
|
||||
|
||||
// Adds public dashboard flag on context
|
||||
func SetPublicDashboardFlag(c *models.ReqContext) {
|
||||
c.IsPublicDashboardView = true
|
||||
}
|
||||
|
||||
// Middleware to enforce that a public dashboards exists before continuing to
|
||||
// handler
|
||||
func RequiresValidAccessToken(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) {
|
||||
return func(c *models.ReqContext) {
|
||||
accessToken, ok := web.Params(c.Req)[":accessToken"]
|
||||
|
||||
// Check access token is present on the request
|
||||
if !ok || accessToken == "" {
|
||||
c.JsonApiErr(http.StatusBadRequest, "Invalid access token", nil)
|
||||
if !ok {
|
||||
c.JsonApiErr(http.StatusBadRequest, "No access token provided", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !tokens.IsValidAccessToken(accessToken) {
|
||||
c.JsonApiErr(http.StatusBadRequest, "Invalid access token", nil)
|
||||
}
|
||||
|
||||
// Check that the access token references an enabled public dashboard
|
||||
exists, err := publicDashboardService.AccessTokenExists(c.Req.Context(), accessToken)
|
||||
|
||||
if err != nil {
|
||||
c.JsonApiErr(http.StatusInternalServerError, "Error validating access token", nil)
|
||||
c.JsonApiErr(http.StatusInternalServerError, "Failed to query access token", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
c.JsonApiErr(http.StatusBadRequest, "Invalid access token", nil)
|
||||
c.JsonApiErr(http.StatusNotFound, "Public dashboard not found", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +1,187 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var validAccessToken, _ = tokens.GenerateAccessToken()
|
||||
|
||||
func TestRequiresValidAccessToken(t *testing.T) {
|
||||
t.Run("Returns 404 when access token is empty", func(t *testing.T) {
|
||||
request, err := http.NewRequest("GET", "/api/public/ma/events/", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := runMiddleware(request, mockAccessTokenExistsResponse(false, nil))
|
||||
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
})
|
||||
|
||||
t.Run("Returns 200 when public dashboard with access token exists", func(t *testing.T) {
|
||||
request, err := http.NewRequest("GET", "/api/public/ma/events/myAccessToken", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := runMiddleware(request, mockAccessTokenExistsResponse(true, nil))
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
})
|
||||
|
||||
t.Run("Returns 400 when public dashboard with access token does not exist", func(t *testing.T) {
|
||||
request, err := http.NewRequest("GET", "/api/public/ma/events/myAccessToken", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := runMiddleware(request, mockAccessTokenExistsResponse(false, nil))
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
})
|
||||
|
||||
t.Run("Returns 500 when public dashboard service gives an error", func(t *testing.T) {
|
||||
request, err := http.NewRequest("GET", "/api/public/ma/events/myAccessToken", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := runMiddleware(request, mockAccessTokenExistsResponse(false, fmt.Errorf("error not found")))
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, resp.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func mockAccessTokenExistsResponse(returnArguments ...interface{}) *publicdashboardsService.PublicDashboardServiceImpl {
|
||||
fakeStore := &publicdashboards.FakePublicDashboardStore{}
|
||||
fakeStore.On("AccessTokenExists", mock.Anything, mock.Anything).Return(returnArguments[0], returnArguments[1])
|
||||
|
||||
qds := query.ProvideService(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
&fakePluginRequestValidator{},
|
||||
&fakeDatasources.FakeDataSourceService{},
|
||||
&fakePluginClient{
|
||||
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
resp := backend.Responses{
|
||||
"A": backend.DataResponse{
|
||||
Error: fmt.Errorf("query failed"),
|
||||
},
|
||||
}
|
||||
return &backend.QueryDataResponse{Responses: resp}, nil
|
||||
},
|
||||
tests := []struct {
|
||||
Name string
|
||||
Path string
|
||||
AccessTokenExists bool
|
||||
AccessTokenExistsErr error
|
||||
AccessToken string
|
||||
ExpectedResponseCode int
|
||||
}{
|
||||
{
|
||||
Name: "Returns 200 when public dashboard with access token exists",
|
||||
Path: "/api/public/ma/events/myAccesstoken",
|
||||
AccessTokenExists: true,
|
||||
AccessTokenExistsErr: nil,
|
||||
AccessToken: validAccessToken,
|
||||
ExpectedResponseCode: http.StatusOK,
|
||||
},
|
||||
&fakeOAuthTokenService{},
|
||||
)
|
||||
{
|
||||
Name: "Returns 400 when access token is empty",
|
||||
Path: "/api/public/ma/events/",
|
||||
AccessTokenExists: false,
|
||||
AccessTokenExistsErr: nil,
|
||||
AccessToken: "",
|
||||
ExpectedResponseCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
Name: "Returns 400 when invalid access token",
|
||||
Path: "/api/public/ma/events/myAccesstoken",
|
||||
AccessTokenExists: false,
|
||||
AccessTokenExistsErr: nil,
|
||||
AccessToken: "invalidAccessToken",
|
||||
ExpectedResponseCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
Name: "Returns 404 when public dashboard with access token does not exist",
|
||||
Path: "/api/public/ma/events/myAccesstoken",
|
||||
AccessTokenExists: false,
|
||||
AccessTokenExistsErr: nil,
|
||||
AccessToken: validAccessToken,
|
||||
ExpectedResponseCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
Name: "Returns 500 when public dashboard service gives an error",
|
||||
Path: "/api/public/ma/events/myAccesstoken",
|
||||
AccessTokenExists: false,
|
||||
AccessTokenExistsErr: fmt.Errorf("error not found"),
|
||||
AccessToken: validAccessToken,
|
||||
ExpectedResponseCode: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
return publicdashboardsService.ProvideService(setting.NewCfg(), fakeStore, qds)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
publicdashboardService := &publicdashboards.FakePublicDashboardService{}
|
||||
publicdashboardService.On("AccessTokenExists", mock.Anything, mock.Anything).Return(tt.AccessTokenExists, tt.AccessTokenExistsErr)
|
||||
params := map[string]string{":accessToken": tt.AccessToken}
|
||||
mw := RequiresValidAccessToken(publicdashboardService)
|
||||
_, resp := runMw(t, nil, "GET", tt.Path, params, mw)
|
||||
require.Equal(t, tt.ExpectedResponseCode, resp.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runMiddleware(request *http.Request, pubdashService *publicdashboardsService.PublicDashboardServiceImpl) *httptest.ResponseRecorder {
|
||||
recorder := httptest.NewRecorder()
|
||||
m := web.New()
|
||||
initCtx := &models.ReqContext{}
|
||||
m.Use(func(c *web.Context) {
|
||||
initCtx.Context = c
|
||||
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), initCtx))
|
||||
func TestSetPublicDashboardOrgIdOnContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
AccessToken string
|
||||
OrgIdResp int64
|
||||
ErrorResp error
|
||||
ExpectedOrgId int64
|
||||
}{
|
||||
{
|
||||
Name: "Adds orgId for enabled public dashboard",
|
||||
AccessToken: validAccessToken,
|
||||
OrgIdResp: 7,
|
||||
ErrorResp: nil,
|
||||
ExpectedOrgId: 7,
|
||||
},
|
||||
{
|
||||
Name: "Does not set orgId or fail with invalid accessToken",
|
||||
AccessToken: "invalidAccessToken",
|
||||
OrgIdResp: 0,
|
||||
ErrorResp: nil,
|
||||
ExpectedOrgId: 0,
|
||||
},
|
||||
{
|
||||
Name: "Does not set orgId or fail with disabled public dashboard",
|
||||
AccessToken: validAccessToken,
|
||||
OrgIdResp: 0,
|
||||
ErrorResp: nil,
|
||||
ExpectedOrgId: 0,
|
||||
},
|
||||
{
|
||||
Name: "Does not set orgId or fail with error querying public dashboard",
|
||||
AccessToken: validAccessToken,
|
||||
OrgIdResp: 0,
|
||||
ErrorResp: errors.New("database error of some sort"),
|
||||
ExpectedOrgId: 0,
|
||||
},
|
||||
{
|
||||
Name: "Does not set orgId or fail with missing public dashboard",
|
||||
AccessToken: validAccessToken,
|
||||
OrgIdResp: 0,
|
||||
ErrorResp: nil,
|
||||
ExpectedOrgId: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
publicdashboardService := &publicdashboards.FakePublicDashboardService{}
|
||||
publicdashboardService.On("GetPublicDashboardOrgId", mock.Anything, tt.AccessToken).Return(
|
||||
tt.OrgIdResp,
|
||||
tt.ErrorResp,
|
||||
)
|
||||
|
||||
params := map[string]string{":accessToken": tt.AccessToken}
|
||||
mw := SetPublicDashboardOrgIdOnContext(publicdashboardService)
|
||||
ctx, _ := runMw(t, nil, "GET", "/public-dashboard/myaccesstoken", params, mw)
|
||||
assert.Equal(t, tt.ExpectedOrgId, ctx.OrgID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPublicDashboardFlag(t *testing.T) {
|
||||
t.Run("Adds context.IsPublicDashboardView=true to request", func(t *testing.T) {
|
||||
ctx := &models.ReqContext{}
|
||||
SetPublicDashboardFlag(ctx)
|
||||
assert.True(t, ctx.IsPublicDashboardView)
|
||||
})
|
||||
m.Get("/api/public/ma/events/:accessToken", RequiresValidAccessToken(pubdashService), mockValidRequestHandler)
|
||||
m.ServeHTTP(recorder, request)
|
||||
|
||||
return recorder
|
||||
}
|
||||
|
||||
func mockValidRequestHandler(c *models.ReqContext) {
|
||||
resp := make(map[string]interface{})
|
||||
resp["message"] = "Valid request"
|
||||
c.JSON(http.StatusOK, resp)
|
||||
// This is a helper to test middleware. It handles creating a
|
||||
// proper models.ReqContext, setting web parameters, executing middleware, and
|
||||
// returning a response. Response will default to result of
|
||||
// httptest.NewRecorder() return value and will only change if modified by the
|
||||
// middlware as this will no accept a handler method
|
||||
func runMw(t *testing.T, ctx *models.ReqContext, httpmethod string, path string, webparams map[string]string, mw func(c *models.ReqContext)) (*models.ReqContext, *httptest.ResponseRecorder) {
|
||||
// create valid request context and set 0 values if they don't exist
|
||||
if ctx == nil {
|
||||
ctx = &models.ReqContext{}
|
||||
}
|
||||
if ctx.Context == nil {
|
||||
ctx.Context = &web.Context{}
|
||||
}
|
||||
if ctx.SignedInUser == nil {
|
||||
ctx.SignedInUser = &user.SignedInUser{}
|
||||
}
|
||||
|
||||
// create request and add params
|
||||
request, err := http.NewRequest(httpmethod, path, nil)
|
||||
require.NoError(t, err)
|
||||
request = web.SetURLParams(request, webparams)
|
||||
ctx.Req = request
|
||||
|
||||
// setup response recorder to return
|
||||
response := httptest.NewRecorder()
|
||||
ctx.Context.Resp = web.NewResponseWriter("GET", response)
|
||||
|
||||
// run middleware
|
||||
mw(ctx)
|
||||
|
||||
// return result
|
||||
return ctx, response
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ func (d *PublicDashboardStoreImpl) GetDashboard(ctx context.Context, dashboardUi
|
||||
return dashboard, err
|
||||
}
|
||||
|
||||
// Retrieves public dashboard configuration
|
||||
// Retrieves public dashboard. This method makes 2 queries to the db so that in the
|
||||
// even that the underlying dashboard is missing it will return a 404.
|
||||
func (d *PublicDashboardStoreImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) {
|
||||
if accessToken == "" {
|
||||
return nil, nil, ErrPublicDashboardIdentifierNotSet
|
||||
@@ -181,7 +182,7 @@ func (d *PublicDashboardStoreImpl) SavePublicDashboardConfig(ctx context.Context
|
||||
return err
|
||||
}
|
||||
|
||||
// updates existing public dashboard configuration
|
||||
// Updates existing public dashboard configuration
|
||||
func (d *PublicDashboardStoreImpl) UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error {
|
||||
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
timeSettingsJSON, err := json.Marshal(cmd.PublicDashboard.TimeSettings)
|
||||
@@ -206,6 +207,7 @@ func (d *PublicDashboardStoreImpl) UpdatePublicDashboardConfig(ctx context.Conte
|
||||
return err
|
||||
}
|
||||
|
||||
// Responds true if public dashboard for a dashboard exists and isEnabled
|
||||
func (d *PublicDashboardStoreImpl) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) {
|
||||
hasPublicDashboard := false
|
||||
err := d.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
@@ -224,6 +226,8 @@ func (d *PublicDashboardStoreImpl) PublicDashboardEnabled(ctx context.Context, d
|
||||
return hasPublicDashboard, err
|
||||
}
|
||||
|
||||
// Responds true if accessToken exists and isEnabled. May be renamed in the
|
||||
// future
|
||||
func (d *PublicDashboardStoreImpl) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) {
|
||||
hasPublicDashboard := false
|
||||
err := d.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
@@ -241,3 +245,20 @@ func (d *PublicDashboardStoreImpl) AccessTokenExists(ctx context.Context, access
|
||||
|
||||
return hasPublicDashboard, err
|
||||
}
|
||||
|
||||
// Responds with OrgId from if exists and isEnabled.
|
||||
func (d *PublicDashboardStoreImpl) GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error) {
|
||||
var orgId int64
|
||||
err := d.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
sql := "SELECT org_id FROM dashboard_public WHERE access_token=? AND is_enabled=true"
|
||||
|
||||
_, err := dbSession.SQL(sql, accessToken).Get(&orgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return orgId, err
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ func TestIntegrationGetDashboard(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetPublicDashboard
|
||||
func TestIntegrationGetPublicDashboard(t *testing.T) {
|
||||
// AccessTokenExists
|
||||
func TestIntegrationAccessTokenExists(t *testing.T) {
|
||||
var sqlStore *sqlstore.SQLStore
|
||||
var dashboardStore *dashboardsDB.DashboardStore
|
||||
var publicdashboardStore *PublicDashboardStoreImpl
|
||||
@@ -87,6 +87,28 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
|
||||
require.True(t, res)
|
||||
})
|
||||
|
||||
t.Run("AccessTokenExists will return false when IsEnabled=false", func(t *testing.T) {
|
||||
setup()
|
||||
|
||||
err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
|
||||
PublicDashboard: PublicDashboard{
|
||||
IsEnabled: false,
|
||||
Uid: "abc123",
|
||||
DashboardUid: savedDashboard.Uid,
|
||||
OrgId: savedDashboard.OrgId,
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: 7,
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := publicdashboardStore.AccessTokenExists(context.Background(), "accessToken")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.False(t, res)
|
||||
})
|
||||
|
||||
t.Run("AccessTokenExists will return false when no public dashboard has matching access token", func(t *testing.T) {
|
||||
setup()
|
||||
|
||||
@@ -95,6 +117,21 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.False(t, res)
|
||||
})
|
||||
}
|
||||
|
||||
// PublicDashboardEnabled
|
||||
func TestIntegrationPublicDashboardEnabled(t *testing.T) {
|
||||
var sqlStore *sqlstore.SQLStore
|
||||
var dashboardStore *dashboardsDB.DashboardStore
|
||||
var publicdashboardStore *PublicDashboardStoreImpl
|
||||
var savedDashboard *models.Dashboard
|
||||
|
||||
setup := func() {
|
||||
sqlStore = sqlstore.InitTestDB(t)
|
||||
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
|
||||
publicdashboardStore = ProvideStore(sqlStore)
|
||||
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
|
||||
}
|
||||
|
||||
t.Run("PublicDashboardEnabled Will return true when dashboard has at least one enabled public dashboard", func(t *testing.T) {
|
||||
setup()
|
||||
@@ -139,6 +176,21 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
|
||||
|
||||
require.False(t, res)
|
||||
})
|
||||
}
|
||||
|
||||
// GetPublicDashboard
|
||||
func TestIntegrationGetPublicDashboard(t *testing.T) {
|
||||
var sqlStore *sqlstore.SQLStore
|
||||
var dashboardStore *dashboardsDB.DashboardStore
|
||||
var publicdashboardStore *PublicDashboardStoreImpl
|
||||
var savedDashboard *models.Dashboard
|
||||
|
||||
setup := func() {
|
||||
sqlStore = sqlstore.InitTestDB(t)
|
||||
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
|
||||
publicdashboardStore = ProvideStore(sqlStore)
|
||||
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
|
||||
}
|
||||
|
||||
t.Run("returns PublicDashboard and Dashboard", func(t *testing.T) {
|
||||
setup()
|
||||
@@ -310,6 +362,7 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// UpdatePublicDashboardConfig
|
||||
func TestIntegrationUpdatePublicDashboard(t *testing.T) {
|
||||
var sqlStore *sqlstore.SQLStore
|
||||
var dashboardStore *dashboardsDB.DashboardStore
|
||||
@@ -389,6 +442,71 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetPublicDashboardOrgId
|
||||
func TestIntegrationGetPublicDashboardOrgId(t *testing.T) {
|
||||
var sqlStore *sqlstore.SQLStore
|
||||
var dashboardStore *dashboardsDB.DashboardStore
|
||||
var publicdashboardStore *PublicDashboardStoreImpl
|
||||
var savedDashboard *models.Dashboard
|
||||
|
||||
setup := func() {
|
||||
sqlStore = sqlstore.InitTestDB(t)
|
||||
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
|
||||
publicdashboardStore = ProvideStore(sqlStore)
|
||||
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
|
||||
}
|
||||
t.Run("GetPublicDashboardOrgId will OrgId when enabled", func(t *testing.T) {
|
||||
setup()
|
||||
|
||||
err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
|
||||
PublicDashboard: PublicDashboard{
|
||||
IsEnabled: true,
|
||||
Uid: "abc123",
|
||||
DashboardUid: savedDashboard.Uid,
|
||||
OrgId: savedDashboard.OrgId,
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: 7,
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
orgId, err := publicdashboardStore.GetPublicDashboardOrgId(context.Background(), "accessToken")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, savedDashboard.OrgId, orgId)
|
||||
})
|
||||
|
||||
t.Run("GetPublicDashboardOrgId will return 0 when IsEnabled=false", func(t *testing.T) {
|
||||
setup()
|
||||
|
||||
err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
|
||||
PublicDashboard: PublicDashboard{
|
||||
IsEnabled: false,
|
||||
Uid: "abc123",
|
||||
DashboardUid: savedDashboard.Uid,
|
||||
OrgId: savedDashboard.OrgId,
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: 7,
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
orgId, err := publicdashboardStore.GetPublicDashboardOrgId(context.Background(), "accessToken")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, savedDashboard.OrgId, orgId)
|
||||
})
|
||||
|
||||
t.Run("GetPublicDashboardOrgId will return 0 when no public dashboard has matching access token", func(t *testing.T) {
|
||||
setup()
|
||||
|
||||
orgId, err := publicdashboardStore.GetPublicDashboardOrgId(context.Background(), "nonExistentAccessToken")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, savedDashboard.OrgId, orgId)
|
||||
})
|
||||
}
|
||||
|
||||
// helper function insertTestDashboard
|
||||
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
|
||||
folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
|
||||
t.Helper()
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// generates a uuid formatted without dashes to use as access token
|
||||
func GenerateAccessToken() (string, error) {
|
||||
token, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", token[:]), nil
|
||||
}
|
||||
|
||||
// asserts that an accessToken is a valid uuid
|
||||
func IsValidAccessToken(token string) bool {
|
||||
_, err := uuid.Parse(token)
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateAccessToken(t *testing.T) {
|
||||
accessToken, err := GenerateAccessToken()
|
||||
|
||||
t.Run("length", func(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 32, len(accessToken))
|
||||
})
|
||||
|
||||
t.Run("no - ", func(t *testing.T) {
|
||||
assert.False(t, strings.Contains("-", accessToken))
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidAccessToken(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
uuid, _ := GenerateAccessToken()
|
||||
assert.True(t, IsValidAccessToken(uuid))
|
||||
})
|
||||
|
||||
t.Run("false when blank", func(t *testing.T) {
|
||||
assert.False(t, IsValidAccessToken(""))
|
||||
})
|
||||
|
||||
t.Run("false when can't be parsed by uuid lib", func(t *testing.T) {
|
||||
// too long
|
||||
assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789"))
|
||||
})
|
||||
}
|
||||
@@ -168,6 +168,27 @@ func (_m *FakePublicDashboardService) GetPublicDashboardConfig(ctx context.Conte
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetPublicDashboardOrgId provides a mock function with given fields: ctx, accessToken
|
||||
func (_m *FakePublicDashboardService) GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error) {
|
||||
ret := _m.Called(ctx, accessToken)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok {
|
||||
r0 = rf(ctx, accessToken)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetQueryDataResponse provides a mock function with given fields: ctx, skipCache, reqDTO, panelId, accessToken
|
||||
func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO publicdashboardsmodels.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) {
|
||||
ret := _m.Called(ctx, skipCache, reqDTO, panelId, accessToken)
|
||||
|
||||
@@ -161,6 +161,27 @@ func (_m *FakePublicDashboardStore) GetPublicDashboardConfig(ctx context.Context
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetPublicDashboardOrgId provides a mock function with given fields: ctx, accessToken
|
||||
func (_m *FakePublicDashboardStore) GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error) {
|
||||
ret := _m.Called(ctx, accessToken)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok {
|
||||
r0 = rf(ctx, accessToken)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// PublicDashboardEnabled provides a mock function with given fields: ctx, dashboardUid
|
||||
func (_m *FakePublicDashboardStore) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) {
|
||||
ret := _m.Called(ctx, dashboardUid)
|
||||
|
||||
@@ -16,10 +16,11 @@ import (
|
||||
type Service interface {
|
||||
AccessTokenExists(ctx context.Context, accessToken string) (bool, error)
|
||||
BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*user.SignedInUser, error)
|
||||
GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error)
|
||||
GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error)
|
||||
GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error)
|
||||
GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error)
|
||||
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
|
||||
GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error)
|
||||
GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error)
|
||||
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
|
||||
SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error)
|
||||
@@ -28,11 +29,12 @@ type Service interface {
|
||||
//go:generate mockery --name Store --structname FakePublicDashboardStore --inpackage --filename public_dashboard_store_mock.go
|
||||
type Store interface {
|
||||
AccessTokenExists(ctx context.Context, accessToken string) (bool, error)
|
||||
GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error)
|
||||
GenerateNewPublicDashboardUid(ctx context.Context) (string, error)
|
||||
GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error)
|
||||
GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error)
|
||||
GetPublicDashboardByUid(ctx context.Context, uid string) (*PublicDashboard, error)
|
||||
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
|
||||
GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error)
|
||||
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
|
||||
SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error
|
||||
UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/queries"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
||||
@@ -147,7 +147,7 @@ func (pd *PublicDashboardServiceImpl) savePublicDashboardConfig(ctx context.Cont
|
||||
return "", err
|
||||
}
|
||||
|
||||
accessToken, err := GenerateAccessToken()
|
||||
accessToken, err := tokens.GenerateAccessToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -292,14 +292,8 @@ func (pd *PublicDashboardServiceImpl) AccessTokenExists(ctx context.Context, acc
|
||||
return pd.store.AccessTokenExists(ctx, accessToken)
|
||||
}
|
||||
|
||||
// generates a uuid formatted without dashes to use as access token
|
||||
func GenerateAccessToken() (string, error) {
|
||||
token, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", token[:]), nil
|
||||
func (pd *PublicDashboardServiceImpl) GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error) {
|
||||
return pd.store.GetPublicDashboardOrgId(ctx, accessToken)
|
||||
}
|
||||
|
||||
// intervalMS and maxQueryData values are being calculated on the frontend for regular dashboards
|
||||
|
||||
+4
-4
@@ -4,12 +4,12 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
publicDashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
)
|
||||
|
||||
func ValidateSavePublicDashboard(dto *publicDashboardModels.SavePublicDashboardConfigDTO, dashboard *models.Dashboard) error {
|
||||
func ValidateSavePublicDashboard(dto *SavePublicDashboardConfigDTO, dashboard *models.Dashboard) error {
|
||||
if hasTemplateVariables(dashboard) {
|
||||
return publicDashboardModels.ErrPublicDashboardHasTemplateVariables
|
||||
return ErrPublicDashboardHasTemplateVariables
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -21,7 +21,7 @@ func hasTemplateVariables(dashboard *models.Dashboard) bool {
|
||||
return len(templateVariables) > 0
|
||||
}
|
||||
|
||||
func ValidateQueryPublicDashboardRequest(req publicDashboardModels.PublicDashboardQueryDTO) error {
|
||||
func ValidateQueryPublicDashboardRequest(req PublicDashboardQueryDTO) error {
|
||||
if req.IntervalMs < 0 {
|
||||
return fmt.Errorf("intervalMS should be greater than 0")
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -22,10 +22,10 @@ func TestValidateSavePublicDashboard(t *testing.T) {
|
||||
}`)
|
||||
dashboardData, _ := simplejson.NewJson(templateVars)
|
||||
dashboard := models.NewDashboardFromJson(dashboardData)
|
||||
dto := &publicdashboardModels.SavePublicDashboardConfigDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil}
|
||||
dto := &SavePublicDashboardConfigDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil}
|
||||
|
||||
err := ValidateSavePublicDashboard(dto, dashboard)
|
||||
require.ErrorContains(t, err, publicdashboardModels.ErrPublicDashboardHasTemplateVariables.Reason)
|
||||
require.ErrorContains(t, err, ErrPublicDashboardHasTemplateVariables.Reason)
|
||||
})
|
||||
|
||||
t.Run("Returns no validation error when dashboard has no template variables", func(t *testing.T) {
|
||||
@@ -36,7 +36,7 @@ func TestValidateSavePublicDashboard(t *testing.T) {
|
||||
}`)
|
||||
dashboardData, _ := simplejson.NewJson(templateVars)
|
||||
dashboard := models.NewDashboardFromJson(dashboardData)
|
||||
dto := &publicdashboardModels.SavePublicDashboardConfigDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil}
|
||||
dto := &SavePublicDashboardConfigDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil}
|
||||
|
||||
err := ValidateSavePublicDashboard(dto, dashboard)
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user