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...) 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 ( var (
sql string sql string
params []interface{} params []interface{}
@ -68,7 +68,7 @@ func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, pe
recQryParams []interface{} recQryParams []interface{}
) )
if !ac.IsDisabled(sb.cfg) { 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() sql, params = filterRBAC.Where()
recQry, recQryParams = filterRBAC.With() recQry, recQryParams = filterRBAC.With()
} else { } else {

View File

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

View File

@ -167,7 +167,7 @@ func (ss *sqlStore) HandleAlertsQuery(ctx context.Context, query *alertmodels.Ge
builder.Write(")") builder.Write(")")
} }
builder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW) builder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW, "")
builder.Write(" ORDER BY name ASC") 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(getFromLibraryElementDTOWithMeta(store.GetDialect()))
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0") builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0")
writeParamSelectorSQL(&builder, params...) writeParamSelectorSQL(&builder, params...)
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW) builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "")
builder.Write(` OR dashboard.id=0`) builder.Write(` OR dashboard.id=0`)
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElements); err != nil { if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElements); err != nil {
return err return err
@ -372,7 +372,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
return err return err
} }
if signedInUser.OrgRole != org.RoleAdmin { if signedInUser.OrgRole != org.RoleAdmin {
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW) builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "")
} }
if query.SortDirection == search.SortAlphaDesc.Name { if query.SortDirection == search.SortAlphaDesc.Name {
builder.Write(" ORDER BY 1 DESC") 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(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id")
builder.Write(` WHERE lec.element_id=?`, element.ID) builder.Write(` WHERE lec.element_id=?`, element.ID)
if signedInUser.OrgRole != org.RoleAdmin { 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 { if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElementConnections); err != nil {
return err return err

View File

@ -90,7 +90,24 @@ func (api *Api) RegisterAPIEndpoints() {
// ListPublicDashboards Gets list of public dashboards by orgId // ListPublicDashboards Gets list of public dashboards by orgId
// GET /api/dashboards/public-dashboards // GET /api/dashboards/public-dashboards
func (api *Api) ListPublicDashboards(c *contextmodel.ReqContext) response.Response { 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 { if err != nil {
return response.Err(err) return response.Err(err)
} }

View File

@ -89,19 +89,21 @@ func TestAPIFeatureFlag(t *testing.T) {
} }
func TestAPIListPublicDashboard(t *testing.T) { func TestAPIListPublicDashboard(t *testing.T) {
successResp := []PublicDashboardListResponse{ successResp := &PublicDashboardListResponseWithPagination{
PublicDashboards: []*PublicDashboardListResponse{
{ {
Uid: "1234asdfasdf", Uid: "1234asdfasdf",
AccessToken: "asdfasdf", AccessToken: "asdfasdf",
DashboardUid: "abc1234", DashboardUid: "abc1234",
IsEnabled: true, IsEnabled: true,
}, },
},
} }
testCases := []struct { testCases := []struct {
Name string Name string
User *user.SignedInUser User *user.SignedInUser
Response []PublicDashboardListResponse Response *PublicDashboardListResponseWithPagination
ResponseErr error ResponseErr error
ExpectedHttpResponse int ExpectedHttpResponse int
}{ }{
@ -131,7 +133,7 @@ func TestAPIListPublicDashboard(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) { t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(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() Return(test.Response, test.ResponseErr).Maybe()
cfg := setting.NewCfg() cfg := setting.NewCfg()
@ -143,10 +145,10 @@ func TestAPIListPublicDashboard(t *testing.T) {
assert.Equal(t, test.ExpectedHttpResponse, response.Code) assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if test.ExpectedHttpResponse == http.StatusOK { if test.ExpectedHttpResponse == http.StatusOK {
var jsonResp []PublicDashboardListResponse var jsonResp PublicDashboardListResponseWithPagination
err := json.Unmarshal(response.Body.Bytes(), &jsonResp) err := json.Unmarshal(response.Body.Bytes(), &jsonResp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, jsonResp[0].Uid, "1234asdfasdf") assert.Equal(t, jsonResp.PublicDashboards[0].Uid, "1234asdfasdf")
} }
if test.ResponseErr != nil { if test.ResponseErr != nil {
@ -155,7 +157,7 @@ func TestAPIListPublicDashboard(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Internal server error", errResp.Message) assert.Equal(t, "Internal server error", errResp.Message)
assert.Equal(t, "publicdashboards.internalServerError", errResp.MessageID) 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() annotationsService := annotationstest.NewFakeAnnotationsRepo()
// create public dashboard // create public dashboard
store := publicdashboardsStore.ProvideStore(db) store := publicdashboardsStore.ProvideStore(db, db.Cfg, featuremgmt.WithFeatures())
cfg := setting.NewCfg() cfg := setting.NewCfg()
ac := acmock.New() ac := acmock.New()
ws := publicdashboardsService.ProvideServiceWrapper(store) ws := publicdashboardsService.ProvideServiceWrapper(store)

View File

@ -7,9 +7,13 @@ import (
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards" "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"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models" . "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/sqlstore" "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 // Define the storage implementation. We're generating the mock implementation
@ -17,6 +21,8 @@ import (
type PublicDashboardStoreImpl struct { type PublicDashboardStoreImpl struct {
sqlStore db.DB sqlStore db.DB
log log.Logger log log.Logger
cfg *setting.Cfg
features featuremgmt.FeatureToggles
} }
var LogPrefix = "publicdashboards.store" var LogPrefix = "publicdashboards.store"
@ -26,25 +32,54 @@ var LogPrefix = "publicdashboards.store"
var _ publicdashboards.Store = (*PublicDashboardStoreImpl)(nil) var _ publicdashboards.Store = (*PublicDashboardStoreImpl)(nil)
// Factory used by wire to dependency injection // 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{ return &PublicDashboardStoreImpl{
sqlStore: sqlStore, sqlStore: sqlStore,
log: log.New(LogPrefix), log: log.New(LogPrefix),
cfg: cfg,
features: features,
} }
} }
// FindAll Returns a list of public dashboards by orgId // FindAllWithPagination Returns a list of public dashboards by orgId, based on permissions and with pagination
func (d *PublicDashboardStoreImpl) FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) { func (d *PublicDashboardStoreImpl) FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error) {
resp := make([]PublicDashboardListResponse, 0) resp := &PublicDashboardListResponseWithPagination{
PublicDashboards: make([]*PublicDashboardListResponse, 0),
TotalCount: 0,
}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { recursiveQueriesAreSupported, err := d.sqlStore.RecursiveQueriesAreSupported()
sess.Table("dashboard_public").Select( if err != nil {
"dashboard_public.uid, dashboard_public.access_token, dashboard.uid as dashboard_uid, dashboard_public.is_enabled, dashboard.title"). return nil, err
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")
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 return err
}) })

View File

@ -2,18 +2,24 @@ package database
import ( import (
"context" "context"
"fmt"
"strings"
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models" . "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/publicdashboards/service" "github.com/grafana/grafana/pkg/services/publicdashboards/service"
"github.com/grafana/grafana/pkg/services/quota/quotatest" "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/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -34,38 +40,122 @@ func TestIntegrationListPublicDashboard(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping integration test") t.Skip("skipping integration test")
} }
sqlStore, cfg := db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
quotaService := quotatest.New(false, nil) var sqlStore *sqlstore.SQLStore
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) var cfg *setting.Cfg
require.NoError(t, err)
publicdashboardStore := ProvideStore(sqlStore) 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 var orgId int64 = 1
bDash := insertTestDashboard(t, dashboardStore, "b", orgId, 0, true) var publicdashboardStore *PublicDashboardStoreImpl
aDash := insertTestDashboard(t, dashboardStore, "a", orgId, 0, true)
cDash := insertTestDashboard(t, dashboardStore, "c", orgId, 0, true) 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 // these are in order of how they should be returned from ListPUblicDashboards
a := insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType) aPublicDash = insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType)
b := insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true, PublicShareType) bPublicDash = insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true, PublicShareType)
c := insertPublicDashboard(t, publicdashboardStore, cDash.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 t.Run("FindAllWithPagination will return dashboard list based on orgId with pagination", func(t *testing.T) {
// null in the exact opposite fashion and there is no shared syntax to sort setup()
// nulls in the same way in all 3 db's.
//d := insertPublicDashboard(t, publicdashboardStore, "missing", orgId, false)
// should not be included in response // should not be included in response
_ = insertPublicDashboard(t, publicdashboardStore, "wrongOrgId", 777, false, PublicShareType) _ = 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) require.NoError(t, err)
assert.Len(t, resp, 3) query := &PublicDashboardListQuery{
assert.Equal(t, resp[0].Uid, a.Uid) User: &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}},
assert.Equal(t, resp[1].Uid, b.Uid) OrgID: orgId,
assert.Equal(t, resp[2].Uid, c.Uid) 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) { 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) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
dashboardStore = store dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) 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) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
dashboardStore = store dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) 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) { 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) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
dashboardStore = store dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) 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) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
dashboardStore = store dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
} }
@ -319,7 +409,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) {
sqlStore, cfg = db.InitTestDBwithCfg(t) sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil)) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil))
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) 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) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
dashboardStore = store dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true) savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true)
insertPublicDashboard(t, publicdashboardStore, savedDashboard2.UID, savedDashboard2.OrgID, false, PublicShareType) insertPublicDashboard(t, publicdashboardStore, savedDashboard2.UID, savedDashboard2.OrgID, false, PublicShareType)
@ -468,7 +558,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
anotherSavedDashboard = insertTestDashboard(t, dashboardStore, "test another Dashie", 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) quotaService := quotatest.New(false, nil)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
} }
t.Run("GetOrgIdByAccessToken will OrgId when enabled", func(t *testing.T) { 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) sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil)) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil))
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, true, PublicShareType) 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) { t.Run("returns nil when dashboard is not a folder", func(t *testing.T) {
sqlStore, _ := db.InitTestDBwithCfg(t) sqlStore, _ := db.InitTestDBwithCfg(t)
dashboard := &dashboards.Dashboard{IsFolder: false} dashboard := &dashboards.Dashboard{IsFolder: false}
store := ProvideStore(sqlStore) store := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
pubdashes, err := store.FindByDashboardFolder(context.Background(), dashboard) pubdashes, err := store.FindByDashboardFolder(context.Background(), dashboard)
require.NoError(t, err) 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) { t.Run("returns nil when dashboard is nil", func(t *testing.T) {
sqlStore, _ := db.InitTestDBwithCfg(t) sqlStore, _ := db.InitTestDBwithCfg(t)
store := ProvideStore(sqlStore) store := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
pubdashes, err := store.FindByDashboardFolder(context.Background(), nil) pubdashes, err := store.FindByDashboardFolder(context.Background(), nil)
require.NoError(t, err) require.NoError(t, err)
@ -696,7 +786,7 @@ func TestGetDashboardByFolder(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
pubdashStore := ProvideStore(sqlStore) pubdashStore := ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
dashboard := insertTestDashboard(t, dashboardStore, "title", 1, 1, true, PublicShareType) dashboard := insertTestDashboard(t, dashboardStore, "title", 1, 1, true, PublicShareType)
pubdash := insertPublicDashboard(t, pubdashStore, dashboard.UID, dashboard.OrgID, true, PublicShareType) pubdash := insertPublicDashboard(t, pubdashStore, dashboard.UID, dashboard.OrgID, true, PublicShareType)
dashboard2 := insertTestDashboard(t, dashboardStore, "title", 1, 2, 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) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
dashboardStore = store dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore) publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures())
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, false) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, false)
savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, false) savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, false)
savedDashboard3 = insertTestDashboard(t, dashboardStore, "testDashie3", 2, 0, false) savedDashboard3 = insertTestDashboard(t, dashboardStore, "testDashie3", 2, 0, false)
@ -827,3 +917,50 @@ func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardSt
return pubdash 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" "time"
"github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/grafana/grafana/pkg/services/user"
) )
// PublicDashboardErr represents a dashboard error. // PublicDashboardErr represents a dashboard error.
@ -103,6 +104,22 @@ func (pd PublicDashboard) TableName() string {
return "dashboard_public" 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 { type PublicDashboardListResponse struct {
Uid string `json:"uid" xorm:"uid"` Uid string `json:"uid" xorm:"uid"`
AccessToken string `json:"accessToken" xorm:"access_token"` 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 return r0, r1
} }
// FindAll provides a mock function with given fields: ctx, u, orgId // FindAllWithPagination provides a mock function with given fields: ctx, query
func (_m *FakePublicDashboardService) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]models.PublicDashboardListResponse, error) { func (_m *FakePublicDashboardService) FindAllWithPagination(ctx context.Context, query *models.PublicDashboardListQuery) (*models.PublicDashboardListResponseWithPagination, error) {
ret := _m.Called(ctx, u, orgId) ret := _m.Called(ctx, query)
var r0 []models.PublicDashboardListResponse var r0 *models.PublicDashboardListResponseWithPagination
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64) []models.PublicDashboardListResponse); ok { if rf, ok := ret.Get(0).(func(context.Context, *models.PublicDashboardListQuery) *models.PublicDashboardListResponseWithPagination); ok {
r0 = rf(ctx, u, orgId) r0 = rf(ctx, query)
} else { } else {
if ret.Get(0) != nil { if ret.Get(0) != nil {
r0 = ret.Get(0).([]models.PublicDashboardListResponse) r0 = ret.Get(0).(*models.PublicDashboardListResponseWithPagination)
} }
} }
var r1 error var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64) error); ok { if rf, ok := ret.Get(1).(func(context.Context, *models.PublicDashboardListQuery) error); ok {
r1 = rf(ctx, u, orgId) r1 = rf(ctx, query)
} else { } else {
r1 = ret.Error(1) r1 = ret.Error(1)
} }

