PublicDashboards: filter by permissions on audit list (#57228)

This commit is contained in:
Ezequiel Victorero 2022-10-19 17:24:00 -03:00 committed by GitHub
parent d81a3e524d
commit 3e6bdf0439
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 277 additions and 26 deletions

View File

@ -118,7 +118,7 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
// Gets list of public dashboards for an org
// GET /api/dashboards/public
func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
resp, err := api.PublicDashboardService.ListPublicDashboards(c.Req.Context(), c.OrgID)
resp, err := api.PublicDashboardService.ListPublicDashboards(c.Req.Context(), c.SignedInUser, c.OrgID)
if err != nil {
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "ListPublicDashboards: failed to list public dashboards", err)
}

View File

@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
@ -189,7 +190,7 @@ func TestAPIListPublicDashboard(t *testing.T) {
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("ListPublicDashboards", mock.Anything, mock.Anything).
service.On("ListPublicDashboards", mock.Anything, mock.Anything, mock.Anything).
Return(test.Response, test.ResponseErr).Maybe()
cfg := setting.NewCfg()
@ -686,8 +687,9 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
// create public dashboard
store := publicdashboardsStore.ProvideStore(db)
cfg := setting.NewCfg()
ac := acmock.New()
cfg.RBACEnabled = false
service := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService)
service := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService, ac)
pubdash, err := service.SavePublicDashboardConfig(context.Background(), &user.SignedInUser{}, savePubDashboardCmd)
require.NoError(t, err)

View File

@ -44,7 +44,7 @@ func (d *PublicDashboardStoreImpl) ListPublicDashboards(ctx context.Context, org
Join("LEFT", "dashboard", "dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id").
Cols("dashboard_public.uid", "dashboard_public.access_token", "dashboard_public.dashboard_uid", "dashboard_public.is_enabled", "dashboard.title").
Where("dashboard_public.org_id = ?", orgId).
OrderBy("is_enabled DESC, dashboard.title ASC")
OrderBy(" is_enabled DESC, dashboard.title IS NULL, dashboard.title ASC")
err := sess.Find(&resp)
return err

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.12.1. DO NOT EDIT.
// Code generated by mockery v2.14.0. DO NOT EDIT.
package publicdashboards
@ -15,8 +15,6 @@ import (
publicdashboardsmodels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
testing "testing"
user "github.com/grafana/grafana/pkg/services/user"
)
@ -228,13 +226,13 @@ func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context,
return r0, r1
}
// ListPublicDashboards provides a mock function with given fields: ctx, orgId
func (_m *FakePublicDashboardService) ListPublicDashboards(ctx context.Context, orgId int64) ([]publicdashboardsmodels.PublicDashboardListResponse, error) {
ret := _m.Called(ctx, orgId)
// ListPublicDashboards provides a mock function with given fields: ctx, u, orgId
func (_m *FakePublicDashboardService) ListPublicDashboards(ctx context.Context, u *user.SignedInUser, orgId int64) ([]publicdashboardsmodels.PublicDashboardListResponse, error) {
ret := _m.Called(ctx, u, orgId)
var r0 []publicdashboardsmodels.PublicDashboardListResponse
if rf, ok := ret.Get(0).(func(context.Context, int64) []publicdashboardsmodels.PublicDashboardListResponse); ok {
r0 = rf(ctx, orgId)
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64) []publicdashboardsmodels.PublicDashboardListResponse); ok {
r0 = rf(ctx, u, orgId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]publicdashboardsmodels.PublicDashboardListResponse)
@ -242,8 +240,8 @@ func (_m *FakePublicDashboardService) ListPublicDashboards(ctx context.Context,
}
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, *user.SignedInUser, int64) error); ok {
r1 = rf(ctx, u, orgId)
} else {
r1 = ret.Error(1)
}
@ -295,8 +293,13 @@ func (_m *FakePublicDashboardService) SavePublicDashboardConfig(ctx context.Cont
return r0, r1
}
// NewFakePublicDashboardService creates a new instance of FakePublicDashboardService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewFakePublicDashboardService(t testing.TB) *FakePublicDashboardService {
type mockConstructorTestingTNewFakePublicDashboardService interface {
mock.TestingT
Cleanup(func())
}
// NewFakePublicDashboardService creates a new instance of FakePublicDashboardService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewFakePublicDashboardService(t mockConstructorTestingTNewFakePublicDashboardService) *FakePublicDashboardService {
mock := &FakePublicDashboardService{}
mock.Mock.Test(t)

View File

