PublicDashboards: Audit table pagination (#69823)

This commit is contained in:
Juan Cabanas 2023-06-21 10:48:09 -03:00 committed by GitHub
parent ed5a697825
commit ee73d41d24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 475 additions and 305 deletions

View File

@ -60,7 +60,7 @@ func (sb *SQLBuilder) AddParams(params ...interface{}) {
sb.params = append(sb.params, params...)
}
func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission dashboards.PermissionType) {
func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission dashboards.PermissionType, queryType string) {
var (
sql string
params []interface{}
@ -68,7 +68,7 @@ func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, pe
recQryParams []interface{}
)
if !ac.IsDisabled(sb.cfg) {
filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, permission, "", sb.features, sb.recursiveQueriesAreSupported)
filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, permission, queryType, sb.features, sb.recursiveQueriesAreSupported)
sql, params = filterRBAC.Where()
recQry, recQryParams = filterRBAC.With()
} else {

View File

@ -480,7 +480,7 @@ func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, acl
var res []*dashboardResponse
builder.Write("SELECT * FROM dashboard WHERE true")
builder.WriteDashboardPermissionFilter(signedInUser, search.RequiredPermission)
builder.WriteDashboardPermissionFilter(signedInUser, search.RequiredPermission, "")
t.Logf("Searching for dashboards, SQL: %q\n", builder.GetSQLString())
err = sqlStore.GetEngine().SQL(builder.GetSQLString(), builder.params...).Find(&res)
require.NoError(t, err)

View File

@ -167,7 +167,7 @@ func (ss *sqlStore) HandleAlertsQuery(ctx context.Context, query *alertmodels.Ge
builder.Write(")")
}
builder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW)
builder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW, "")
builder.Write(" ORDER BY name ASC")

View File

