Guardian: Introduce additional constructors (#59577)

* Guardian: Use dashboard UID instead of ID

* Apply suggestions from code review

Introduce several guardian constructors and each time use
the most appropriate one.
This commit is contained in:
Sofia Papagiannaki
2022-12-15 16:34:17 +02:00
committed by GitHub
parent 6478d0a5ef
commit 11d8bcbea9
30 changed files with 678 additions and 193 deletions
@@ -57,7 +57,10 @@ func (c *PermissionChecker) CheckReadPermissions(ctx context.Context, orgId int6
if err != nil {
return false, err
}
guard := guardian.New(ctx, dash.Id, orgId, signedInUser)
guard, err := guardian.NewByDashboard(ctx, dash, orgId, signedInUser)
if err != nil {
return false, err
}
if ok, err := guard.CanView(); err != nil || !ok {
return false, nil
}
@@ -81,7 +84,10 @@ func (c *PermissionChecker) CheckReadPermissions(ctx context.Context, orgId int6
if err != nil {
return false, err
}
guard := guardian.New(ctx, dash.Id, orgId, signedInUser)
guard, err := guardian.NewByDashboard(ctx, dash, orgId, signedInUser)
if err != nil {
return false, err
}
if ok, err := guard.CanView(); err != nil || !ok {
return false, nil
}
@@ -103,7 +109,10 @@ func (c *PermissionChecker) CheckWritePermissions(ctx context.Context, orgId int
if err != nil {
return false, err
}
guard := guardian.New(ctx, dash.Id, orgId, signedInUser)
guard, err := guardian.NewByDashboard(ctx, dash, orgId, signedInUser)
if err != nil {
return false, err
}
if ok, err := guard.CanEdit(); err != nil || !ok {
return false, nil
}
@@ -133,7 +142,10 @@ func (c *PermissionChecker) CheckWritePermissions(ctx context.Context, orgId int
if err != nil {
return false, nil
}
guard := guardian.New(ctx, dash.Id, orgId, signedInUser)
guard, err := guardian.NewByDashboard(ctx, dash, orgId, signedInUser)
if err != nil {
return false, err
}
if ok, err := guard.CanEdit(); err != nil || !ok {
return false, nil
}
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -120,7 +121,10 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
if isParentFolderChanged {
// Check that the user is allowed to add a dashboard to the folder
guardian := guardian.New(ctx, dash.Id, dto.OrgId, dto.User)
guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgId, dto.User)
if err != nil {
return nil, err
}
if canSave, err := guardian.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canSave {
if err != nil {
return nil, err
@@ -140,7 +144,11 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
}
}
guard := guardian.New(ctx, dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
guard, err := getGuardianForSavePermissionCheck(ctx, dash, dto.User)
if err != nil {
return nil, err
}
if dash.Id == 0 {
if canCreate, err := guard.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canCreate {
if err != nil {
@@ -183,6 +191,26 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.
return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd)
}
// getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard
// It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck()
func getGuardianForSavePermissionCheck(ctx context.Context, d *models.Dashboard, user *user.SignedInUser) (guardian.DashboardGuardian, error) {
newDashboard := d.Id == 0
if newDashboard {
// if it's a new dashboard/folder check the parent folder permissions
guard, err := guardian.New(ctx, d.FolderId, d.OrgId, user)
if err != nil {
return nil, err
}
return guard, nil
}
guard, err := guardian.NewByDashboard(ctx, d, d.OrgId, user)
if err != nil {
return nil, err
}
return guard, nil
}
func validateDashboardRefreshInterval(dash *models.Dashboard) error {
if setting.MinRefreshInterval == "" {
return nil
@@ -108,7 +108,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sqlStore)
assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId)
assert.Equal(t, "", sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -128,7 +128,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -148,7 +148,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -169,7 +169,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -190,7 +190,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -211,7 +211,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -232,7 +232,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -253,7 +253,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -274,7 +274,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
@@ -295,7 +295,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID)
})
+34 -5
View File
@@ -104,7 +104,14 @@ func (s *Service) Get(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.
return nil, err
}
g := guardian.New(ctx, f.ID, f.OrgID, cmd.SignedInUser)
// do not get guardian by the folder ID because it differs from the nested folder ID
// and the legacy folder ID has been associated with the permissions:
// use the folde UID instead that is the same for both
g, err := guardian.NewByUID(ctx, f.UID, f.OrgID, cmd.SignedInUser)
if err != nil {
return nil, err
}
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
@@ -166,7 +173,14 @@ func (s *Service) getFolderByID(ctx context.Context, user *user.SignedInUser, id
return nil, err
}
g := guardian.New(ctx, dashFolder.ID, orgID, user)
// do not get guardian by the folder ID because it differs from the nested folder ID
// and the legacy folder ID has been associated with the permissions:
// use the folde UID instead that is the same for both
g, err := guardian.NewByUID(ctx, dashFolder.UID, orgID, user)
if err != nil {
return nil, err
}
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
@@ -183,7 +197,14 @@ func (s *Service) getFolderByUID(ctx context.Context, user *user.SignedInUser, o
return nil, err
}
g := guardian.New(ctx, dashFolder.ID, orgID, user)
// do not get guardian by the folder ID because it differs from the nested folder ID
// and the legacy folder ID has been associated with the permissions:
// use the folde UID instead that is the same for both
g, err := guardian.NewByUID(ctx, dashFolder.UID, orgID, user)
if err != nil {
return nil, err
}
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
@@ -200,7 +221,11 @@ func (s *Service) getFolderByTitle(ctx context.Context, user *user.SignedInUser,
return nil, err
}
g := guardian.New(ctx, dashFolder.ID, orgID, user)
g, err := guardian.NewByUID(ctx, dashFolder.UID, orgID, user)
if err != nil {
return nil, err
}
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
@@ -426,7 +451,11 @@ func (s *Service) DeleteFolder(ctx context.Context, cmd *folder.DeleteFolderComm
return err
}
guard := guardian.New(ctx, dashFolder.ID, cmd.OrgID, cmd.SignedInUser)
guard, err := guardian.NewByUID(ctx, dashFolder.UID, cmd.OrgID, cmd.SignedInUser)
if err != nil {
return err
}
if canSave, err := guard.CanDelete(); err != nil || !canSave {
if err != nil {
return toFolderError(err)
+95 -28
View File
@@ -2,6 +2,7 @@ package guardian
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
@@ -21,30 +22,106 @@ var permissionMap = map[string]models.PermissionType{
var _ DashboardGuardian = new(AccessControlDashboardGuardian)
// NewAccessControlDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboardId.
func NewAccessControlDashboardGuardian(
ctx context.Context, dashboardId int64, user *user.SignedInUser,
store db.DB, ac accesscontrol.AccessControl,
folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService,
dashboardService dashboards.DashboardService,
) *AccessControlDashboardGuardian {
) (*AccessControlDashboardGuardian, error) {
var dashboard *models.Dashboard
if dashboardId != 0 {
q := &models.GetDashboardQuery{
Id: dashboardId,
OrgId: user.OrgID,
}
if err := dashboardService.GetDashboard(ctx, q); err != nil {
if errors.Is(err, dashboards.ErrDashboardNotFound) {
return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err)
}
return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err)
}
dashboard = q.Result
}
return &AccessControlDashboardGuardian{
ctx: ctx,
log: log.New("dashboard.permissions"),
dashboardID: dashboardId,
dashboard: dashboard,
user: user,
store: store,
ac: ac,
folderPermissionsService: folderPermissionsService,
dashboardPermissionsService: dashboardPermissionsService,
dashboardService: dashboardService,
}, nil
}
// NewAccessControlDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboardUID.
func NewAccessControlDashboardGuardianByUID(
ctx context.Context, dashboardUID string, user *user.SignedInUser,
store db.DB, ac accesscontrol.AccessControl,
folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService,
dashboardService dashboards.DashboardService,
) (*AccessControlDashboardGuardian, error) {
var dashboard *models.Dashboard
if dashboardUID != "" {
q := &models.GetDashboardQuery{
Uid: dashboardUID,
OrgId: user.OrgID,
}
if err := dashboardService.GetDashboard(ctx, q); err != nil {
if errors.Is(err, dashboards.ErrDashboardNotFound) {
return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err)
}
return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err)
}
dashboard = q.Result
}
return &AccessControlDashboardGuardian{
ctx: ctx,
log: log.New("dashboard.permissions"),
dashboard: dashboard,
user: user,
store: store,
ac: ac,
folderPermissionsService: folderPermissionsService,
dashboardPermissionsService: dashboardPermissionsService,
dashboardService: dashboardService,
}, nil
}
// NewAccessControlDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboard.
// This constructor should be preferred over the other two if the dashboard in available
// since it avoids querying the database for fetching the dashboard.
func NewAccessControlDashboardGuardianByDashboard(
ctx context.Context, dashboard *models.Dashboard, user *user.SignedInUser,
store db.DB, ac accesscontrol.AccessControl,
folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService,
dashboardService dashboards.DashboardService,
) (*AccessControlDashboardGuardian, error) {
return &AccessControlDashboardGuardian{
ctx: ctx,
log: log.New("dashboard.permissions"),
dashboard: dashboard,
user: user,
store: store,
ac: ac,
folderPermissionsService: folderPermissionsService,
dashboardPermissionsService: dashboardPermissionsService,
dashboardService: dashboardService,
}, nil
}
type AccessControlDashboardGuardian struct {
ctx context.Context
log log.Logger
dashboardID int64
dashboard *models.Dashboard
user *user.SignedInUser
store db.DB
@@ -55,8 +132,8 @@ type AccessControlDashboardGuardian struct {
}
func (a *AccessControlDashboardGuardian) CanSave() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
if a.dashboard == nil {
return false, ErrGuardianDashboardNotFound
}
if a.dashboard.IsFolder {
@@ -69,9 +146,10 @@ func (a *AccessControlDashboardGuardian) CanSave() (bool, error) {
}
func (a *AccessControlDashboardGuardian) CanEdit() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
if a.dashboard == nil {
return false, ErrGuardianDashboardNotFound
}
if setting.ViewersCanEdit {
return a.CanView()
}
@@ -86,8 +164,8 @@ func (a *AccessControlDashboardGuardian) CanEdit() (bool, error) {
}
func (a *AccessControlDashboardGuardian) CanView() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
if a.dashboard == nil {
return false, ErrGuardianDashboardNotFound
}
if a.dashboard.IsFolder {
@@ -100,8 +178,8 @@ func (a *AccessControlDashboardGuardian) CanView() (bool, error) {
}
func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
if a.dashboard == nil {
return false, ErrGuardianDashboardNotFound
}
if a.dashboard.IsFolder {
@@ -118,8 +196,8 @@ func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) {
}
func (a *AccessControlDashboardGuardian) CanDelete() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
if a.dashboard == nil {
return false, ErrGuardianDashboardNotFound
}
if a.dashboard.IsFolder {
@@ -145,11 +223,11 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool
func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) {
ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator)
if err != nil {
a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserID, "id", a.dashboardID)
a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserID, "id", a.dashboard.Id)
}
if !ok && err == nil {
a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserID, "id", a.dashboardID, "permissions", evaluator.GoString())
a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserID, "id", a.dashboard.Id, "permissions", evaluator.GoString())
}
return ok, err
@@ -162,8 +240,8 @@ func (a *AccessControlDashboardGuardian) CheckPermissionBeforeUpdate(permission
// GetACL translate access control permissions to dashboard acl info
func (a *AccessControlDashboardGuardian) GetACL() ([]*models.DashboardACLInfoDTO, error) {
if err := a.loadDashboard(); err != nil {
return nil, err
if a.dashboard == nil {
return nil, ErrGuardianGetDashboardFailure
}
var svc accesscontrol.PermissionsService
@@ -254,17 +332,6 @@ func (a *AccessControlDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*mode
return hiddenACL, nil
}
func (a *AccessControlDashboardGuardian) loadDashboard() error {
if a.dashboard == nil {
query := &models.GetDashboardQuery{Id: a.dashboardID, OrgId: a.user.OrgID}
if err := a.dashboardService.GetDashboard(a.ctx, query); err != nil {
return err
}
a.dashboard = query.Result
}
return nil
}
func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*models.Dashboard, error) {
if folderID == 0 {
return &models.Dashboard{Uid: accesscontrol.GeneralFolderUID}, nil
@@ -616,9 +616,21 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce
setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac, teamSvc, userSvc)
require.NoError(t, err)
if dashboardSvc == nil {
dashboardSvc = &dashboards.FakeDashboardService{}
fakeDashboardService := dashboards.NewFakeDashboardService(t)
fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{
Id: q.Id,
Uid: q.Uid,
OrgId: q.OrgId,
}
}).Return(nil)
dashboardSvc = fakeDashboardService
}
return NewAccessControlDashboardGuardian(context.Background(), dash.Id, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc), dash
g, err := NewAccessControlDashboardGuardian(context.Background(), dash.Id, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc)
require.NoError(t, err)
return g, dash
}
func testDashSvc(t *testing.T) dashboards.DashboardService {
+100 -8
View File
@@ -12,11 +12,14 @@ import (
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
)
var (
ErrGuardianPermissionExists = errors.New("permission already exists")
ErrGuardianOverride = errors.New("you can only override a permission to be higher")
ErrGuardianPermissionExists = errors.New("permission already exists")
ErrGuardianOverride = errors.New("you can only override a permission to be higher")
ErrGuardianGetDashboardFailure = errutil.NewBase(errutil.StatusInternal, "guardian.getDashboardFailure", errutil.WithPublicMessage("Failed to get dashboard"))
ErrGuardianDashboardNotFound = errutil.NewBase(errutil.StatusNotFound, "guardian.dashboardNotFound")
)
// DashboardGuardian to be used for guard against operations without access on dashboard and acl
@@ -54,11 +57,38 @@ type dashboardGuardianImpl struct {
// New factory for creating a new dashboard guardian instance
// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned
var New = func(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser) DashboardGuardian {
var New = func(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
panic("no guardian factory implementation provided")
}
func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) *dashboardGuardianImpl {
// NewByUID factory for creating a new dashboard guardian instance
// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned
var NewByUID = func(ctx context.Context, dashUID string, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
panic("no guardian factory implementation provided")
}
// NewByDashboard factory for creating a new dashboard guardian instance
// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned
var NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
panic("no guardian factory implementation provided")
}
// newDashboardGuardian creates a dashboard guardian by the provided dashId.
func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) {
if dashId != 0 {
q := &models.GetDashboardQuery{
Id: dashId,
OrgId: orgId,
}
if err := dashSvc.GetDashboard(ctx, q); err != nil {
if errors.Is(err, dashboards.ErrDashboardNotFound) {
return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err)
}
return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err)
}
}
return &dashboardGuardianImpl{
user: user,
dashId: dashId,
@@ -68,7 +98,53 @@ func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user *
store: store,
dashboardService: dashSvc,
teamService: teamSvc,
}, nil
}
// newDashboardGuardianByUID creates a dashboard guardian by the provided dashUID.
func newDashboardGuardianByUID(ctx context.Context, dashUID string, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) {
dashID := int64(0)
if dashUID != "" {
q := &models.GetDashboardQuery{
Uid: dashUID,
OrgId: orgId,
}
if err := dashSvc.GetDashboard(ctx, q); err != nil {
if errors.Is(err, dashboards.ErrDashboardNotFound) {
return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err)
}
return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err)
}
dashID = q.Result.Id
}
return &dashboardGuardianImpl{
user: user,
dashId: dashID,
orgId: orgId,
log: log.New("dashboard.permissions"),
ctx: ctx,
store: store,
dashboardService: dashSvc,
teamService: teamSvc,
}, nil
}
// newDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboard.
// This constructor should be preferred over the other two if the dashboard in available
// since it avoids querying the database for fetching the dashboard.
func newDashboardGuardianByDashboard(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) {
return &dashboardGuardianImpl{
user: user,
dashId: dash.Id,
orgId: orgId,
log: log.New("dashboard.permissions"),
ctx: ctx,
store: store,
dashboardService: dashSvc,
teamService: teamSvc,
}, nil
}
func (g *dashboardGuardianImpl) CanSave() (bool, error) {
@@ -319,7 +395,8 @@ func (g *dashboardGuardianImpl) GetHiddenACL(cfg *setting.Cfg) ([]*models.Dashbo
// nolint:unused
type FakeDashboardGuardian struct {
DashId int64
DashID int64
DashUID string
OrgId int64
User *user.SignedInUser
CanSaveValue bool
@@ -379,10 +456,25 @@ func (g *FakeDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*models.Dashbo
// nolint:unused
func MockDashboardGuardian(mock *FakeDashboardGuardian) {
New = func(_ context.Context, dashId int64, orgId int64, user *user.SignedInUser) DashboardGuardian {
New = func(_ context.Context, dashID int64, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
mock.OrgId = orgId
mock.DashId = dashId
mock.DashID = dashID
mock.User = user
return mock
return mock, nil
}
NewByUID = func(_ context.Context, dashUID string, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
mock.OrgId = orgId
mock.DashUID = dashUID
mock.User = user
return mock, nil
}
NewByDashboard = func(_ context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
mock.OrgId = orgId
mock.DashUID = dash.Uid
mock.DashID = dash.Id
mock.User = user
return mock, nil
}
}
+31 -3
View File
@@ -23,6 +23,7 @@ const (
orgID = int64(1)
defaultDashboardID = int64(-1)
dashboardID = int64(1)
dashboardUID = "uid"
parentFolderID = int64(2)
childDashboardID = int64(3)
userID = int64(1)
@@ -697,6 +698,14 @@ func TestGuardianGetHiddenACL(t *testing.T) {
{Inherited: true, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW},
}
}).Return(nil)
dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{
Id: q.Id,
Uid: q.Uid,
OrgId: q.OrgId,
}
}).Return(nil)
cfg := setting.NewCfg()
cfg.HiddenUsers = map[string]struct{}{"user2": {}}
@@ -707,7 +716,8 @@ func TestGuardianGetHiddenACL(t *testing.T) {
UserID: 1,
Login: "user1",
}
g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{})
g, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{})
require.NoError(t, err)
hiddenACL, err := g.GetHiddenACL(cfg)
require.NoError(t, err)
@@ -723,7 +733,16 @@ func TestGuardianGetHiddenACL(t *testing.T) {
Login: "user1",
IsGrafanaAdmin: true,
}
g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, &dashboards.FakeDashboardService{}, &teamtest.FakeService{})
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{
Id: q.Id,
Uid: q.Uid,
}
}).Return(nil)
g, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{})
require.NoError(t, err)
hiddenACL, err := g.GetHiddenACL(cfg)
require.NoError(t, err)
@@ -750,6 +769,14 @@ func TestGuardianGetACLWithoutDuplicates(t *testing.T) {
{Inherited: false, UserId: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT},
}
}).Return(nil)
dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{
Id: q.Id,
Uid: q.Uid,
OrgId: q.OrgId,
}
}).Return(nil)
t.Run("Should get acl without duplicates", func(t *testing.T) {
user := &user.SignedInUser{
@@ -757,7 +784,8 @@ func TestGuardianGetACLWithoutDuplicates(t *testing.T) {
UserID: 1,
Login: "user1",
}
g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{})
g, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{})
require.NoError(t, err)
acl, err := g.GetACLWithoutDuplicates()
require.NoError(t, err)
+35 -3
View File
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/models"
@@ -43,7 +44,17 @@ func orgRoleScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFu
OrgRole: role,
}
store := dbtest.NewFakeDB()
guard := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, &dashboards.FakeDashboardService{}, &teamtest.FakeService{})
fakeDashboardService := dashboards.NewFakeDashboardService(t)
fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{
Id: q.Id,
Uid: q.Uid,
}
}).Return(nil)
guard, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, fakeDashboardService, &teamtest.FakeService{})
require.NoError(t, err)
sc := &scenarioContext{
t: t,
@@ -65,7 +76,17 @@ func apiKeyScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFun
ApiKeyID: 10,
}
store := dbtest.NewFakeDB()
guard := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, &dashboards.FakeDashboardService{}, &teamtest.FakeService{})
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{
Id: q.Id,
Uid: q.Uid,
}
}).Return(nil)
guard, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{})
require.NoError(t, err)
sc := &scenarioContext{
t: t,
orgRoleScenario: desc,
@@ -96,9 +117,20 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext,
q := args.Get(1).(*models.GetDashboardACLInfoListQuery)
q.Result = permissions
}).Return(nil)
dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{
Id: q.Id,
Uid: q.Uid,
OrgId: q.OrgId,
}
}).Return(nil)
sc.permissionScenario = desc
sc.g = newDashboardGuardian(context.Background(), dashboardID, sc.givenUser.OrgID, sc.givenUser, store, dashSvc, teamSvc)
g, err := newDashboardGuardian(context.Background(), dashboardID, sc.givenUser.OrgID, sc.givenUser, store, dashSvc, teamSvc)
require.NoError(t, err)
sc.g = g
sc.givenDashboardID = dashboardID
sc.givenPermissions = permissions
sc.givenTeams = teams
+19 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/team"
@@ -27,16 +28,32 @@ func ProvideService(
}
func InitLegacyGuardian(store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) {
New = func(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser) DashboardGuardian {
New = func(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
return newDashboardGuardian(ctx, dashId, orgId, user, store, dashSvc, teamSvc)
}
NewByUID = func(ctx context.Context, dashUID string, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
return newDashboardGuardianByUID(ctx, dashUID, orgId, user, store, dashSvc, teamSvc)
}
NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
return newDashboardGuardianByDashboard(ctx, dash, orgId, user, store, dashSvc, teamSvc)
}
}
func InitAccessControlGuardian(
store db.DB, ac accesscontrol.AccessControl, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardService dashboards.DashboardService,
) {
New = func(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser) DashboardGuardian {
New = func(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
return NewAccessControlDashboardGuardian(ctx, dashId, user, store, ac, folderPermissionsService, dashboardPermissionsService, dashboardService)
}
NewByUID = func(ctx context.Context, dashUID string, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
return NewAccessControlDashboardGuardianByUID(ctx, dashUID, user, store, ac, folderPermissionsService, dashboardPermissionsService, dashboardService)
}
NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) {
return NewAccessControlDashboardGuardianByDashboard(ctx, dash, user, store, ac, folderPermissionsService, dashboardPermissionsService, dashboardService)
}
}
+3 -3
View File
@@ -50,7 +50,7 @@ func (l *LibraryElementService) createHandler(c *models.ReqContext) response.Res
} else {
folder, err := l.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.OrgID, UID: cmd.FolderUID, SignedInUser: c.SignedInUser})
if err != nil || folder == nil {
return response.Error(http.StatusBadRequest, "failed to get folder", err)
return response.ErrOrFallback(http.StatusBadRequest, "failed to get folder", err)
}
cmd.FolderID = folder.ID
}
@@ -64,7 +64,7 @@ func (l *LibraryElementService) createHandler(c *models.ReqContext) response.Res
if element.FolderID != 0 {
folder, err := l.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.OrgID, ID: &element.FolderID, SignedInUser: c.SignedInUser})
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to get folder", err)
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get folder", err)
}
element.FolderUID = folder.UID
element.Meta.FolderUID = folder.UID
@@ -270,7 +270,7 @@ func toLibraryElementError(err error, message string) response.Response {
if errors.Is(err, errLibraryElementUIDTooLong) {
return response.Error(400, errLibraryElementUIDTooLong.Error(), err)
}
return response.Error(500, message, err)
return response.ErrOrFallback(http.StatusInternalServerError, message, err)
}
// swagger:parameters getLibraryElementByUID getLibraryElementConnections
+3 -7
View File
@@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
@@ -40,13 +39,12 @@ func (l *LibraryElementService) requireEditPermissionsOnFolder(ctx context.Conte
if isGeneralFolder(folderID) && user.HasRole(org.RoleViewer) {
return dashboards.ErrFolderAccessDenied
}
folder, err := l.folderService.Get(ctx, &folder.GetFolderQuery{ID: &folderID, OrgID: user.OrgID, SignedInUser: user})
g, err := guardian.New(ctx, folderID, user.OrgID, user)
if err != nil {
return err
}
g := guardian.New(ctx, folder.ID, user.OrgID, user)
canEdit, err := g.CanEdit()
if err != nil {
return err
@@ -63,13 +61,11 @@ func (l *LibraryElementService) requireViewPermissionsOnFolder(ctx context.Conte
return nil
}
folder, err := l.folderService.Get(ctx, &folder.GetFolderQuery{ID: &folderID, OrgID: user.OrgID, SignedInUser: user})
g, err := guardian.New(ctx, folderID, user.OrgID, user)
if err != nil {
return err
}
g := guardian.New(ctx, folder.ID, user.OrgID, user)
canView, err := g.CanView()
if err != nil {
return err
@@ -7,6 +7,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
@@ -764,7 +765,16 @@ func updateFolderACL(t *testing.T, dashboardStore *database.DashboardStore, fold
func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
store := dbtest.NewFakeDB()
guardian.InitLegacyGuardian(store, &dashboards.FakeDashboardService{}, &teamtest.FakeService{})
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
q := args.Get(1).(*models.GetDashboardQuery)
q.Result = &models.Dashboard{
Id: q.Id,
Uid: q.Uid,
}
}).Return(nil)
guardian.InitLegacyGuardian(store, dashSvc, &teamtest.FakeService{})
t.Helper()
testScenario(t, desc, func(t *testing.T, sc scenarioContext) {
+10 -2
View File
@@ -73,7 +73,10 @@ func (h *DashboardHandler) OnSubscribe(ctx context.Context, user *user.SignedInU
}
dash := query.Result
guard := guardian.New(ctx, dash.Id, user.OrgID, user)
guard, err := guardian.NewByDashboard(ctx, dash, user.OrgID, user)
if err != nil {
return models.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, err
}
if canView, err := guard.CanView(); err != nil || !canView {
return models.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil
}
@@ -119,7 +122,12 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, user *user.SignedInUse
return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil
}
guard := guardian.New(ctx, query.Result.Id, user.OrgID, user)
guard, err := guardian.NewByDashboard(ctx, query.Result, user.OrgID, user)
if err != nil {
logger.Error("Failed to create guardian", "err", err)
return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
}
canEdit, err := guard.CanEdit()
if err != nil {
return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
+5 -1
View File
@@ -375,7 +375,11 @@ func (st DBstore) GetNamespaceByTitle(ctx context.Context, namespace string, org
// if access control is disabled, check that the user is allowed to save in the folder.
if withCanSave && st.AccessControl.IsDisabled() {
g := guardian.New(ctx, folder.ID, orgID, user)
g, err := guardian.NewByUID(ctx, folder.UID, orgID, user)
if err != nil {
return folder, err
}
if canSave, err := g.CanSave(); err != nil || !canSave {
if err != nil {
st.Logger.Error("checking can save permission has failed", "userId", user.UserID, "username", user.Login, "namespace", namespace, "orgId", orgID, "error", err)
+20 -26
View File
@@ -182,7 +182,12 @@ func (hs *thumbService) parseImageReq(c *models.ReqContext, checkSave bool) *pre
}
// Check permissions and status
status := hs.getStatus(c, req.UID, checkSave)
status, err := hs.getStatus(c, req.UID, checkSave)
if err != nil {
c.JSON(status, map[string]string{"error": err.Error()})
return nil
}
if status != 200 {
c.JSON(status, map[string]string{"error": fmt.Sprintf("code: %d", status)})
return nil
@@ -463,35 +468,24 @@ func (hs *thumbService) CrawlerStatus(c *models.ReqContext) response.Response {
}
// Ideally this service would not require first looking up the full dashboard just to bet the id!
func (hs *thumbService) getStatus(c *models.ReqContext, uid string, checkSave bool) int {
dashboardID, err := hs.getDashboardId(c, uid)
func (hs *thumbService) getStatus(c *models.ReqContext, uid string, checkSave bool) (int, error) {
guardian, err := guardian.NewByUID(c.Req.Context(), uid, c.OrgID, c.SignedInUser)
if err != nil {
return 404
}
guardian := guardian.New(c.Req.Context(), dashboardID, c.OrgID, c.SignedInUser)
if checkSave {
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return 403 // forbidden
}
return 200
}
if canView, err := guardian.CanView(); err != nil || !canView {
return 403 // forbidden
}
return 200 // found and OK
}
func (hs *thumbService) getDashboardId(c *models.ReqContext, uid string) (int64, error) {
query := models.GetDashboardQuery{Uid: uid, OrgId: c.OrgID}
if err := hs.dashboardService.GetDashboard(c.Req.Context(), &query); err != nil {
return 0, err
}
return query.Result.Id, nil
if checkSave {
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return 403, nil // forbidden
}
return 200, nil
}
if canView, err := guardian.CanView(); err != nil || !canView {
return 403, nil // forbidden
}
return 200, nil // found and OK
}
func (hs *thumbService) runOnDemandCrawl(parentCtx context.Context, theme models.Theme, mode CrawlerMode, kind ThumbnailKind, authOpts rendering.AuthOpts) {