View File

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

View File

@ -21,7 +21,7 @@ type Service interface {
FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error)
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, 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) Find(ctx context.Context, uid string) (*PublicDashboard, error)
Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
Update(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) FindByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, error)
FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, 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) Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error)
Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error)
Delete(ctx context.Context, uid string) (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) sqlStore := sqlstore.InitTestDB(t)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
fakeQueryService := &query.FakeQueryService{} fakeQueryService := &query.FakeQueryService{}
fakeQueryService.On("QueryData", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&backend.QueryDataResponse{}, nil) 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) sqlStore := db.InitTestDB(t)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err) 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) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
publicDashboard := &PublicDashboard{ publicDashboard := &PublicDashboard{
Uid: "1", Uid: "1",
@ -1194,7 +1194,7 @@ func TestBuildMetricRequest(t *testing.T) {
sqlStore := db.InitTestDB(t) sqlStore := db.InitTestDB(t)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 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") return "", ErrInternalServerError.Errorf("failed to generate a unique accessToken for public dashboard")
} }
// FindAll Returns a list of public dashboards by orgId // FindAllWithPagination Returns a list of public dashboards by orgId, based on permissions and with pagination
func (pd *PublicDashboardServiceImpl) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) { func (pd *PublicDashboardServiceImpl) FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error) {
publicDashboards, err := pd.store.FindAll(ctx, orgId) query.Offset = query.Limit * (query.Page - 1)
resp, err := pd.store.FindAllWithPagination(ctx, query)
if err != nil { 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) { 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 // Checks to see if PublicDashboard.ExistsEnabledByDashboardUid is true on create or changed on update
func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdash *PublicDashboard) bool { func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdash *PublicDashboard) bool {
// creating dashboard, enabled true // creating dashboard, enabled true

View File

@ -195,7 +195,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err) 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) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -285,7 +285,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err) 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) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -325,7 +325,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err) 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) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -359,7 +359,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
templateVars := make([]map[string]interface{}, 1) templateVars := make([]map[string]interface{}, 1)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, templateVars, nil) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, templateVars, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -474,7 +474,7 @@ func TestCreatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err) 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) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
@ -519,7 +519,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) 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) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) 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) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err) require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures())
serviceWrapper := ProvideServiceWrapper(publicdashboardStore) serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) 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) { func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
type args struct { type args struct {
ctx context.Context ctx context.Context
u *user.SignedInUser query *PublicDashboardListQuery
orgId int64 }
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 { testCases := []struct {
name string name string
args args args args
evaluateFunc func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error) want *PublicDashboardListResponseWithPagination
want []PublicDashboardListResponse mockResponse *mockResponse
wantErr assert.ErrorAssertionFunc 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{ args: args{
ctx: context.Background(), ctx: context.Background(),
u: &user.SignedInUser{OrgID: 1}, query: &PublicDashboardListQuery{
orgId: 1, User: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
},
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{
1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}}, 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, wantErr: assert.NoError,
}, },
{ {
name: "should return orphaned public dashboards", name: "should return error when store returns error",
args: args{ args: args{
ctx: context.Background(), 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"}}}, 1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}},
}, },
orgId: 1, OrgID: 1,
}, Page: 1,
evaluateFunc: func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error) { Limit: 50,
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,
}, },
}, },
wantErr: assert.NoError, mockResponse: &mockResponse{
}, PublicDashboardListResponseWithPagination: nil,
{ Err: errors.New("an err"),
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
}, },
want: nil, want: nil,
wantErr: assert.Error, 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, ac := tests.SetupMockAccesscontrol(t,
func(c context.Context, siu *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) { func(c context.Context, siu *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) {
return []accesscontrol.Permission{}, nil return []accesscontrol.Permission{}, nil
@ -1102,21 +1018,23 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
false, 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{ pd := &PublicDashboardServiceImpl{
log: log.New("test.logger"), log: log.New("test.logger"),
store: store, store: store,
ac: ac, ac: ac,
} }
for _, tt := range testCases { got, err := pd.FindAllWithPagination(tt.args.ctx, tt.args.query)
t.Run(tt.name, func(t *testing.T) { if !tt.wantErr(t, err, fmt.Sprintf("FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query)) {
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)) {
return 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, SessionUser,
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { DashboardModel } from 'app/features/dashboard/state'; 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 = { type ReqOptions = {
manageError?: (err: unknown) => { error: unknown }; manageError?: (err: unknown) => { error: unknown };
@ -137,9 +140,13 @@ export const publicDashboardApi = createApi({
}), }),
providesTags: (result, _, email) => [{ type: 'ActiveUserDashboards', id: email }], providesTags: (result, _, email) => [{ type: 'ActiveUserDashboards', id: email }],
}), }),
listPublicDashboards: builder.query<ListPublicDashboardResponse[], void>({ listPublicDashboards: builder.query<PublicDashboardListWithPagination, number | void>({
query: () => ({ query: (page = 1) => ({
url: '/dashboards/public-dashboards', url: `/dashboards/public-dashboards?page=${page}&perpage=8`,
}),
transformResponse: (response: PublicDashboardListWithPaginationResponse) => ({
...response,
totalPages: Math.ceil(response.totalCount / response.perPage),
}), }),
providesTags: ['AuditTablePublicDashboard'], 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 { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { ListPublicDashboardResponse } from '../../types'; import { PublicDashboardListResponse, PublicDashboardListWithPaginationResponse } from '../../types';
import { PublicDashboardListTable } from './PublicDashboardListTable'; import { PublicDashboardListTable } from './PublicDashboardListTable';
const publicDashboardListResponse: ListPublicDashboardResponse[] = [ const publicDashboardListResponse: PublicDashboardListResponse[] = [
{ {
uid: 'SdZwuCZVz', uid: 'SdZwuCZVz',
accessToken: 'beeaf92f6ab3467f80b2be922c7741ab', accessToken: 'beeaf92f6ab3467f80b2be922c7741ab',
@ -32,7 +32,7 @@ const publicDashboardListResponse: ListPublicDashboardResponse[] = [
}, },
]; ];
const orphanedDashboardListResponse: ListPublicDashboardResponse[] = [ const orphanedDashboardListResponse: PublicDashboardListResponse[] = [
{ {
uid: 'SdZwuCZVz2', uid: 'SdZwuCZVz2',
accessToken: 'beeaf92f6ab3467f80b2be922c7741ab', accessToken: 'beeaf92f6ab3467f80b2be922c7741ab',
@ -49,9 +49,15 @@ const orphanedDashboardListResponse: ListPublicDashboardResponse[] = [
}, },
]; ];
const paginationResponse: Omit<PublicDashboardListWithPaginationResponse, 'publicDashboards'> = {
page: 1,
perPage: 50,
totalCount: 50,
};
const server = setupServer( const server = setupServer(
rest.get('/api/dashboards/public-dashboards', (_, res, ctx) => 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))) 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); expect(screen.getAllByRole('listitem')).toHaveLength(publicDashboardListResponse.length);
}); });
it('renders empty list', async () => { it('renders empty list', async () => {
const emptyListRS: PublicDashboardListWithPaginationResponse = {
publicDashboards: [],
totalCount: 0,
page: 1,
perPage: 50,
};
server.use( server.use(
rest.get('/api/dashboards/public-dashboards', (req, res, ctx) => { 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', () => { describe('Orphaned public dashboard', () => {
it('renders orphaned and non orphaned public dashboards items correctly', async () => { it('renders orphaned and non orphaned public dashboards items correctly', async () => {
const response = [...publicDashboardListResponse, ...orphanedDashboardListResponse]; const response: PublicDashboardListWithPaginationResponse = {
...paginationResponse,
publicDashboards: [...publicDashboardListResponse, ...orphanedDashboardListResponse],
};
server.use( server.use(
rest.get('/api/dashboards/public-dashboards', (req, res, ctx) => { rest.get('/api/dashboards/public-dashboards', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(response)); return res(ctx.status(200), ctx.json(response));
@ -158,13 +174,13 @@ describe('Orphaned public dashboard', () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
await renderPublicDashboardTable(true); await renderPublicDashboardTable(true);
response.forEach((pd, idx) => { response.publicDashboards.forEach((pd, idx) => {
renderPublicDashboardItemCorrectly(pd, idx, true); 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 isOrphaned = !pd.dashboardUid;
const cardItems = screen.getAllByRole('listitem'); const cardItems = screen.getAllByRole('listitem');

View File

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

View File

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