@ -250,7 +250,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed
builder.Write(getFromLibraryElementDTOWithMeta(store.GetDialect()))
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0")
writeParamSelectorSQL(&builder, params...)
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW)
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "")
builder.Write(` OR dashboard.id=0`)
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElements); err != nil {
return err
@ -372,7 +372,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
return err
}
if signedInUser.OrgRole != org.RoleAdmin {
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW)
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "")
}
if query.SortDirection == search.SortAlphaDesc.Name {
builder.Write(" ORDER BY 1 DESC")
@ -603,7 +603,7 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser *
builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id")
builder.Write(` WHERE lec.element_id=?`, element.ID)
if signedInUser.OrgRole != org.RoleAdmin {
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW)
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "")
}
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElementConnections); err != nil {
return err

View File

@ -90,7 +90,24 @@ func (api *Api) RegisterAPIEndpoints() {
// ListPublicDashboards Gets list of public dashboards by orgId
// GET /api/dashboards/public-dashboards
func (api *Api) ListPublicDashboards(c *contextmodel.ReqContext) response.Response {
resp, err := api.PublicDashboardService.FindAll(c.Req.Context(), c.SignedInUser, c.OrgID)
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
resp, err := api.PublicDashboardService.FindAllWithPagination(c.Req.Context(), &PublicDashboardListQuery{
OrgID: c.OrgID,
Query: c.Query("query"),
Page: page,
Limit: perPage,
User: c.SignedInUser,
})
if err != nil {
return response.Err(err)
}

View File

@ -89,19 +89,21 @@ func TestAPIFeatureFlag(t *testing.T) {
}
func TestAPIListPublicDashboard(t *testing.T) {
successResp := []PublicDashboardListResponse{
successResp := &PublicDashboardListResponseWithPagination{
PublicDashboards: []*PublicDashboardListResponse{
{
Uid: "1234asdfasdf",
AccessToken: "asdfasdf",
DashboardUid: "abc1234",
IsEnabled: true,
},
},
}
testCases := []struct {
Name string
User *user.SignedInUser
Response []PublicDashboardListResponse
Response *PublicDashboardListResponseWithPagination
ResponseErr error
ExpectedHttpResponse int
}{
@ -131,7 +133,7 @@ func TestAPIListPublicDashboard(t *testing.T) {
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("FindAll", mock.Anything, mock.Anything, mock.Anything).
service.On("FindAllWithPagination", mock.Anything, mock.Anything, mock.Anything).
Return(test.Response, test.ResponseErr).Maybe()
cfg := setting.NewCfg()
@ -143,10 +145,10 @@ func TestAPIListPublicDashboard(t *testing.T) {
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if test.ExpectedHttpResponse == http.StatusOK {
var jsonResp []PublicDashboardListResponse
var jsonResp PublicDashboardListResponseWithPagination
err := json.Unmarshal(response.Body.Bytes(), &jsonResp)
require.NoError(t, err)
assert.Equal(t, jsonResp[0].Uid, "1234asdfasdf")
assert.Equal(t, jsonResp.PublicDashboards[0].Uid, "1234asdfasdf")
}
if test.ResponseErr != nil {
@ -155,7 +157,7 @@ func TestAPIListPublicDashboard(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "Internal server error", errResp.Message)
assert.Equal(t, "publicdashboards.internalServerError", errResp.MessageID)
service.AssertNotCalled(t, "FindAll")
service.AssertNotCalled(t, "FindAllWithPagination")
}
})
}

View File

@ -322,7 +322,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
annotationsService := annotationstest.NewFakeAnnotationsRepo()
// create public dashboard
store := publicdashboardsStore.ProvideStore(db)
store := publicdashboardsStore.ProvideStore(db, db.Cfg, featuremgmt.WithFeatures())
cfg := setting.NewCfg()
ac := acmock.New()
ws := publicdashboardsService.ProvideServiceWrapper(store)

View File

@ -7,9 +7,13 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/publicdashboards"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/setting"
)
// Define the storage implementation. We're generating the mock implementation
@ -17,6 +21,8 @@ import (
type PublicDashboardStoreImpl struct {
sqlStore db.DB
log log.Logger
cfg *setting.Cfg
features featuremgmt.FeatureToggles
}
var LogPrefix = "publicdashboards.store"
@ -26,25 +32,54 @@ var LogPrefix = "publicdashboards.store"
var _ publicdashboards.Store = (*PublicDashboardStoreImpl)(nil)
// Factory used by wire to dependency injection
func ProvideStore(sqlStore db.DB) *PublicDashboardStoreImpl {
func ProvideStore(sqlStore db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles) *PublicDashboardStoreImpl {
return &PublicDashboardStoreImpl{
sqlStore: sqlStore,
log: log.New(LogPrefix),
cfg: cfg,
features: features,
}
}
// FindAll Returns a list of public dashboards by orgId
func (d *PublicDashboardStoreImpl) FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) {
resp := make([]PublicDashboardListResponse, 0)
// FindAllWithPagination Returns a list of public dashboards by orgId, based on permissions and with pagination
func (d *PublicDashboardStoreImpl) FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error) {
resp := &PublicDashboardListResponseWithPagination{
PublicDashboards: make([]*PublicDashboardListResponse, 0),
TotalCount: 0,
}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
sess.Table("dashboard_public").Select(
"dashboard_public.uid, dashboard_public.access_token, dashboard.uid as dashboard_uid, dashboard_public.is_enabled, dashboard.title").
Join("LEFT", "dashboard", "dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id").
Where("dashboard_public.org_id = ?", orgId).
OrderBy(" dashboard.title IS NULL, dashboard.title ASC")
recursiveQueriesAreSupported, err := d.sqlStore.RecursiveQueriesAreSupported()
if err != nil {
return nil, err
}
err := sess.Find(&resp)
pubdashBuilder := db.NewSqlBuilder(d.cfg, d.features, d.sqlStore.GetDialect(), recursiveQueriesAreSupported)
pubdashBuilder.Write("SELECT dashboard_public.uid, dashboard_public.access_token, dashboard.uid as dashboard_uid, dashboard_public.is_enabled, dashboard.title")
pubdashBuilder.Write(" FROM dashboard_public")
pubdashBuilder.Write(" JOIN dashboard ON dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id")
pubdashBuilder.Write(` WHERE dashboard_public.org_id = ?`, query.OrgID)
if query.User.OrgRole != org.RoleAdmin {
pubdashBuilder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard)
}
pubdashBuilder.Write(" ORDER BY dashboard.title")
pubdashBuilder.Write(d.sqlStore.GetDialect().LimitOffset(int64(query.Limit), int64(query.Offset)))
counterBuilder := db.NewSqlBuilder(d.cfg, d.features, d.sqlStore.GetDialect(), recursiveQueriesAreSupported)
counterBuilder.Write("SELECT COUNT(*)")
counterBuilder.Write(" FROM dashboard_public")
counterBuilder.Write(" JOIN dashboard ON dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id")
counterBuilder.Write(` WHERE dashboard_public.org_id = ?`, query.OrgID)
if query.User.OrgRole != org.RoleAdmin {
counterBuilder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard)
}
err = d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
err := sess.SQL(pubdashBuilder.GetSQLString(), pubdashBuilder.GetParams()...).Find(&resp.PublicDashboards)
if err != nil {
return err
}
_, err = sess.SQL(counterBuilder.GetSQLString(), counterBuilder.GetParams()...).Get(&resp.TotalCount)
return err
})

View File

@ -2,18 +2,24 @@ package database
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"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/models"
"github.com/grafana/grafana/pkg/services/publicdashboards/service"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
@ -34,38 +40,122 @@ func TestIntegrationListPublicDashboard(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore, cfg := db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := ProvideStore(sqlStore)
var sqlStore *sqlstore.SQLStore
var cfg *setting.Cfg
var aDash *dashboards.Dashboard
var bDash *dashboards.Dashboard
var cDash *dashboards.Dashboard
var aPublicDash *PublicDashboard
var bPublicDash *PublicDashboard
var cPublicDash *PublicDashboard
var orgId int64 = 1
bDash := insertTestDashboard(t, dashboardStore, "b", orgId, 0, true)
aDash := insertTestDashboard(t, dashboardStore, "a", orgId, 0, true)
cDash := insertTestDashboard(t, dashboardStore, "c", orgId, 0, true)
var publicdashboardStore *PublicDashboardStoreImpl
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
bDash = insertTestDashboard(t, dashboardStore, "b", orgId, 0, false)
aDash = insertTestDashboard(t, dashboardStore, "a", orgId, 0, false)
cDash = insertTestDashboard(t, dashboardStore, "c", orgId, 0, false)
// these are in order of how they should be returned from ListPUblicDashboards
a := insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType)
b := insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true, PublicShareType)
c := insertPublicDashboard(t, publicdashboardStore, cDash.UID, orgId, true, PublicShareType)
aPublicDash = insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType)
bPublicDash = insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true, PublicShareType)
cPublicDash = insertPublicDashboard(t, publicdashboardStore, cDash.UID, orgId, true, PublicShareType)
}
// 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)
t.Run("FindAllWithPagination will return dashboard list based on orgId with pagination", func(t *testing.T) {
setup()
// should not be included in response
_ = insertPublicDashboard(t, publicdashboardStore, "wrongOrgId", 777, false, PublicShareType)
resp, err := publicdashboardStore.FindAll(context.Background(), orgId)
permissions := []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", aDash.UID)},
{Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", bDash.UID)},
{Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", cDash.UID)},
}
err := insertPermissions(sqlStore, orgId, "viewer", permissions)
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)
query := &PublicDashboardListQuery{
User: &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}},
OrgID: orgId,
Page: 1,
Limit: 50,
Offset: 0,
}
resp, err := publicdashboardStore.FindAllWithPagination(context.Background(), query)
require.NoError(t, err)
assert.Len(t, resp.PublicDashboards, 3)
assert.Equal(t, resp.PublicDashboards[0].Uid, aPublicDash.Uid)
assert.Equal(t, resp.PublicDashboards[1].Uid, bPublicDash.Uid)
assert.Equal(t, resp.PublicDashboards[2].Uid, cPublicDash.Uid)
assert.Equal(t, resp.TotalCount, int64(3))
})
t.Run("FindAllWithPagination will return dashboard list based on read permissions with pagination", func(t *testing.T) {
setup()
permissions := []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", aDash.UID)},
{Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", cDash.UID)},
}
err := insertPermissions(sqlStore, orgId, "viewer", permissions)
require.NoError(t, err)
query := &PublicDashboardListQuery{
User: &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}},
OrgID: orgId,
Page: 1,
Limit: 50,
Offset: 0,
}
resp, err := publicdashboardStore.FindAllWithPagination(context.Background(), query)
require.NoError(t, err)
assert.Len(t, resp.PublicDashboards, 2)
assert.Equal(t, resp.PublicDashboards[0].Uid, aPublicDash.Uid)
assert.Equal(t, resp.PublicDashboards[1].Uid, cPublicDash.Uid)
assert.Equal(t, resp.TotalCount, int64(2))
})
t.Run("FindAllWithPagination will return empty dashboard list based on read permissions with pagination", func(t *testing.T) {
setup()
permissions := []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:another-dashboard-uid"},
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:another-dashboard-2-uid"},
}
err := insertPermissions(sqlStore, orgId, "viewer", permissions)
require.NoError(t, err)
query := &PublicDashboardListQuery{
User: &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}},
OrgID: orgId,
Page: 1,
Limit: 50,
Offset: 0,
}
resp, err := publicdashboardStore.FindAllWithPagination(context.Background(), query)
require.NoError(t, err)
assert.Len(t, resp.PublicDashboards, 0)
assert.Equal(t, resp.TotalCount, int64(0))
})
}
func TestIntegrationFindDashboard(t *testing.T) {
@ -84,7 +174,7 @@ func TestIntegrationFindDashboard(t *testing.T) {
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -114,7 +204,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) {
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("ExistsEnabledByAccessToken will return true when at least one public dashboard has a matching access token", func(t *testing.T) {
@ -187,7 +277,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) {
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -252,7 +342,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) {
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -319,7 +409,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -389,7 +479,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) {
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true)
insertPublicDashboard(t, publicdashboardStore, savedDashboard2.UID, savedDashboard2.OrgID, false, PublicShareType)
@ -468,7 +558,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
anotherSavedDashboard = insertTestDashboard(t, dashboardStore, "test another Dashie", 1, 0, true)
}
@ -572,7 +662,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("GetOrgIdByAccessToken will OrgId when enabled", func(t *testing.T) {
@ -644,7 +734,7 @@ func TestIntegrationDelete(t *testing.T) {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, true, PublicShareType)
}
@ -675,7 +765,7 @@ func TestGetDashboardByFolder(t *testing.T) {
t.Run("returns nil when dashboard is not a folder", func(t *testing.T) {
sqlStore, _ := db.InitTestDBwithCfg(t)
dashboard := &dashboards.Dashboard{IsFolder: false}
store := ProvideStore(sqlStore)
store := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
pubdashes, err := store.FindByDashboardFolder(context.Background(), dashboard)
require.NoError(t, err)
@ -684,7 +774,7 @@ func TestGetDashboardByFolder(t *testing.T) {
t.Run("returns nil when dashboard is nil", func(t *testing.T) {
sqlStore, _ := db.InitTestDBwithCfg(t)
store := ProvideStore(sqlStore)
store := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
pubdashes, err := store.FindByDashboardFolder(context.Background(), nil)
require.NoError(t, err)
@ -696,7 +786,7 @@ func TestGetDashboardByFolder(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
pubdashStore := ProvideStore(sqlStore)
pubdashStore := ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
dashboard := insertTestDashboard(t, dashboardStore, "title", 1, 1, true, PublicShareType)
pubdash := insertPublicDashboard(t, pubdashStore, dashboard.UID, dashboard.OrgID, true, PublicShareType)
dashboard2 := insertTestDashboard(t, dashboardStore, "title", 1, 2, true, PublicShareType)
@ -729,7 +819,7 @@ func TestGetMetrics(t *testing.T) {
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, false)
savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, false)
savedDashboard3 = insertTestDashboard(t, dashboardStore, "testDashie3", 2, 0, false)
@ -827,3 +917,50 @@ func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardSt
return pubdash
}
func insertPermissions(sqlStore *sqlstore.SQLStore, orgId int64, role string, permissions []accesscontrol.Permission) error {
return sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
newRole := &accesscontrol.Role{
OrgID: orgId,
UID: fmt.Sprintf("basic_%s", role),
Name: fmt.Sprintf("basic:%s", role),
Updated: time.Now(),
Created: time.Now(),
}
_, err := sess.Insert(newRole)
if err != nil {
return err
}
_, err = sess.Insert(accesscontrol.BuiltinRole{
OrgID: orgId,
RoleID: newRole.ID,
Role: strings.ToUpper(role[:1]) + role[1:],
Created: time.Now(),
Updated: time.Now(),
})
if err != nil {
return err
}
for i := range permissions {
permissions[i].RoleID = newRole.ID
permissions[i].Created = time.Now()
permissions[i].Updated = time.Now()
}
_, err = sess.InsertMulti(&permissions)
if err != nil {
return err
}
_, err = sess.Insert(accesscontrol.UserRole{
OrgID: orgId,
RoleID: newRole.ID,
UserID: 1,
Created: time.Now(),
})
return err
})
}

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/grafana/grafana/pkg/services/user"
)
// PublicDashboardErr represents a dashboard error.
@ -103,6 +104,22 @@ func (pd PublicDashboard) TableName() string {
return "dashboard_public"
}
type PublicDashboardListQuery struct {
OrgID int64
Query string
Page int
Limit int
Offset int
User *user.SignedInUser
}
type PublicDashboardListResponseWithPagination struct {
PublicDashboards []*PublicDashboardListResponse `json:"publicDashboards"`
TotalCount int64 `json:"totalCount"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
type PublicDashboardListResponse struct {
Uid string `json:"uid" xorm:"uid"`
AccessToken string `json:"accessToken" xorm:"access_token"`

View File

@ -139,22 +139,22 @@ func (_m *FakePublicDashboardService) Find(ctx context.Context, uid string) (*mo
return r0, r1
}
// FindAll provides a mock function with given fields: ctx, u, orgId
func (_m *FakePublicDashboardService) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]models.PublicDashboardListResponse, error) {
ret := _m.Called(ctx, u, orgId)
// FindAllWithPagination provides a mock function with given fields: ctx, query
func (_m *FakePublicDashboardService) FindAllWithPagination(ctx context.Context, query *models.PublicDashboardListQuery) (*models.PublicDashboardListResponseWithPagination, error) {
ret := _m.Called(ctx, query)
var r0 []models.PublicDashboardListResponse
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64) []models.PublicDashboardListResponse); ok {
r0 = rf(ctx, u, orgId)
var r0 *models.PublicDashboardListResponseWithPagination
if rf, ok := ret.Get(0).(func(context.Context, *models.PublicDashboardListQuery) *models.PublicDashboardListResponseWithPagination); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]models.PublicDashboardListResponse)
r0 = ret.Get(0).(*models.PublicDashboardListResponseWithPagination)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64) error); ok {
r1 = rf(ctx, u, orgId)
if rf, ok := ret.Get(1).(func(context.Context, *models.PublicDashboardListQuery) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}

View File

@ -123,22 +123,22 @@ func (_m *FakePublicDashboardStore) Find(ctx context.Context, uid string) (*mode
return r0, r1
}
// FindAll provides a mock function with given fields: ctx, orgId
func (_m *FakePublicDashboardStore) FindAll(ctx context.Context, orgId int64) ([]models.PublicDashboardListResponse, error) {
ret := _m.Called(ctx, orgId)
// FindAllWithPagination provides a mock function with given fields: ctx, query
func (_m *FakePublicDashboardStore) FindAllWithPagination(ctx context.Context, query *models.PublicDashboardListQuery) (*models.PublicDashboardListResponseWithPagination, error) {
ret := _m.Called(ctx, query)
var r0 []models.PublicDashboardListResponse
if rf, ok := ret.Get(0).(func(context.Context, int64) []models.PublicDashboardListResponse); ok {
r0 = rf(ctx, orgId)
var r0 *models.PublicDashboardListResponseWithPagination
if rf, ok := ret.Get(0).(func(context.Context, *models.PublicDashboardListQuery) *models.PublicDashboardListResponseWithPagination); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]models.PublicDashboardListResponse)
r0 = ret.Get(0).(*models.PublicDashboardListResponseWithPagination)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, orgId)
if rf, ok := ret.Get(1).(func(context.Context, *models.PublicDashboardListQuery) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}

View File

@ -21,7 +21,7 @@ type Service interface {
FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error)
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error)
FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error)
FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error)
Find(ctx context.Context, uid string) (*PublicDashboard, error)
Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
@ -52,7 +52,7 @@ type Store interface {
FindByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, error)
FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error)
FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error)
FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error)
Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error)
Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error)
Delete(ctx context.Context, uid string) (int64, error)

View File

@ -663,7 +663,7 @@ func TestGetQueryDataResponse(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
fakeQueryService := &query.FakeQueryService{}
fakeQueryService.On("QueryData", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&backend.QueryDataResponse{}, nil)
@ -1109,7 +1109,7 @@ func TestGetMetricRequest(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
publicDashboard := &PublicDashboard{
Uid: "1",
@ -1194,7 +1194,7 @@ func TestBuildMetricRequest(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true, []map[string]interface{}{}, nil)

View File

@ -276,14 +276,18 @@ func (pd *PublicDashboardServiceImpl) NewPublicDashboardAccessToken(ctx context.
return "", ErrInternalServerError.Errorf("failed to generate a unique accessToken for public dashboard")
}
// FindAll Returns a list of public dashboards by orgId
func (pd *PublicDashboardServiceImpl) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) {
publicDashboards, err := pd.store.FindAll(ctx, orgId)
// FindAllWithPagination Returns a list of public dashboards by orgId, based on permissions and with pagination
func (pd *PublicDashboardServiceImpl) FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error) {
query.Offset = query.Limit * (query.Page - 1)
resp, err := pd.store.FindAllWithPagination(ctx, query)
if err != nil {
return nil, ErrInternalServerError.Errorf("FindAll: %w", err)
return nil, ErrInternalServerError.Errorf("FindAllWithPagination: %w", err)
}
return pd.filterDashboardsByPermissions(ctx, u, publicDashboards)
resp.Page = query.Page
resp.PerPage = query.Limit
return resp, nil
}
func (pd *PublicDashboardServiceImpl) ExistsEnabledByDashboardUid(ctx context.Context, dashboardUid string) (bool, error) {
@ -371,25 +375,6 @@ func (pd *PublicDashboardServiceImpl) logIsEnabledChanged(existingPubdash *Publi
}
}
// Filter out dashboards that user does not have read access to
func (pd *PublicDashboardServiceImpl) filterDashboardsByPermissions(ctx context.Context, u *user.SignedInUser, publicDashboards []PublicDashboardListResponse) ([]PublicDashboardListResponse, error) {
result := make([]PublicDashboardListResponse, 0)
for i := range publicDashboards {
hasAccess, err := pd.ac.Evaluate(ctx, u, accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(publicDashboards[i].DashboardUid)))
// If original dashboard does not exist, the public dashboard is an orphan. We want to list it anyway
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {
return nil, ErrInternalServerError.Errorf("filterDashboardsByPermissions: error evaluating permissions %w", err)
}
// If user has access to the original dashboard or the dashboard does not exist, add the pubdash to the result
if hasAccess || errors.Is(err, dashboards.ErrDashboardNotFound) {
result = append(result, publicDashboards[i])
}
}
return result, nil
}
// Checks to see if PublicDashboard.ExistsEnabledByDashboardUid is true on create or changed on update
func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdash *PublicDashboard) bool {
// creating dashboard, enabled true

View File

@ -195,7 +195,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -285,7 +285,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -325,7 +325,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -359,7 +359,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
templateVars := make([]map[string]interface{}, 1)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, templateVars, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -474,7 +474,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -519,7 +519,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
@ -588,7 +588,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
@ -686,7 +686,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
@ -915,186 +915,102 @@ func TestDashboardEnabledChanged(t *testing.T) {
func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
type args struct {
ctx context.Context
u *user.SignedInUser
orgId int64
query *PublicDashboardListQuery
}
type mockResponse struct {
PublicDashboardListResponseWithPagination *PublicDashboardListResponseWithPagination
Err error
}
mockedDashboards := []*PublicDashboardListResponse{
{
Uid: "0GwW7mgVk",
AccessToken: "0b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "0S6TmO67z",
Title: "my zero dashboard",
IsEnabled: true,
},
{
Uid: "1GwW7mgVk",
AccessToken: "1b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "1S6TmO67z",
Title: "my first dashboard",
IsEnabled: true,
},
{
Uid: "2GwW7mgVk",
AccessToken: "2b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "2S6TmO67z",
Title: "my second dashboard",
IsEnabled: false,
},
{
Uid: "9GwW7mgVk",
AccessToken: "deletedashboardaccesstoken",
DashboardUid: "9S6TmO67z",
Title: "",
IsEnabled: true,
},
}
testCases := []struct {
name string
args args
evaluateFunc func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error)
want []PublicDashboardListResponse
want *PublicDashboardListResponseWithPagination
mockResponse *mockResponse
wantErr assert.ErrorAssertionFunc
}{
{
name: "should return empty list when user does not have permissions to read any dashboard",
name: "should return correct pagination response",
args: args{
ctx: context.Background(),
u: &user.SignedInUser{OrgID: 1},
orgId: 1,
},
want: []PublicDashboardListResponse{},
wantErr: assert.NoError,
},
{
name: "should return all dashboards when has permissions",
args: args{
ctx: context.Background(),
u: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {"dashboards:read": {
"dashboards:uid:0S6TmO67z", "dashboards:uid:1S6TmO67z", "dashboards:uid:2S6TmO67z", "dashboards:uid:9S6TmO67z",
}}},
},
orgId: 1,
},
want: []PublicDashboardListResponse{
{
Uid: "0GwW7mgVk",
AccessToken: "0b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "0S6TmO67z",
Title: "my zero dashboard",
IsEnabled: true,
},
{
Uid: "1GwW7mgVk",
AccessToken: "1b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "1S6TmO67z",
Title: "my first dashboard",
IsEnabled: true,
},
{
Uid: "2GwW7mgVk",
AccessToken: "2b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "2S6TmO67z",
Title: "my second dashboard",
IsEnabled: false,
},
{
Uid: "9GwW7mgVk",
AccessToken: "deletedashboardaccesstoken",
DashboardUid: "9S6TmO67z",
Title: "",
IsEnabled: true,
},
},
wantErr: assert.NoError,
},
{
name: "should return only dashboards with permissions",
args: args{
ctx: context.Background(),
u: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
query: &PublicDashboardListQuery{
User: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}},
},
orgId: 1,
OrgID: 1,
Page: 1,
Limit: 50,
},
want: []PublicDashboardListResponse{
{
Uid: "0GwW7mgVk",
AccessToken: "0b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "0S6TmO67z",
Title: "my zero dashboard",
IsEnabled: true,
},
mockResponse: &mockResponse{
PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{
TotalCount: int64(len(mockedDashboards)),
PublicDashboards: mockedDashboards,
},
Err: nil,
},
want: &PublicDashboardListResponseWithPagination{
Page: 1,
PerPage: 50,
TotalCount: int64(len(mockedDashboards)),
PublicDashboards: mockedDashboards,
},
wantErr: assert.NoError,
},
{
name: "should return orphaned public dashboards",
name: "should return error when store returns error",
args: args{
ctx: context.Background(),
u: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
query: &PublicDashboardListQuery{
User: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}},
},
orgId: 1,
},
evaluateFunc: func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error) {
return false, dashboards.ErrDashboardNotFound
},
want: []PublicDashboardListResponse{
{
Uid: "0GwW7mgVk",
AccessToken: "0b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "0S6TmO67z",
Title: "my zero dashboard",
IsEnabled: true,
},
{
Uid: "1GwW7mgVk",
AccessToken: "1b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "1S6TmO67z",
Title: "my first dashboard",
IsEnabled: true,
},
{
Uid: "2GwW7mgVk",
AccessToken: "2b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "2S6TmO67z",
Title: "my second dashboard",
IsEnabled: false,
},
{
Uid: "9GwW7mgVk",
AccessToken: "deletedashboardaccesstoken",
DashboardUid: "9S6TmO67z",
Title: "",
IsEnabled: true,
OrgID: 1,
Page: 1,
Limit: 50,
},
},
wantErr: assert.NoError,
},
{
name: "errors different than not data found should be returned",
args: args{
ctx: context.Background(),
u: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}},
},
orgId: 1,
},
evaluateFunc: func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error) {
return false, dashboards.ErrDashboardCorrupt
mockResponse: &mockResponse{
PublicDashboardListResponseWithPagination: nil,
Err: errors.New("an err"),
},
want: nil,
wantErr: assert.Error,
},
}
mockedDashboards := []PublicDashboardListResponse{
{
Uid: "0GwW7mgVk",
AccessToken: "0b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "0S6TmO67z",
Title: "my zero dashboard",
IsEnabled: true,
},
{
Uid: "1GwW7mgVk",
AccessToken: "1b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "1S6TmO67z",
Title: "my first dashboard",
IsEnabled: true,
},
{
Uid: "2GwW7mgVk",
AccessToken: "2b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "2S6TmO67z",
Title: "my second dashboard",
IsEnabled: false,
},
{
Uid: "9GwW7mgVk",
AccessToken: "deletedashboardaccesstoken",
DashboardUid: "9S6TmO67z",
Title: "",
IsEnabled: true,
},
}
store := NewFakePublicDashboardStore(t)
store.On("FindAll", mock.Anything, mock.Anything).
Return(mockedDashboards, nil)
ac := tests.SetupMockAccesscontrol(t,
func(c context.Context, siu *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) {
return []accesscontrol.Permission{}, nil
@ -1102,21 +1018,23 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
false,
)
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
store := NewFakePublicDashboardStore(t)
store.On("FindAllWithPagination", mock.Anything, mock.Anything).
Return(tt.mockResponse.PublicDashboardListResponseWithPagination, tt.mockResponse.Err)
pd := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: store,
ac: ac,
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ac.EvaluateFunc = tt.evaluateFunc
got, err := pd.FindAll(tt.args.ctx, tt.args.u, tt.args.orgId)
if !tt.wantErr(t, err, fmt.Sprintf("FindAll(%v, %v, %v)", tt.args.ctx, tt.args.u, tt.args.orgId)) {
got, err := pd.FindAllWithPagination(tt.args.ctx, tt.args.query)
if !tt.wantErr(t, err, fmt.Sprintf("FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query)) {
return
}
assert.Equalf(t, tt.want, got, "FindAll(%v, %v, %v)", tt.args.ctx, tt.args.u, tt.args.orgId)
assert.Equalf(t, tt.want, got, "FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query)
})
}
}

View File

@ -11,7 +11,10 @@ import {
SessionUser,
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { DashboardModel } from 'app/features/dashboard/state';
import { ListPublicDashboardResponse } from 'app/features/manage-dashboards/types';
import {
PublicDashboardListWithPagination,
PublicDashboardListWithPaginationResponse,
} from 'app/features/manage-dashboards/types';
type ReqOptions = {
manageError?: (err: unknown) => { error: unknown };
@ -137,9 +140,13 @@ export const publicDashboardApi = createApi({
}),
providesTags: (result, _, email) => [{ type: 'ActiveUserDashboards', id: email }],
}),
listPublicDashboards: builder.query<ListPublicDashboardResponse[], void>({
query: () => ({
url: '/dashboards/public-dashboards',
listPublicDashboards: builder.query<PublicDashboardListWithPagination, number | void>({
query: (page = 1) => ({
url: `/dashboards/public-dashboards?page=${page}&perpage=8`,
}),
transformResponse: (response: PublicDashboardListWithPaginationResponse) => ({
...response,
totalPages: Math.ceil(response.totalCount / response.perPage),
}),
providesTags: ['AuditTablePublicDashboard'],
}),

View File

@ -11,11 +11,11 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv';
import { ListPublicDashboardResponse } from '../../types';
import { PublicDashboardListResponse, PublicDashboardListWithPaginationResponse } from '../../types';
import { PublicDashboardListTable } from './PublicDashboardListTable';
const publicDashboardListResponse: ListPublicDashboardResponse[] = [
const publicDashboardListResponse: PublicDashboardListResponse[] = [
{
uid: 'SdZwuCZVz',
accessToken: 'beeaf92f6ab3467f80b2be922c7741ab',
@ -32,7 +32,7 @@ const publicDashboardListResponse: ListPublicDashboardResponse[] = [
},
];
const orphanedDashboardListResponse: ListPublicDashboardResponse[] = [
const orphanedDashboardListResponse: PublicDashboardListResponse[] = [
{
uid: 'SdZwuCZVz2',
accessToken: 'beeaf92f6ab3467f80b2be922c7741ab',
@ -49,9 +49,15 @@ const orphanedDashboardListResponse: ListPublicDashboardResponse[] = [
},
];
const paginationResponse: Omit<PublicDashboardListWithPaginationResponse, 'publicDashboards'> = {
page: 1,
perPage: 50,
totalCount: 50,
};
const server = setupServer(
rest.get('/api/dashboards/public-dashboards', (_, res, ctx) =>
res(ctx.status(200), ctx.json(publicDashboardListResponse))
res(ctx.status(200), ctx.json({ ...paginationResponse, publicDashboards: publicDashboardListResponse }))
),
rest.delete('/api/dashboards/uid/:dashboardUid/public-dashboards/:uid', (_, res, ctx) => res(ctx.status(200)))
);
@ -104,9 +110,16 @@ describe('Show table', () => {
expect(screen.getAllByRole('listitem')).toHaveLength(publicDashboardListResponse.length);
});
it('renders empty list', async () => {
const emptyListRS: PublicDashboardListWithPaginationResponse = {
publicDashboards: [],
totalCount: 0,
page: 1,
perPage: 50,
};
server.use(
rest.get('/api/dashboards/public-dashboards', (req, res, ctx) => {
return res(ctx.status(200), ctx.json([]));
return res(ctx.status(200), ctx.json(emptyListRS));
})
);
@ -149,7 +162,10 @@ describe('Delete public dashboard', () => {
describe('Orphaned public dashboard', () => {
it('renders orphaned and non orphaned public dashboards items correctly', async () => {
const response = [...publicDashboardListResponse, ...orphanedDashboardListResponse];
const response: PublicDashboardListWithPaginationResponse = {
...paginationResponse,
publicDashboards: [...publicDashboardListResponse, ...orphanedDashboardListResponse],
};
server.use(
rest.get('/api/dashboards/public-dashboards', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(response));
@ -158,13 +174,13 @@ describe('Orphaned public dashboard', () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
await renderPublicDashboardTable(true);
response.forEach((pd, idx) => {
response.publicDashboards.forEach((pd, idx) => {
renderPublicDashboardItemCorrectly(pd, idx, true);
});
});
});
const renderPublicDashboardItemCorrectly = (pd: ListPublicDashboardResponse, idx: number, hasWriteAccess: boolean) => {
const renderPublicDashboardItemCorrectly = (pd: PublicDashboardListResponse, idx: number, hasWriteAccess: boolean) => {
const isOrphaned = !pd.dashboardUid;
const cardItems = screen.getAllByRole('listitem');

View File

@ -1,11 +1,22 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import { useMedia } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime';
import { LinkButton, useStyles2, Spinner, Card, useTheme2, Tooltip, Icon, Switch } from '@grafana/ui/src';
import {
LinkButton,
useStyles2,
Spinner,
Card,
useTheme2,
Tooltip,
Icon,
Switch,
Pagination,
HorizontalGroup,
} from '@grafana/ui/src';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/services/context_srv';
import {
@ -19,11 +30,11 @@ import {
import { isOrgAdmin } from 'app/features/plugins/admin/permissions';
import { AccessControlAction } from 'app/types';
import { ListPublicDashboardResponse } from '../../types';
import { PublicDashboardListResponse } from '../../types';
import { DeletePublicDashboardButton } from './DeletePublicDashboardButton';
const PublicDashboardCard = ({ pd }: { pd: ListPublicDashboardResponse }) => {
const PublicDashboardCard = ({ pd }: { pd: PublicDashboardListResponse }) => {
const styles = useStyles2(getStyles);
const theme = useTheme2();
const isMobile = useMedia(`(max-width: ${theme.breakpoints.values.sm}px)`);
@ -34,7 +45,7 @@ const PublicDashboardCard = ({ pd }: { pd: ListPublicDashboardResponse }) => {
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const isOrphaned = !pd.dashboardUid;
const onTogglePause = (pd: ListPublicDashboardResponse, isPaused: boolean) => {
const onTogglePause = (pd: PublicDashboardListResponse, isPaused: boolean) => {
const req = {
dashboard: { uid: pd.dashboardUid },
payload: {
@ -118,20 +129,33 @@ const PublicDashboardCard = ({ pd }: { pd: ListPublicDashboardResponse }) => {
};
export const PublicDashboardListTable = () => {
const styles = useStyles2(getStyles);
const [page, setPage] = useState(1);
const { data: publicDashboards, isLoading, isFetching } = useListPublicDashboardsQuery();
const styles = useStyles2(getStyles);
const { data: paginatedPublicDashboards, isLoading, isFetching, isError } = useListPublicDashboardsQuery(page);
return (
<Page navId="dashboards/public" actions={isFetching && <Spinner />}>
<Page.Contents isLoading={isLoading}>
{!isLoading && !isError && !!paginatedPublicDashboards && (
<div>
<ul className={styles.list}>
{publicDashboards?.map((pd: ListPublicDashboardResponse) => (
{paginatedPublicDashboards.publicDashboards.map((pd: PublicDashboardListResponse) => (
<li key={pd.uid}>
<PublicDashboardCard pd={pd} />
</li>
))}
</ul>
<HorizontalGroup justify="flex-end">
<Pagination
onNavigate={setPage}
currentPage={paginatedPublicDashboards.page}
numberOfPages={paginatedPublicDashboards.totalPages}
hideWhenSinglePage
/>
</HorizontalGroup>
</div>
)}
</Page.Contents>
</Page>
);
@ -140,6 +164,7 @@ export const PublicDashboardListTable = () => {
const getStyles = (theme: GrafanaTheme2) => ({
list: css`
list-style-type: none;
margin-bottom: ${theme.spacing(2)};
`,
card: css`
${theme.breakpoints.up('sm')} {

View File

@ -18,10 +18,21 @@ export type DeleteDashboardResponse = {
title: string;
};
export interface ListPublicDashboardResponse {
export interface PublicDashboardListWithPaginationResponse {
publicDashboards: PublicDashboardListResponse[];
page: number;
perPage: number;
totalCount: number;
}
export interface PublicDashboardListResponse {
uid: string;
accessToken: string;
dashboardUid: string;
title: string;
isEnabled: boolean;
}
export interface PublicDashboardListWithPagination extends PublicDashboardListWithPaginationResponse {
totalPages: number;
}