From 331110bde571bcb3c30a86ea3525c54ff7ab3963 Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Thu, 6 Oct 2022 12:35:19 -0800 Subject: [PATCH] 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. --- pkg/api/api.go | 7 +- .../publicdashboards/api/middleware.go | 44 ++-- .../publicdashboards/api/middleware_test.go | 244 ++++++++++++------ .../publicdashboards/database/database.go | 25 +- .../database/database_test.go | 122 ++++++++- .../internal/tokens/tokens.go | 23 ++ .../internal/tokens/tokens_test.go | 38 +++ .../public_dashboard_service_mock.go | 21 ++ .../public_dashboard_store_mock.go | 21 ++ .../publicdashboards/publicdashboard.go | 6 +- .../publicdashboards/service/service.go | 14 +- .../{validator.go => validation.go} | 8 +- .../validation/validation_test.go | 8 +- 13 files changed, 460 insertions(+), 121 deletions(-) create mode 100644 pkg/services/publicdashboards/internal/tokens/tokens.go create mode 100644 pkg/services/publicdashboards/internal/tokens/tokens_test.go rename pkg/services/publicdashboards/validation/{validator.go => validation.go} (57%) diff --git a/pkg/api/api.go b/pkg/api/api.go index 0465608b1ab..77729f1324e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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) { diff --git a/pkg/services/publicdashboards/api/middleware.go b/pkg/services/publicdashboards/api/middleware.go index 58fccb6759e..900701ad056 100644 --- a/pkg/services/publicdashboards/api/middleware.go +++ b/pkg/services/publicdashboards/api/middleware.go @@ -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 } } diff --git a/pkg/services/publicdashboards/api/middleware_test.go b/pkg/services/publicdashboards/api/middleware_test.go index d21ca3a798e..0a663fdca0e 100644 --- a/pkg/services/publicdashboards/api/middleware_test.go +++ b/pkg/services/publicdashboards/api/middleware_test.go @@ -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 } diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index 04d92ce3fba..3c4b001343f 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -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 +} diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 0d2e62c4aa8..82200966860 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -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() diff --git a/pkg/services/publicdashboards/internal/tokens/tokens.go b/pkg/services/publicdashboards/internal/tokens/tokens.go new file mode 100644 index 00000000000..e8f1f91c3aa --- /dev/null +++ b/pkg/services/publicdashboards/internal/tokens/tokens.go @@ -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 +} diff --git a/pkg/services/publicdashboards/internal/tokens/tokens_test.go b/pkg/services/publicdashboards/internal/tokens/tokens_test.go new file mode 100644 index 00000000000..fdf0da97a9f --- /dev/null +++ b/pkg/services/publicdashboards/internal/tokens/tokens_test.go @@ -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")) + }) +} diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index 898306bf485..2deb1b68694 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -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) diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index d28c40f4cad..441f696cba3 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -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) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index 32ebd37ddad..ccd235899cc 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -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 diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 848a69d6654..dc4ea02e0bc 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -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 diff --git a/pkg/services/publicdashboards/validation/validator.go b/pkg/services/publicdashboards/validation/validation.go similarity index 57% rename from pkg/services/publicdashboards/validation/validator.go rename to pkg/services/publicdashboards/validation/validation.go index 888c80ffea1..0acfb64d06f 100644 --- a/pkg/services/publicdashboards/validation/validator.go +++ b/pkg/services/publicdashboards/validation/validation.go @@ -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") } diff --git a/pkg/services/publicdashboards/validation/validation_test.go b/pkg/services/publicdashboards/validation/validation_test.go index 9f134765dc9..3b7113aea46 100644 --- a/pkg/services/publicdashboards/validation/validation_test.go +++ b/pkg/services/publicdashboards/validation/validation_test.go @@ -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)