Public Dashboards: Add audit table (#54508)

This PR adds an audit table for public dashboards allowing a user to view all public dashboards on an instance of grafana. The public dashboards team is working on a proposal for adding RBAC support to the audit table for 9.3

Co-authored-by: juanicabanas <juan.cabanas@grafana.com>
This commit is contained in:
Jeff Levin
2022-10-12 21:36:05 -08:00
committed by GitHub
co-authored by juanicabanas
parent c7c640d903
commit cc27214dca
20 changed files with 516 additions and 44 deletions
@@ -381,6 +381,15 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
Url: s.cfg.AppSubURL + "/library-panels",
Icon: "library-panel",
})
if s.features.IsEnabled(featuremgmt.FlagPublicDashboards) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Public dashboards",
Id: "dashboards/public",
Url: s.cfg.AppSubURL + "/dashboard/public",
Icon: "library-panel",
})
}
}
if s.features.IsEnabled(featuremgmt.FlagScenes) {
+13 -1
View File
@@ -63,9 +63,11 @@ func (api *Api) RegisterAPIEndpoints() {
api.RouteRegister.Get("/api/public/dashboards/:accessToken", routing.Wrap(api.GetPublicDashboard))
api.RouteRegister.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard))
// List Public Dashboards
api.RouteRegister.Get("/api/dashboards/public", middleware.ReqSignedIn, routing.Wrap(api.ListPublicDashboards))
// Create/Update Public Dashboard
uidScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
api.RouteRegister.Get("/api/dashboards/uid/:uid/public-config",
auth(middleware.ReqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, uidScope)),
routing.Wrap(api.GetPublicDashboardConfig))
@@ -111,6 +113,16 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
return response.JSON(http.StatusOK, dto)
}
// Gets list of public dashboards for an org
// GET /api/dashboards/public
func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
resp, err := api.PublicDashboardService.ListPublicDashboards(c.Req.Context(), c.OrgID)
if err != nil {
return api.handleError(http.StatusInternalServerError, "failed to list public dashboards", err)
}
return response.JSON(http.StatusOK, resp)
}
// Gets public dashboard configuration for dashboard
// GET /api/dashboards/uid/:uid/public-config
func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
+123 -32
View File
@@ -46,30 +46,129 @@ var userViewer = &user.SignedInUser{UserID: 3, OrgID: 1, OrgRole: org.RoleViewer
var userViewerRBAC = &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleViewer, Login: "testViewerUserRBAC", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}}}}
var anonymousUser *user.SignedInUser
type JsonErrResponse struct {
Error string `json:"error"`
}
func TestAPIFeatureFlag(t *testing.T) {
testCases := []struct {
Name string
Method string
Path string
}{
{
Name: "API: Load Dashboard",
Method: http.MethodGet,
Path: "/api/public/dashboards/acbc123",
},
{
Name: "API: Query Dashboard",
Method: http.MethodGet,
Path: "/api/public/dashboards/abc123/panels/2/query",
},
{
Name: "API: List Dashboards",
Method: http.MethodGet,
Path: "/api/dashboards/public",
},
{
Name: "API: Get Public Dashboard Config",
Method: http.MethodPost,
Path: "/api/dashboards/uid/abc123/public-config",
},
{
Name: "API: Upate Public Dashboard",
Method: http.MethodPost,
Path: "/api/dashboards/uid/abc123/public-config",
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = false
service := publicdashboards.NewFakePublicDashboardService(t)
features := featuremgmt.WithFeatures()
testServer := setupTestServer(t, cfg, features, service, nil, userAdmin)
response := callAPI(testServer, test.Method, test.Path, nil, t)
assert.Equal(t, http.StatusNotFound, response.Code)
})
}
}
func TestAPIListPublicDashboard(t *testing.T) {
successResp := []PublicDashboardListResponse{
{
Uid: "1234asdfasdf",
AccessToken: "asdfasdf",
DashboardUid: "abc1234",
IsEnabled: true,
},
}
testCases := []struct {
Name string
User *user.SignedInUser
Response []PublicDashboardListResponse
ResponseErr error
ExpectedHttpResponse int
}{
{
Name: "Anonymous user cannot list dashboards",
User: anonymousUser,
Response: successResp,
ResponseErr: nil,
ExpectedHttpResponse: http.StatusUnauthorized,
},
{
Name: "User viewer can see public dashboards",
User: userViewer,
Response: successResp,
ResponseErr: nil,
ExpectedHttpResponse: http.StatusOK,
},
{
Name: "Handles Service error",
User: userViewer,
Response: nil,
ResponseErr: errors.New("error, service broken"),
ExpectedHttpResponse: http.StatusInternalServerError,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("ListPublicDashboards", mock.Anything, mock.Anything).
Return(test.Response, test.ResponseErr).Maybe()
cfg := setting.NewCfg()
cfg.RBACEnabled = false
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
testServer := setupTestServer(t, cfg, features, service, nil, test.User)
response := callAPI(testServer, http.MethodGet, "/api/dashboards/public", nil, t)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if test.ExpectedHttpResponse == http.StatusOK {
var jsonResp []PublicDashboardListResponse
err := json.Unmarshal(response.Body.Bytes(), &jsonResp)
require.NoError(t, err)
assert.Equal(t, jsonResp[0].Uid, "1234asdfasdf")
}
if test.ResponseErr != nil {
var errResp JsonErrResponse
err := json.Unmarshal(response.Body.Bytes(), &errResp)
require.NoError(t, err)
assert.Equal(t, "error, service broken", errResp.Error)
service.AssertNotCalled(t, "ListPublicDashboards")
}
})
}
}
func TestAPIGetPublicDashboard(t *testing.T) {
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = false
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(&PublicDashboard{}, &models.Dashboard{}, nil).Maybe()
service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(&PublicDashboard{}, nil).Maybe()
testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(), service, nil, anonymousUser)
response := callAPI(testServer, http.MethodGet, "/api/public/dashboards", nil, t)
assert.Equal(t, http.StatusNotFound, response.Code)
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
assert.Equal(t, http.StatusNotFound, response.Code)
// control set. make sure routes are mounted
testServer = setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, userAdmin)
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
assert.NotEqual(t, http.StatusNotFound, response.Code)
})
DashboardUid := "dashboard-abcd1234"
token, err := uuid.NewRandom()
require.NoError(t, err)
@@ -138,9 +237,7 @@ func TestAPIGetPublicDashboard(t *testing.T) {
assert.Equal(t, false, dashResp.Meta.CanDelete)
assert.Equal(t, false, dashResp.Meta.CanSave)
} else {
var errResp struct {
Error string `json:"error"`
}
var errResp JsonErrResponse
err := json.Unmarshal(response.Body.Bytes(), &errResp)
require.NoError(t, err)
assert.Equal(t, test.Err.Error(), errResp.Error)
@@ -435,12 +532,6 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
return testServer, service
}
t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) {
server, _ := setup(false)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
require.Equal(t, http.StatusNotFound, resp.Code)
})
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) {
server, _ := setup(true)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/notanumber/query", strings.NewReader("{}"), t)
@@ -37,6 +37,28 @@ func ProvideStore(sqlStore *sqlstore.SQLStore) *PublicDashboardStoreImpl {
}
}
// Gets list of public dashboards by orgId
func (d *PublicDashboardStoreImpl) ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) {
resp := make([]PublicDashboardListResponse, 0)
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
sess.Table("dashboard_public").
Join("LEFT", "dashboard", "dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id").
Cols("dashboard_public.uid", "dashboard_public.access_token", "dashboard_public.dashboard_uid", "dashboard_public.is_enabled", "dashboard.title").
Where("dashboard_public.org_id = ?", orgId).
OrderBy("is_enabled DESC, dashboard.title ASC")
err := sess.Find(&resp)
return err
})
if err != nil {
return nil, err
}
return resp, nil
}
func (d *PublicDashboardStoreImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
dashboard := &models.Dashboard{Uid: dashboardUid}
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
@@ -29,6 +30,39 @@ func TestLogPrefix(t *testing.T) {
assert.Equal(t, LogPrefix, "publicdashboards.store")
}
func TestIntegrationListPublicDashboard(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
publicdashboardStore := ProvideStore(sqlStore)
var orgId int64 = 1
aDash := insertTestDashboard(t, dashboardStore, "a", orgId, 0, true)
bDash := insertTestDashboard(t, dashboardStore, "b", orgId, 0, true)
cDash := insertTestDashboard(t, dashboardStore, "c", orgId, 0, true)
// these are in order of how they should be returned from ListPUblicDashboards
a := insertPublicDashboard(t, publicdashboardStore, bDash.Uid, orgId, true)
b := insertPublicDashboard(t, publicdashboardStore, cDash.Uid, orgId, true)
c := insertPublicDashboard(t, publicdashboardStore, aDash.Uid, orgId, false)
// this is case that can happen as of now, however, postgres and mysql sort
// null in the exact opposite fashion and there is no shared syntax to sort
// nulls in the same way in all 3 db's.
//d := insertPublicDashboard(t, publicdashboardStore, "missing", orgId, false)
// should not be included in response
_ = insertPublicDashboard(t, publicdashboardStore, "wrongOrgId", 777, false)
resp, err := publicdashboardStore.ListPublicDashboards(context.Background(), orgId)
require.NoError(t, err)
assert.Len(t, resp, 3)
assert.Equal(t, resp[0].Uid, a.Uid)
assert.Equal(t, resp[1].Uid, b.Uid)
assert.Equal(t, resp[2].Uid, c.Uid)
}
func TestIntegrationGetDashboard(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *dashboardsDB.DashboardStore
@@ -506,7 +540,7 @@ func TestIntegrationGetPublicDashboardOrgId(t *testing.T) {
})
}
// helper function insertTestDashboard
// helper function to insert a dashboard
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
t.Helper()
@@ -527,3 +561,35 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto
dash.Data.Set("uid", dash.Uid)
return dash
}
// helper function to insert a public dashboard
func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardStoreImpl, dashboardUid string, orgId int64, isEnabled bool) *PublicDashboard {
ctx := context.Background()
uid, err := publicdashboardStore.GenerateNewPublicDashboardUid(ctx)
require.NoError(t, err)
accessToken, err := tokens.GenerateAccessToken()
require.NoError(t, err)
cmd := SavePublicDashboardConfigCommand{
PublicDashboard: PublicDashboard{
Uid: uid,
DashboardUid: dashboardUid,
OrgId: orgId,
IsEnabled: isEnabled,
TimeSettings: &TimeSettings{},
CreatedBy: 1,
CreatedAt: time.Now(),
AccessToken: accessToken,
},
}
err = publicdashboardStore.SavePublicDashboardConfig(ctx, cmd)
require.NoError(t, err)
pubdash, err := publicdashboardStore.GetPublicDashboardByUid(ctx, uid)
require.NoError(t, err)
return pubdash
}
@@ -81,6 +81,14 @@ func (pd PublicDashboard) TableName() string {
return "dashboard_public"
}
type PublicDashboardListResponse struct {
Uid string `json:"uid" xorm:"uid"`
AccessToken string `json:"accessToken" xorm:"access_token"`
Title string `json:"title" xorm:"title"`
DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"`
IsEnabled bool `json:"isEnabled" xorm:"is_enabled"`
}
type TimeSettings struct {
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
@@ -212,6 +212,29 @@ func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context,
return r0, r1
}
// ListPublicDashboards provides a mock function with given fields: ctx, orgId
func (_m *FakePublicDashboardService) ListPublicDashboards(ctx context.Context, orgId int64) ([]publicdashboardsmodels.PublicDashboardListResponse, error) {
ret := _m.Called(ctx, orgId)
var r0 []publicdashboardsmodels.PublicDashboardListResponse
if rf, ok := ret.Get(0).(func(context.Context, int64) []publicdashboardsmodels.PublicDashboardListResponse); ok {
r0 = rf(ctx, orgId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]publicdashboardsmodels.PublicDashboardListResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, orgId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PublicDashboardEnabled provides a mock function with given fields: ctx, dashboardUid
func (_m *FakePublicDashboardService) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) {
ret := _m.Called(ctx, dashboardUid)
@@ -182,6 +182,29 @@ func (_m *FakePublicDashboardStore) GetPublicDashboardOrgId(ctx context.Context,
return r0, r1
}
// ListPublicDashboards provides a mock function with given fields: ctx, orgId
func (_m *FakePublicDashboardStore) ListPublicDashboards(ctx context.Context, orgId int64) ([]publicdashboardsmodels.PublicDashboardListResponse, error) {
ret := _m.Called(ctx, orgId)
var r0 []publicdashboardsmodels.PublicDashboardListResponse
if rf, ok := ret.Get(0).(func(context.Context, int64) []publicdashboardsmodels.PublicDashboardListResponse); ok {
r0 = rf(ctx, orgId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]publicdashboardsmodels.PublicDashboardListResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, orgId)
} 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)
@@ -22,6 +22,7 @@ type Service interface {
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)
ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error)
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error)
}
@@ -35,6 +36,7 @@ type Store interface {
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)
ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error)
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error
UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error
@@ -54,6 +54,12 @@ func ProvideService(
}
}
// Gets a list of public dashboards by orgId
func (pd *PublicDashboardServiceImpl) ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) {
return pd.store.ListPublicDashboards(ctx, orgId)
}
// Gets a dashboard by Uid
func (pd *PublicDashboardServiceImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
dashboard, err := pd.store.GetDashboard(ctx, dashboardUid)