@ -23,7 +23,7 @@ type Service interface {
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error)
GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error)
ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error)
ListPublicDashboards(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error)
PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error)
SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error)
}

View File

@ -2,6 +2,7 @@ package service
import (
"context"
"errors"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@ -33,6 +34,7 @@ type PublicDashboardServiceImpl struct {
intervalCalculator intervalv2.Calculator
QueryDataService *query.Service
AnnotationsRepo annotations.Repository
ac accesscontrol.AccessControl
}
var LogPrefix = "publicdashboards.service"
@ -48,6 +50,7 @@ func ProvideService(
store publicdashboards.Store,
qds *query.Service,
anno annotations.Repository,
ac accesscontrol.AccessControl,
) *PublicDashboardServiceImpl {
return &PublicDashboardServiceImpl{
log: log.New(LogPrefix),
@ -56,14 +59,10 @@ func ProvideService(
intervalCalculator: intervalv2.NewCalculator(),
QueryDataService: qds,
AnnotationsRepo: anno,
ac: ac,
}
}
// Gets a list of public dashboards by orgId
func (pd *PublicDashboardServiceImpl) ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) {
return pd.store.ListPublicDashboards(ctx, orgId)
}
// Gets a dashboard by Uid
func (pd *PublicDashboardServiceImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
dashboard, err := pd.store.GetDashboard(ctx, dashboardUid)
@ -392,6 +391,16 @@ func (pd *PublicDashboardServiceImpl) BuildAnonymousUser(ctx context.Context, da
return anonymousUser
}
// Gets a list of public dashboards by orgId
func (pd *PublicDashboardServiceImpl) ListPublicDashboards(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) {
publicDashboards, err := pd.store.ListPublicDashboards(ctx, orgId)
if err != nil {
return nil, err
}
return pd.filterDashboardsByPermissions(ctx, u, publicDashboards)
}
func (pd *PublicDashboardServiceImpl) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) {
return pd.store.PublicDashboardEnabled(ctx, dashboardUid)
}
@ -444,7 +453,26 @@ func (pd *PublicDashboardServiceImpl) logIsEnabledChanged(existingPubdash *Publi
}
}
// Checks to see if PublicDashboard.Isenabled is true on create or changed on update
// 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, 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.IsEnabled is true on create or changed on update
func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdash *PublicDashboard) bool {
// creating dashboard, enabled true
newDashCreated := existingPubdash == nil && newPubdash.IsEnabled

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"testing"
"time"
@ -17,14 +18,17 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
"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"
"github.com/grafana/grafana/pkg/services/publicdashboards/database"
"github.com/grafana/grafana/pkg/services/publicdashboards/internal"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
@ -1190,3 +1194,212 @@ func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations
return dash
}
func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
type args struct {
ctx context.Context
u *user.SignedInUser
orgId int64
}
testCases := []struct {
name string
args args
evaluateFunc func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error)
want []PublicDashboardListResponse
wantErr assert.ErrorAssertionFunc
}{
{
name: "should return empty list when user does not have permissions to read any dashboard",
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{
1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}},
},
orgId: 1,
},
want: []PublicDashboardListResponse{
{
Uid: "0GwW7mgVk",
AccessToken: "0b458cb7fe7f42c68712078bcacee6e3",
DashboardUid: "0S6TmO67z",
Title: "my zero dashboard",
IsEnabled: true,
},
},
wantErr: assert.NoError,
},
{
name: "should return orphaned public dashboards",
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.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,
},
{
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,
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("ListPublicDashboards", 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
},
false,
)
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.ListPublicDashboards(tt.args.ctx, tt.args.u, tt.args.orgId)
if !tt.wantErr(t, err, fmt.Sprintf("ListPublicDashboards(%v, %v, %v)", tt.args.ctx, tt.args.u, tt.args.orgId)) {
return
}
assert.Equalf(t, tt.want, got, "ListPublicDashboards(%v, %v, %v)", tt.args.ctx, tt.args.u, tt.args.orgId)
})
}
}

View File

@ -38,8 +38,10 @@ export const ListPublicDashboardTable = () => {
<table className="filter-table">
<thead>
<tr>
<th>Dashboard</th>
<th>Public dashboard enabled</th>
<th>Name</th>
<th>Status</th>
<th>Public URL</th>
<th>Configuration</th>
<th></th>
</tr>
</thead>
@ -65,7 +67,10 @@ export const ListPublicDashboardTable = () => {
>
<Icon name="external-link-alt" />
</LinkButton>
</ButtonGroup>
</td>
<td>
<ButtonGroup>
<LinkButton
fill="text"
href={`/d/${pd.dashboardUid}?shareView=share`}