Public Dashboards: Usage Insights (#52768)

This commit is contained in:
owensmallwood 2022-08-10 11:14:48 -06:00 committed by GitHub
parent 5e4d5eb14b
commit dc23643bee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 217 additions and 0 deletions

View File

@ -11,6 +11,7 @@ export interface DashboardInfo {
dashboardUid: string;
dashboardName: string;
folderName?: string;
publicDashboardUid?: string;
}
/**

View File

@ -34,6 +34,7 @@ type DashboardMeta struct {
ProvisionedExternalId string `json:"provisionedExternalId"`
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
PublicDashboardAccessToken string `json:"publicDashboardAccessToken"`
PublicDashboardUID string `json:"publicDashboardUid"`
PublicDashboardEnabled bool `json:"publicDashboardEnabled"`
}
type AnnotationPermission struct {

View File

@ -79,6 +79,11 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err)
}
pubDash, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), dash.OrgId, dash.Uid)
if err != nil {
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err)
}
meta := dtos.DashboardMeta{
Slug: dash.Slug,
Type: models.DashTypeDB,
@ -93,6 +98,7 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
IsFolder: false,
FolderId: dash.FolderId,
PublicDashboardAccessToken: accessToken,
PublicDashboardUID: pubDash.Uid,
}
dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data}

View File

@ -43,6 +43,8 @@ func TestAPIGetPublicDashboard(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(&models.Dashboard{}, nil).Maybe()
service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(&PublicDashboard{}, nil).Maybe()
testServer := setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(), service, nil)
@ -95,6 +97,8 @@ func TestAPIGetPublicDashboard(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardErr).Maybe()
service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(&PublicDashboard{}, nil).Maybe()
testServer := setupTestServer(
t,

View File

@ -1,8 +1,12 @@
package api
import (
"net/http"
"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/web"
)
func SetPublicDashboardFlag() func(c *models.ReqContext) {
@ -11,6 +15,31 @@ func SetPublicDashboardFlag() func(c *models.ReqContext) {
}
}
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)
return
}
// 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)
return
}
if !exists {
c.JsonApiErr(http.StatusBadRequest, "Invalid access token", nil)
return
}
}
}
func CountPublicDashboardRequest() func(c *models.ReqContext) {
return func(c *models.ReqContext) {
metrics.MPublicDashboardRequestCount.Inc()

View File

@ -0,0 +1,75 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
"github.com/grafana/grafana/pkg/services/publicdashboards"
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
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])
return publicdashboardsService.ProvideService(setting.NewCfg(), fakeStore)
}
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))
})
m.Get("/api/public/ma/events/:accessToken", RequiresValidAccessToken(pubdashService))
m.ServeHTTP(recorder, request)
return recorder
}

View File

@ -197,3 +197,21 @@ func (d *PublicDashboardStoreImpl) PublicDashboardEnabled(ctx context.Context, d
return hasPublicDashboard, err
}
func (d *PublicDashboardStoreImpl) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) {
hasPublicDashboard := false
err := d.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
sql := "SELECT COUNT(*) FROM dashboard_public WHERE access_token=? AND is_enabled=true"
result, err := dbSession.SQL(sql, accessToken).Count()
if err != nil {
return err
}
hasPublicDashboard = result > 0
return err
})
return hasPublicDashboard, err
}

View File

@ -64,6 +64,38 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("AccessTokenExists will return true when at least one public dashboard has a matching access token", func(t *testing.T) {
setup()
_, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: PublicDashboard{
IsEnabled: true,
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.True(t, res)
})
t.Run("AccessTokenExists will return false when no public dashboard has matching access token", func(t *testing.T) {
setup()
res, err := publicdashboardStore.AccessTokenExists(context.Background(), "accessToken")
require.NoError(t, err)
require.False(t, res)
})
t.Run("PublicDashboardEnabled Will return true when dashboard has at least one enabled public dashboard", func(t *testing.T) {
setup()

View File

@ -21,6 +21,27 @@ type FakePublicDashboardService struct {
mock.Mock
}
// AccessTokenExists provides a mock function with given fields: ctx, accessToken
func (_m *FakePublicDashboardService) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) {
ret := _m.Called(ctx, accessToken)
var r0 bool
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
r0 = rf(ctx, accessToken)
} else {
r0 = ret.Get(0).(bool)
}
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
}
// BuildAnonymousUser provides a mock function with given fields: ctx, dashboard
func (_m *FakePublicDashboardService) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*user.SignedInUser, error) {
ret := _m.Called(ctx, dashboard)

View File

@ -18,6 +18,27 @@ type FakePublicDashboardStore struct {
mock.Mock
}
// AccessTokenExists provides a mock function with given fields: ctx, accessToken
func (_m *FakePublicDashboardStore) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) {
ret := _m.Called(ctx, accessToken)
var r0 bool
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
r0 = rf(ctx, accessToken)
} else {
r0 = ret.Get(0).(bool)
}
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
}
// GenerateNewPublicDashboardUid provides a mock function with given fields: ctx
func (_m *FakePublicDashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) {
ret := _m.Called(ctx)

View File

@ -20,6 +20,7 @@ type Service interface {
SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error)
BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error)
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
AccessTokenExists(ctx context.Context, accessToken string) (bool, error)
}
//go:generate mockery --name Store --structname FakePublicDashboardStore --inpackage --filename public_dashboard_store_mock.go
@ -31,4 +32,5 @@ type Store interface {
SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) (*PublicDashboard, error)
UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
AccessTokenExists(ctx context.Context, accessToken string) (bool, error)
}

View File

@ -211,6 +211,10 @@ func (pd *PublicDashboardServiceImpl) PublicDashboardEnabled(ctx context.Context
return pd.store.PublicDashboardEnabled(ctx, dashboardUid)
}
func (pd *PublicDashboardServiceImpl) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) {
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()

View File

@ -9,6 +9,7 @@ export function emitDashboardViewEvent(dashboard: DashboardModel) {
dashboardUid: dashboard.uid,
folderName: dashboard.meta.folderTitle,
eventName: MetaAnalyticsEventName.DashboardView,
publicDashboardUid: dashboard.meta.publicDashboardUid,
};
reportMetaAnalytics(eventData);

View File

@ -50,6 +50,7 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
eventData.dashboardName = dashboard.title;
eventData.dashboardUid = dashboard.uid;
eventData.folderName = dashboard.meta.folderTitle;
eventData.publicDashboardUid = dashboard.meta.publicDashboardUid;
}
if (data.series && data.series.length > 0) {

View File

@ -42,6 +42,7 @@ export interface DashboardMeta {
hasUnsavedFolderChange?: boolean;
annotationsPermissions?: AnnotationsPermissions;
publicDashboardAccessToken?: string;
publicDashboardUid?: string;
publicDashboardEnabled?: boolean;
dashboardNotFound?: boolean;
}