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)