public dashboards: finalize db schema & v1 feature complete (#50467)

This PR completes public dashboards v1 functionality and simplifies public dashboard conventions. It exists as a large PR so that we are not making constant changes to the database schema.

models.PublicDashboardConfig model replaced with models.PublicDashboard directly
dashboard_public_config table renamed to dashboard_public
models.Dashboard.IsPublic removed from the dashboard and replaced with models.PublicDashboard.isEnabled
Routing now uses a uuid v4 as an access token for viewing a public dashboard anonymously, PublicDashboard.Uid only used as database identifier
Frontend utilizes uuid for auth'd operations and access token for anonymous access
Default to time range defined on dashboard when viewing public dashboard
Add audit fields to public dashboard

Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com>, Ezequiel Victorero <ezequiel.victorero@grafana.com>, Jesse Weaver <jesse.weaver@grafana.com>
This commit is contained in:
Jeff Levin
2022-06-22 13:58:52 -08:00
committed by GitHub
co-authored by Owen Smallwood <owen.smallwood@grafana.com>, Ezequiel Victorero <ezequiel.victorero@grafana.com>, Jesse Weaver
parent 773c269084
commit d076bedb5e
37 changed files with 1308 additions and 627 deletions
@@ -2,7 +2,6 @@ package database
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -10,14 +9,14 @@ import (
)
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) {
if uid == "" {
func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) {
if accessToken == "" {
return nil, nil, models.ErrPublicDashboardIdentifierNotSet
}
// get public dashboard
pdRes := &models.PublicDashboard{Uid: uid}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
pdRes := &models.PublicDashboard{AccessToken: accessToken}
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
has, err := sess.Get(pdRes)
if err != nil {
return err
@@ -34,7 +33,7 @@ func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard
// find dashboard
dashRes := &models.Dashboard{OrgId: pdRes.OrgId, Uid: pdRes.DashboardUid}
err = d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
err = d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
has, err := sess.Get(dashRes)
if err != nil {
return err
@@ -53,44 +52,43 @@ func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard
}
// generates a new unique uid to retrieve a public dashboard
func generateNewPublicDashboardUid(sess *sqlstore.DBSession) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) {
var uid string
exists, err := sess.Get(&models.PublicDashboard{Uid: uid})
if err != nil {
return "", err
err := d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
for i := 0; i < 3; i++ {
uid = util.GenerateShortUID()
exists, err := sess.Get(&models.PublicDashboard{Uid: uid})
if err != nil {
return err
}
if !exists {
return nil
}
}
if !exists {
return uid, nil
}
return models.ErrPublicDashboardFailedGenerateUniqueUid
})
if err != nil {
return "", err
}
return "", models.ErrPublicDashboardFailedGenerateUniqueUid
return uid, nil
}
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
func (d *DashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) {
if dashboardUid == "" {
return nil, models.ErrDashboardIdentifierNotSet
}
// get dashboard and publicDashboard
dashRes := &models.Dashboard{OrgId: orgId, Uid: dashboardUid}
pdRes := &models.PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
// dashboard
has, err := sess.Get(dashRes)
if err != nil {
return err
}
if !has {
return models.ErrDashboardNotFound
}
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// publicDashboard
_, err = sess.Get(pdRes)
_, err := sess.Get(pdRes)
if err != nil {
return err
}
@@ -102,46 +100,13 @@ func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid stri
return nil, err
}
pdc := &models.PublicDashboardConfig{
IsPublic: dashRes.IsPublic,
PublicDashboard: *pdRes,
}
return pdc, err
return pdRes, err
}
// persists public dashboard configuration
func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) {
if len(cmd.PublicDashboardConfig.PublicDashboard.DashboardUid) == 0 {
return nil, models.ErrDashboardIdentifierNotSet
}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
// update isPublic on dashboard entry
affectedRowCount, err := sess.Table("dashboard").Where("org_id = ? AND uid = ?", cmd.OrgId, cmd.DashboardUid).Update(map[string]interface{}{"is_public": cmd.PublicDashboardConfig.IsPublic})
if err != nil {
return err
}
if affectedRowCount == 0 {
return models.ErrDashboardNotFound
}
// update dashboard_public_config
// if we have a uid, public dashboard config exists. delete it otherwise generate a uid
if cmd.PublicDashboardConfig.PublicDashboard.Uid != "" {
if _, err = sess.Exec("DELETE FROM dashboard_public_config WHERE uid=?", cmd.PublicDashboardConfig.PublicDashboard.Uid); err != nil {
return err
}
} else {
uid, err := generateNewPublicDashboardUid(sess)
if err != nil {
return fmt.Errorf("failed to generate UID for public dashboard: %w", err)
}
cmd.PublicDashboardConfig.PublicDashboard.Uid = uid
}
_, err = sess.Insert(&cmd.PublicDashboardConfig.PublicDashboard)
func (d *DashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) {
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard)
if err != nil {
return err
}
@@ -153,5 +118,19 @@ func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboar
return nil, err
}
return &cmd.PublicDashboardConfig, nil
return &cmd.PublicDashboard, nil
}
// updates existing public dashboard configuration
func (d *DashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error {
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.UseBool("is_enabled").Update(&cmd.PublicDashboard)
if err != nil {
return err
}
return nil
})
return err
}
@@ -1,8 +1,11 @@
package database
import (
"context"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -11,6 +14,12 @@ import (
"github.com/stretchr/testify/require"
)
// This is what the db sets empty time settings to
var DefaultTimeSettings, _ = simplejson.NewJson([]byte(`{}`))
// Default time to pass in with seconds rounded
var DefaultTime = time.Now().UTC().Round(time.Second)
// GetPublicDashboard
func TestIntegrationGetPublicDashboard(t *testing.T) {
var sqlStore *sqlstore.SQLStore
@@ -25,54 +34,55 @@ func TestIntegrationGetPublicDashboard(t *testing.T) {
t.Run("returns PublicDashboard and Dashboard", func(t *testing.T) {
setup()
pdc, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "abc1234",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
pubdash, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
PublicDashboard: models.PublicDashboard{
IsEnabled: true,
Uid: "abc1234",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: DefaultTimeSettings,
CreatedAt: DefaultTime,
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
require.NoError(t, err)
pd, d, err := dashboardStore.GetPublicDashboard("abc1234")
pd, d, err := dashboardStore.GetPublicDashboard(context.Background(), "NOTAREALUUID")
require.NoError(t, err)
assert.Equal(t, pd, &pdc.PublicDashboard)
assert.Equal(t, d.Uid, pdc.PublicDashboard.DashboardUid)
assert.Equal(t, pd, pubdash)
assert.Equal(t, d.Uid, pubdash.DashboardUid)
})
t.Run("returns ErrPublicDashboardNotFound with empty uid", func(t *testing.T) {
setup()
_, _, err := dashboardStore.GetPublicDashboard("")
_, _, err := dashboardStore.GetPublicDashboard(context.Background(), "")
require.Error(t, models.ErrPublicDashboardIdentifierNotSet, err)
})
t.Run("returns ErrPublicDashboardNotFound when PublicDashboard not found", func(t *testing.T) {
setup()
_, _, err := dashboardStore.GetPublicDashboard("zzzzzz")
_, _, err := dashboardStore.GetPublicDashboard(context.Background(), "zzzzzz")
require.Error(t, models.ErrPublicDashboardNotFound, err)
})
t.Run("returns ErrDashboardNotFound when Dashboard not found", func(t *testing.T) {
setup()
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
_, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "abc1234",
DashboardUid: "nevergonnafindme",
OrgId: savedDashboard.OrgId,
},
PublicDashboard: models.PublicDashboard{
IsEnabled: true,
Uid: "abc1234",
DashboardUid: "nevergonnafindme",
OrgId: savedDashboard.OrgId,
CreatedAt: DefaultTime,
CreatedBy: 7,
},
})
require.NoError(t, err)
_, _, err = dashboardStore.GetPublicDashboard("abc1234")
_, _, err = dashboardStore.GetPublicDashboard(context.Background(), "abc1234")
require.Error(t, models.ErrDashboardNotFound, err)
})
}
@@ -91,38 +101,40 @@ func TestIntegrationGetPublicDashboardConfig(t *testing.T) {
t.Run("returns isPublic and set dashboardUid and orgId", func(t *testing.T) {
setup()
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, &models.PublicDashboardConfig{IsPublic: false, PublicDashboard: models.PublicDashboard{DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}}, pdc)
assert.Equal(t, &models.PublicDashboard{IsEnabled: false, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}, pubdash)
})
t.Run("returns dashboard errDashboardIdentifierNotSet", func(t *testing.T) {
setup()
_, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, "")
_, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, "")
require.Error(t, models.ErrDashboardIdentifierNotSet, err)
})
t.Run("returns isPublic along with public dashboard when exists", func(t *testing.T) {
setup()
// insert test public dashboard
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: "{from: now, to: then}",
},
PublicDashboard: models.PublicDashboard{
IsEnabled: true,
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: DefaultTimeSettings,
CreatedAt: DefaultTime,
CreatedBy: 7,
},
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, resp, pdc)
assert.True(t, assert.ObjectsAreEqualValues(resp, pubdash))
assert.True(t, assert.ObjectsAreEqual(resp, pubdash))
})
}
@@ -142,89 +154,92 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) {
t.Run("saves new public dashboard", func(t *testing.T) {
setup()
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
PublicDashboard: models.PublicDashboard{
IsEnabled: true,
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: DefaultTimeSettings,
CreatedAt: DefaultTime,
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
//verify saved response and queried response are the same
assert.Equal(t, resp, pdc)
assert.Equal(t, resp, pubdash)
// verify we have a valid uid
assert.True(t, util.IsValidShortUID(pdc.PublicDashboard.Uid))
assert.True(t, util.IsValidShortUID(pubdash.Uid))
// verify we didn't update all dashboards
pdc2, err := dashboardStore.GetPublicDashboardConfig(savedDashboard2.OrgId, savedDashboard2.Uid)
pubdash2, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid)
require.NoError(t, err)
assert.False(t, pdc2.IsPublic)
assert.False(t, pubdash2.IsEnabled)
})
}
t.Run("returns ErrDashboardIdentifierNotSet", func(t *testing.T) {
setup()
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
DashboardUid: "",
OrgId: savedDashboard.OrgId,
},
},
})
require.Error(t, models.ErrDashboardIdentifierNotSet, err)
})
func TestIntegrationUpdatePublicDashboard(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var savedDashboard *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore = ProvideDashboardStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("overwrites existing public dashboard", func(t *testing.T) {
t.Run("updates an existing dashboard", func(t *testing.T) {
setup()
pdUid := util.GenerateShortUID()
pdUid := "asdf1234"
// insert initial record
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
_, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
PublicDashboard: models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
IsEnabled: true,
CreatedAt: DefaultTime,
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
require.NoError(t, err)
// update initial record
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
updatedPublicDashboard := models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: false,
PublicDashboard: models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: "{}",
},
},
IsEnabled: false,
TimeSettings: simplejson.NewFromAny(map[string]interface{}{"from": "now-8", "to": "now"}),
UpdatedAt: time.Now().UTC().Round(time.Second),
UpdatedBy: 8,
}
// update initial record
err = dashboardStore.UpdatePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboard: updatedPublicDashboard,
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
pdRetrieved, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, resp, pdc)
assert.Equal(t, updatedPublicDashboard.UpdatedAt, pdRetrieved.UpdatedAt)
// make sure we're correctly updated IsEnabled because we have to call
// UseBool with xorm
assert.Equal(t, updatedPublicDashboard.IsEnabled, pdRetrieved.IsEnabled)
})
}