Access control: use uid for dashboard and folder scopes (#46807)

* use uid:s for folder and dashboard permissions

* evaluate folder and dashboard permissions based on uids

* add dashboard.uid to accept list

* Check for exact suffix

* Check parent folder on create

* update test

* drop dashboard:create actions with dashboard scope

* fix typo

* AccessControl: test id 0 scope conversion

* AccessControl: store only parent folder UID

* AccessControl: extract general as a constant

* FolderServices: Prevent creation of a folder uid'd general

* FolderServices: Test folder creation prevention

* Update pkg/services/guardian/accesscontrol_guardian.go

* FolderServices: fix mock call expect

* FolderServices: remove uneeded mocks

Co-authored-by: jguer <joao.guerreiro@grafana.com>
This commit is contained in:
Karl Persson 2022-03-30 15:14:26 +02:00 committed by GitHub
parent 56e9c24f08
commit a5e4a533fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 369 additions and 256 deletions

View File

@ -308,14 +308,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
dashboardsCreatorRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 1,
Version: 2,
Name: "fixed:dashboards:creator",
DisplayName: "Dashboard creator",
Description: "Create dashboard in general folder.",
Group: "Dashboards",
Permissions: []ac.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScope("0")},
{Action: ac.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScope("0")},
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
{Action: ac.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
},
},
Grants: []string{"Editor"},

View File

@ -329,18 +329,20 @@ func (hs *HTTPServer) registerRoutes() {
// Folders
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
idScope := dashboards.ScopeFoldersProvider.GetResourceScope(ac.Parameter(":id"))
uidScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.Parameter(":uid"))
folderRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
folderRoute.Get("/id/:id", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScope(ac.Parameter(":id")))), routing.Wrap(hs.GetFolderByID))
folderRoute.Get("/id/:id", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
folderRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Put("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersWrite)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Delete("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersDelete)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Put("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Delete("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersPermissionsRead)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersPermissionsWrite)), routing.Wrap(hs.UpdateFolderPermissions))
folderPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
})
})
})

View File

@ -120,7 +120,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
if err != nil {
return response.Error(500, "Error while checking dashboard permissions", err)
}
if err := hs.updateDashboardAccessControl(c.Req.Context(), dash.OrgId, dash.Id, false, items, old); err != nil {
if err := hs.updateDashboardAccessControl(c.Req.Context(), dash.OrgId, dash.Uid, false, items, old); err != nil {
return response.Error(500, "Failed to update permissions", err)
}
return response.Success("Dashboard permissions updated")
@ -138,7 +138,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
}
// updateDashboardAccessControl is used for api backward compatibility
func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID, dashID int64, isFolder bool, items []*models.DashboardAcl, old []*models.DashboardAclInfoDTO) error {
func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID int64, uid string, isFolder bool, items []*models.DashboardAcl, old []*models.DashboardAclInfoDTO) error {
commands := []accesscontrol.SetResourcePermissionCommand{}
for _, item := range items {
permissions := item.Permission.String()
@ -191,7 +191,7 @@ func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID, d
svc = hs.permissionServices.GetFolderService()
}
_, err := svc.SetPermissions(ctx, orgID, strconv.FormatInt(dashID, 10), commands...)
_, err := svc.SetPermissions(ctx, orgID, uid, commands...)
return err
}

View File

@ -120,7 +120,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res
if err != nil {
return response.Error(500, "Error while checking dashboard permissions", err)
}
if err := hs.updateDashboardAccessControl(c.Req.Context(), c.OrgId, folder.Id, true, items, old); err != nil {
if err := hs.updateDashboardAccessControl(c.Req.Context(), c.OrgId, folder.Uid, true, items, old); err != nil {
return response.Error(500, "Failed to create permission", err)
}
return response.Success("Dashboard permissions updated")

View File

@ -12,6 +12,7 @@ var (
ErrFolderVersionMismatch = errors.New("the folder has been changed by someone else")
ErrFolderTitleEmpty = errors.New("folder title cannot be empty")
ErrFolderWithSameUIDExists = errors.New("a folder/dashboard with the same uid already exists")
ErrFolderInvalidUID = errors.New("invalid uid for folder provided")
ErrFolderSameNameExists = errors.New("a folder or dashboard in the general folder with the same name already exists")
ErrFolderFailedGenerateUniqueUid = errors.New("failed to generate unique folder ID")
ErrFolderAccessDenied = errors.New("access denied to folder")

View File

@ -16,8 +16,7 @@ var sqlIDAcceptList = map[string]struct{}{
"u.id": {},
"\"user\".\"id\"": {}, // For Postgres
"`user`.`id`": {}, // For MySQL and SQLite
"dashboard.id": {},
"dashboard.folder_id": {},
"dashboard.uid": {},
}
var (

View File

@ -242,6 +242,7 @@ type SetResourcePermissionCommand struct {
const (
GlobalOrgID = 0
GeneralFolderUID = "general"
// Permission actions

View File

@ -23,11 +23,11 @@ func ProvidePermissionsServices(
if err != nil {
return nil, err
}
folderPermissions, err := provideFolderService(cfg, router, sql, ac, store)
folderPermissions, err := ProvideFolderPermissions(cfg, router, sql, ac, store)
if err != nil {
return nil, err
}
dashboardPermissions, err := provideDashboardService(cfg, router, sql, ac, store)
dashboardPermissions, err := ProvideDashboardPermissions(cfg, router, sql, ac, store)
if err != nil {
return nil, err
}
@ -141,20 +141,13 @@ func ProvideTeamPermissions(
var DashboardViewActions = []string{accesscontrol.ActionDashboardsRead}
var DashboardEditActions = append(DashboardViewActions, []string{accesscontrol.ActionDashboardsWrite, accesscontrol.ActionDashboardsDelete}...)
var DashboardAdminActions = append(DashboardEditActions, []string{accesscontrol.ActionDashboardsPermissionsRead, accesscontrol.ActionDashboardsPermissionsWrite}...)
var FolderViewActions = []string{dashboards.ActionFoldersRead}
var FolderEditActions = append(FolderViewActions, []string{dashboards.ActionFoldersWrite, dashboards.ActionFoldersDelete, accesscontrol.ActionDashboardsCreate}...)
var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...)
func provideDashboardService(
func ProvideDashboardPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore,
ac accesscontrol.AccessControl, store resourcepermissions.Store,
) (*resourcepermissions.Service, error) {
getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*models.Dashboard, error) {
id, err := strconv.ParseInt(resourceID, 10, 64)
if err != nil {
return nil, err
}
query := &models.GetDashboardQuery{Id: id, OrgId: orgID}
query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID}
if err := sql.GetDashboard(ctx, query); err != nil {
return nil, err
}
@ -163,7 +156,7 @@ func provideDashboardService(
options := resourcepermissions.Options{
Resource: "dashboards",
ResourceAttribute: "id",
ResourceAttribute: "uid",
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
dashboard, err := getDashboard(ctx, orgID, resourceID)
if err != nil {
@ -176,23 +169,13 @@ func provideDashboardService(
return nil
},
UidSolver: func(ctx context.Context, orgID int64, uid string) (int64, error) {
query := &models.GetDashboardQuery{
Uid: uid,
OrgId: orgID,
}
if err := sql.GetDashboard(ctx, query); err != nil {
return 0, err
}
return query.Result.Id, nil
},
InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) {
dashboard, err := getDashboard(ctx, orgID, resourceID)
if err != nil {
return nil, err
}
if dashboard.FolderId > 0 {
return []string{accesscontrol.GetResourceScope("folders", strconv.FormatInt(dashboard.FolderId, 10))}, nil
return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.Uid)}, nil
}
return []string{}, nil
},
@ -214,19 +197,19 @@ func provideDashboardService(
return resourcepermissions.New(options, cfg, router, ac, store, sql)
}
func provideFolderService(
var FolderViewActions = []string{dashboards.ActionFoldersRead}
var FolderEditActions = append(FolderViewActions, []string{dashboards.ActionFoldersWrite, dashboards.ActionFoldersDelete, accesscontrol.ActionDashboardsCreate}...)
var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...)
func ProvideFolderPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore,
accesscontrol accesscontrol.AccessControl, store resourcepermissions.Store,
) (*resourcepermissions.Service, error) {
options := resourcepermissions.Options{
Resource: "folders",
ResourceAttribute: "id",
ResourceAttribute: "uid",
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
id, err := strconv.ParseInt(resourceID, 10, 64)
if err != nil {
return err
}
query := &models.GetDashboardQuery{Id: id, OrgId: orgID}
query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID}
if err := sql.GetDashboard(ctx, query); err != nil {
return err
}
@ -237,16 +220,6 @@ func provideFolderService(
return nil
},
UidSolver: func(ctx context.Context, orgID int64, uid string) (int64, error) {
query := &models.GetDashboardQuery{
Uid: uid,
OrgId: orgID,
}
if err := sql.GetDashboard(ctx, query); err != nil {
return 0, err
}
return query.Result.Id, nil
},
Assignments: resourcepermissions.Assignments{
Users: true,
Teams: true,

View File

@ -9,6 +9,9 @@ import (
)
const (
ScopeFoldersRoot = "folders"
ScopeFoldersPrefix = "folders:uid:"
ActionFoldersCreate = "folders:create"
ActionFoldersRead = "folders:read"
ActionFoldersWrite = "folders:write"
@ -16,7 +19,8 @@ const (
ActionFoldersPermissionsRead = "folders.permissions:read"
ActionFoldersPermissionsWrite = "folders.permissions:write"
ScopeFoldersRoot = "folders"
ScopeDashboardsRoot = "dashboards"
ScopeDashboardsPrefix = "dashboards:uid:"
)
var (
@ -24,7 +28,7 @@ var (
ScopeFoldersProvider = ac.NewScopeProvider(ScopeFoldersRoot)
)
// NewNameScopeResolver provides an AttributeScopeResolver that is able to convert a scope prefixed with "folders:name:" into an id based scope.
// NewNameScopeResolver provides an AttributeScopeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope.
func NewNameScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
prefix := ScopeFoldersProvider.GetResourceScopeName("")
resolver := func(ctx context.Context, orgID int64, scope string) (string, error) {
@ -39,27 +43,34 @@ func NewNameScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
if err != nil {
return "", err
}
return ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(folder.Id, 10)), nil
return ScopeFoldersProvider.GetResourceScopeUID(folder.Uid), nil
}
return prefix, resolver
}
// NewUidScopeResolver provides an AttributeScopeResolver that is able to convert a scope prefixed with "folders:uid:" into an id based scope.
func NewUidScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
prefix := ScopeFoldersProvider.GetResourceScopeUID("")
// NewIDScopeResolver provides an AttributeScopeResolver that is able to convert a scope prefixed with "folders:id:" into an uid based scope.
func NewIDScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
prefix := ScopeFoldersProvider.GetResourceScope("")
resolver := func(ctx context.Context, orgID int64, scope string) (string, error) {
if !strings.HasPrefix(scope, prefix) {
return "", ac.ErrInvalidScope
}
uid := scope[len(prefix):]
if len(uid) == 0 {
id, err := strconv.ParseInt(scope[len(prefix):], 10, 64)
if err != nil {
return "", ac.ErrInvalidScope
}
folder, err := db.GetFolderByUID(ctx, orgID, uid)
if id == 0 {
return ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID), nil
}
folder, err := db.GetFolderByID(ctx, orgID, id)
if err != nil {
return "", err
}
return ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(folder.Id, 10)), nil
return ScopeFoldersProvider.GetResourceScopeUID(folder.Uid), nil
}
return prefix, resolver
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/rand"
"strconv"
"testing"
"github.com/stretchr/testify/mock"
@ -20,7 +21,7 @@ func TestNewNameScopeResolver(t *testing.T) {
require.Equal(t, "folders:name:", prefix)
})
t.Run("resolver should convert to id scope", func(t *testing.T) {
t.Run("resolver should convert to uid scope", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewNameScopeResolver(dashboardStore)
@ -30,6 +31,7 @@ func TestNewNameScopeResolver(t *testing.T) {
db := models.NewFolder(title)
db.Id = rand.Int63()
db.Uid = util.GenerateShortUID()
dashboardStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything).Return(db, nil).Once()
scope := "folders:name:" + title
@ -37,7 +39,7 @@ func TestNewNameScopeResolver(t *testing.T) {
resolvedScope, err := resolver(context.Background(), orgId, scope)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("folders:id:%v", db.Id), resolvedScope)
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScope)
dashboardStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title)
})
@ -71,56 +73,70 @@ func TestNewNameScopeResolver(t *testing.T) {
})
}
func TestNewUidScopeResolver(t *testing.T) {
func TestNewIDScopeResolver(t *testing.T) {
t.Run("prefix should be expected", func(t *testing.T) {
prefix, _ := NewUidScopeResolver(&FakeDashboardStore{})
require.Equal(t, "folders:uid:", prefix)
prefix, _ := NewIDScopeResolver(&FakeDashboardStore{})
require.Equal(t, "folders:id:", prefix)
})
t.Run("resolver should convert to id scope", func(t *testing.T) {
t.Run("resolver should convert to uid scope", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewUidScopeResolver(dashboardStore)
_, resolver := NewIDScopeResolver(dashboardStore)
orgId := rand.Int63()
uid := util.GenerateShortUID()
db := &models.Folder{Id: rand.Int63()}
dashboardStore.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything).Return(db, nil).Once()
db := &models.Folder{Id: rand.Int63(), Uid: uid}
dashboardStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(db, nil).Once()
scope := "folders:uid:" + uid
scope := "folders:id:" + strconv.FormatInt(db.Id, 10)
resolvedScope, err := resolver(context.Background(), orgId, scope)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("folders:id:%v", db.Id), resolvedScope)
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScope)
dashboardStore.AssertCalled(t, "GetFolderByUID", mock.Anything, orgId, uid)
dashboardStore.AssertCalled(t, "GetFolderByID", mock.Anything, orgId, db.Id)
})
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewUidScopeResolver(dashboardStore)
_, resolver := NewIDScopeResolver(dashboardStore)
_, err := resolver(context.Background(), rand.Int63(), "folders:id:123")
_, err := resolver(context.Background(), rand.Int63(), "folders:uid:123")
require.ErrorIs(t, err, ac.ErrInvalidScope)
})
t.Run("resolver should convert id 0 to general uid scope", func(t *testing.T) {
var (
dashboardStore = &FakeDashboardStore{}
orgId = rand.Int63()
scope = "folders:id:0"
_, resolver = NewIDScopeResolver(dashboardStore)
)
resolvedScope, err := resolver(context.Background(), orgId, scope)
require.NoError(t, err)
require.Equal(t, "folders:uid:general", resolvedScope)
})
t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewUidScopeResolver(dashboardStore)
_, resolver := NewIDScopeResolver(dashboardStore)
_, err := resolver(context.Background(), rand.Int63(), "folders:uid:")
_, err := resolver(context.Background(), rand.Int63(), "folders:id:")
require.ErrorIs(t, err, ac.ErrInvalidScope)
})
t.Run("returns 'not found' if folder does not exist", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewUidScopeResolver(dashboardStore)
_, resolver := NewIDScopeResolver(dashboardStore)
orgId := rand.Int63()
dashboardStore.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
scope := "folders:uid:" + util.GenerateShortUID()
dashboardStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
scope := "folders:id:10"
resolvedScope, err := resolver(context.Background(), orgId, scope)
require.ErrorIs(t, err, models.ErrDashboardNotFound)
require.Empty(t, resolvedScope)

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"strconv"
"time"
"github.com/grafana/grafana/pkg/infra/log"
@ -488,8 +487,7 @@ func saveDashboard(sess *sqlstore.DBSession, cmd *models.SaveDashboardCommand) e
}
// delete existing tags
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
if err != nil {
if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id); err != nil {
return err
}
@ -725,8 +723,9 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses
var dashIds []struct {
Id int64
Uid string
}
err := sess.SQL("SELECT id FROM dashboard WHERE folder_id = ?", dashboard.Id).Find(&dashIds)
err := sess.SQL("SELECT id, uid FROM dashboard WHERE folder_id = ?", dashboard.Id).Find(&dashIds)
if err != nil {
return err
}
@ -738,14 +737,14 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses
}
// remove all access control permission with folder scope
_, err = sess.Exec("DELETE FROM permission WHERE scope = ?", dashboards.ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(dashboard.Id, 10)))
_, err = sess.Exec("DELETE FROM permission WHERE scope = ?", dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.Uid))
if err != nil {
return err
}
for _, dash := range dashIds {
// remove all access control permission with child dashboard scopes
_, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.Scope("dashboards", "id", strconv.FormatInt(dash.Id, 10)))
_, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.GetResourceScopeUID("dashboards", dash.Uid))
if err != nil {
return err
}
@ -792,7 +791,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses
}
}
} else {
_, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.Scope("dashboards", "id", strconv.FormatInt(dashboard.Id, 10)))
_, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.GetResourceScopeUID("dashboards", dashboard.Uid))
if err != nil {
return err
}

View File

@ -3,7 +3,6 @@ package service
import (
"context"
"fmt"
"strconv"
"strings"
"time"
@ -449,7 +448,6 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que
func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *m.SaveDashboardDTO, dash *models.Dashboard, provisioned bool) error {
inFolder := dash.FolderId > 0
if dr.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
resourceID := strconv.FormatInt(dash.Id, 10)
var permissions []accesscontrol.SetResourcePermissionCommand
if !provisioned {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
@ -469,7 +467,7 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *
svc = dr.folderPermissions
}
_, err := svc.SetPermissions(ctx, dto.OrgId, resourceID, permissions...)
_, err := svc.SetPermissions(ctx, dto.OrgId, dash.Uid, permissions...)
if err != nil {
return err
}

View File

@ -3,7 +3,6 @@ package service
import (
"context"
"errors"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
@ -34,7 +33,7 @@ func ProvideFolderService(
ac accesscontrol.AccessControl, sqlStore sqlstore.Store,
) *FolderServiceImpl {
ac.RegisterAttributeScopeResolver(dashboards.NewNameScopeResolver(dashboardStore))
ac.RegisterAttributeScopeResolver(dashboards.NewUidScopeResolver(dashboardStore))
ac.RegisterAttributeScopeResolver(dashboards.NewIDScopeResolver(dashboardStore))
return &FolderServiceImpl{
cfg: cfg,
@ -134,7 +133,13 @@ func (f *FolderServiceImpl) GetFolderByTitle(ctx context.Context, user *models.S
func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) {
dashFolder := models.NewDashboardFolder(title)
dashFolder.OrgId = orgID
dashFolder.SetUid(strings.TrimSpace(uid))
trimmedUID := strings.TrimSpace(uid)
if trimmedUID == accesscontrol.GeneralFolderUID {
return nil, models.ErrFolderInvalidUID
}
dashFolder.SetUid(trimmedUID)
userID := user.UserId
if userID == 0 {
userID = -1
@ -167,8 +172,7 @@ func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.Signe
var permissionErr error
if f.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
resourceID := strconv.FormatInt(folder.Id, 10)
_, permissionErr = f.permissions.SetPermissions(ctx, orgID, resourceID, []accesscontrol.SetResourcePermissionCommand{
_, permissionErr = f.permissions.SetPermissions(ctx, orgID, folder.Uid, []accesscontrol.SetResourcePermissionCommand{
{UserID: userID, Permission: models.PERMISSION_ADMIN.String()},
{BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()},
{BuiltinRole: string(models.ROLE_VIEWER), Permission: models.PERMISSION_VIEW.String()},

View File

@ -138,6 +138,14 @@ func TestFolderService(t *testing.T) {
require.Equal(t, f, actualFolder)
})
t.Run("When creating folder should return error if uid is general", func(t *testing.T) {
dash := models.NewDashboardFolder("Test-Folder")
dash.Id = rand.Int63()
_, err := service.CreateFolder(context.Background(), user, orgID, dash.Title, "general")
require.ErrorIs(t, err, models.ErrFolderInvalidUID)
})
t.Run("When updating folder should not return access denied error", func(t *testing.T) {
dashboardFolder := models.NewDashboardFolder("Folder")
dashboardFolder.Id = rand.Int63()

View File

@ -40,6 +40,7 @@ type AccessControlDashboardGuardian struct {
log log.Logger
dashboardID int64
dashboard *models.Dashboard
parentFolderUID string
user *models.SignedInUser
store sqlstore.Store
ac accesscontrol.AccessControl
@ -52,12 +53,12 @@ func (a *AccessControlDashboardGuardian) CanSave() (bool, error) {
}
if a.dashboard.IsFolder {
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, folderScope(a.dashboardID)))
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, folderScope(a.dashboard.Uid)))
}
return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.dashboard.FolderId)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.parentFolderUID)),
))
}
@ -65,18 +66,17 @@ func (a *AccessControlDashboardGuardian) CanEdit() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
}
if setting.ViewersCanEdit {
return a.CanView()
}
if a.dashboard.IsFolder {
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, folderScope(a.dashboardID)))
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, folderScope(a.dashboard.Uid)))
}
return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.dashboard.FolderId)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.parentFolderUID)),
))
}
@ -86,12 +86,12 @@ func (a *AccessControlDashboardGuardian) CanView() (bool, error) {
}
if a.dashboard.IsFolder {
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, folderScope(a.dashboardID)))
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, folderScope(a.dashboard.Uid)))
}
return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, folderScope(a.dashboard.FolderId)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, folderScope(a.parentFolderUID)),
))
}
@ -102,19 +102,19 @@ func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) {
if a.dashboard.IsFolder {
return a.evaluate(accesscontrol.EvalAll(
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, folderScope(a.dashboard.Id)),
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, folderScope(a.dashboard.Id)),
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, folderScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, folderScope(a.dashboard.Uid)),
))
}
return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, dashboardScope(a.dashboard.Uid)),
),
accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, folderScope(a.dashboard.FolderId)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, folderScope(a.dashboard.FolderId)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, folderScope(a.parentFolderUID)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, folderScope(a.parentFolderUID)),
),
))
}
@ -125,12 +125,12 @@ func (a *AccessControlDashboardGuardian) CanDelete() (bool, error) {
}
if a.dashboard.IsFolder {
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, folderScope(a.dashboard.Id)))
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, folderScope(a.dashboard.Uid)))
}
return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, folderScope(a.dashboard.FolderId)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, folderScope(a.parentFolderUID)),
))
}
@ -138,8 +138,11 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool
if isFolder {
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate))
}
return a.evaluate(accesscontrol.EvalPermission(accesscontrol.ActionDashboardsCreate, folderScope(folderID)))
folder, err := a.loadParentFolder(folderID)
if err != nil {
return false, err
}
return a.evaluate(accesscontrol.EvalPermission(accesscontrol.ActionDashboardsCreate, folderScope(folder.Uid)))
}
func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) {
@ -258,15 +261,33 @@ func (a *AccessControlDashboardGuardian) loadDashboard() error {
if err := a.store.GetDashboard(a.ctx, query); err != nil {
return err
}
if !query.Result.IsFolder {
folder, err := a.loadParentFolder(query.Result.FolderId)
if err != nil {
return err
}
a.parentFolderUID = folder.Uid
}
a.dashboard = query.Result
}
return nil
}
func dashboardScope(dashboardID int64) string {
return accesscontrol.Scope("dashboards", "id", strconv.FormatInt(dashboardID, 10))
func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*models.Dashboard, error) {
if folderID == 0 {
return &models.Dashboard{Uid: accesscontrol.GeneralFolderUID}, nil
}
folderQuery := &models.GetDashboardQuery{Id: folderID, OrgId: a.user.OrgId}
if err := a.store.GetDashboard(a.ctx, folderQuery); err != nil {
return nil, err
}
return folderQuery.Result, nil
}
func folderScope(folderID int64) string {
return dashboards.ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(folderID, 10))
func dashboardScope(uid string) string {
return accesscontrol.GetResourceScopeUID("dashboards", uid)
}
func folderScope(uid string) string {
return dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)
}

View File

@ -10,7 +10,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
@ -24,7 +23,7 @@ import (
type accessControlGuardianTestCase struct {
desc string
dashboardID int64
dashUID string
permissions []*accesscontrol.Permission
viewersCanEdit bool
expected bool
@ -34,7 +33,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to save with dashboard wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
@ -45,7 +44,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
},
{
desc: "should be able to save with folder wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
@ -56,44 +55,44 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
},
{
desc: "should be able to save with dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:1",
Scope: "dashboards:uid:1",
},
},
expected: true,
},
{
desc: "should be able to save with folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:0",
Scope: "folders:uid:general",
},
},
expected: true,
},
{
desc: "should not be able to save with incorrect dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:10",
Scope: "dashboards:uid:10",
},
},
expected: false,
},
{
desc: "should not be able to save with incorrect folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:10",
Scope: "folders:uid:100",
},
},
expected: false,
@ -102,7 +101,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions)
can, err := guardian.CanSave()
require.NoError(t, err)
@ -110,12 +109,11 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
})
}
}
func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to edit with dashboard wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
@ -126,7 +124,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
},
{
desc: "should be able to edit with folder wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
@ -137,55 +135,55 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
},
{
desc: "should be able to edit with dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:1",
Scope: "dashboards:uid:1",
},
},
expected: true,
},
{
desc: "should be able to edit with folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:0",
Scope: "folders:uid:general",
},
},
expected: true,
},
{
desc: "should not be able to edit with incorrect dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:10",
Scope: "dashboards:uid:10",
},
},
expected: false,
},
{
desc: "should not be able to edit with incorrect folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:10",
Scope: "folders:uid:10",
},
},
expected: false,
},
{
desc: "should be able to edit with read action when viewer_can_edit is true",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:1",
Scope: "dashboards:uid:1",
},
},
viewersCanEdit: true,
@ -195,25 +193,23 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions)
if tt.viewersCanEdit {
setting.ViewersCanEdit = true
defer func() { setting.ViewersCanEdit = false }()
}
can, err := guardian.CanEdit()
require.NoError(t, err)
assert.Equal(t, tt.expected, can)
})
}
}
func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to view with dashboard wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
@ -224,7 +220,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
},
{
desc: "should be able to view with folder wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
@ -235,44 +231,44 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
},
{
desc: "should be able to view with dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:1",
Scope: "dashboards:uid:1",
},
},
expected: true,
},
{
desc: "should be able to view with folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "folders:id:0",
Scope: "folders:uid:general",
},
},
expected: true,
},
{
desc: "should not be able to view with incorrect dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:10",
Scope: "dashboards:uid:10",
},
},
expected: false,
},
{
desc: "should not be able to view with incorrect folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "folders:id:10",
Scope: "folders:uid:10",
},
},
expected: false,
@ -281,7 +277,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions)
can, err := guardian.CanView()
require.NoError(t, err)
@ -289,12 +285,11 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
})
}
}
func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to admin with dashboard wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
@ -309,7 +304,7 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
},
{
desc: "should be able to admin with folder wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
@ -324,60 +319,60 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
},
{
desc: "should be able to admin with dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "dashboards:id:1",
Scope: "dashboards:uid:1",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "dashboards:id:1",
Scope: "dashboards:uid:1",
},
},
expected: true,
},
{
desc: "should be able to admin with folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "folders:id:0",
Scope: "folders:uid:general",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "folders:id:0",
Scope: "folders:uid:general",
},
},
expected: true,
},
{
desc: "should not be able to admin with incorrect dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "dashboards:id:10",
Scope: "dashboards:uid:10",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "dashboards:id:10",
Scope: "dashboards:uid:10",
},
},
expected: false,
},
{
desc: "should not be able to admin with incorrect folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "folders:id:10",
Scope: "folders:uid:10",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "folders:id:10",
Scope: "folders:uid:10",
},
},
expected: false,
@ -386,7 +381,7 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions)
can, err := guardian.CanAdmin()
require.NoError(t, err)
@ -394,12 +389,11 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
})
}
}
func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to delete with dashboard wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
@ -410,7 +404,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
},
{
desc: "should be able to delete with folder wildcard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
@ -421,44 +415,44 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
},
{
desc: "should be able to delete with dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "dashboards:id:1",
Scope: "dashboards:uid:1",
},
},
expected: true,
},
{
desc: "should be able to delete with folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "folders:id:0",
Scope: "folders:uid:general",
},
},
expected: true,
},
{
desc: "should not be able to delete with incorrect dashboard scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "dashboards:id:10",
Scope: "dashboards:uid:10",
},
},
expected: false,
},
{
desc: "should not be able to delete with incorrect folder scope",
dashboardID: 1,
dashUID: "1",
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "folders:id:10",
Scope: "folders:uid:10",
},
},
expected: false,
@ -467,7 +461,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions)
can, err := guardian.CanDelete()
require.NoError(t, err)
@ -487,18 +481,18 @@ type accessControlGuardianCanCreateTestCase struct {
func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
tests := []accessControlGuardianCanCreateTestCase{
{
desc: "should be able to create dashboard in folder 0",
desc: "should be able to create dashboard in general folder",
isFolder: false,
folderID: 0,
permissions: []*accesscontrol.Permission{
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:id:0"},
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:uid:general"},
},
expected: true,
},
{
desc: "should be able to create dashboard in any folder",
isFolder: false,
folderID: 100,
folderID: 0,
permissions: []*accesscontrol.Permission{
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:*"},
},
@ -507,7 +501,7 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
{
desc: "should not be able to create dashboard without permissions",
isFolder: false,
folderID: 100,
folderID: 0,
permissions: []*accesscontrol.Permission{},
expected: false,
},
@ -523,7 +517,7 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
{
desc: "should not be able to create folders without permissions",
isFolder: true,
folderID: 100,
folderID: 0,
permissions: []*accesscontrol.Permission{},
expected: false,
},
@ -531,7 +525,7 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, 0, tt.permissions)
guardian, _ := setupAccessControlGuardianTest(t, "0", tt.permissions)
can, err := guardian.CanCreate(tt.folderID, tt.isFolder)
require.NoError(t, err)
@ -563,16 +557,14 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, 1, nil)
guardian, _ := setupAccessControlGuardianTest(t, "1", nil)
mocked := accesscontrolmock.NewPermissionsServicesMock()
guardian.permissionServices = mocked
mocked.Dashboards.On("MapActions", mock.Anything).Return("View")
mocked.Dashboards.On("GetPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tt.permissions, nil)
cfg := setting.NewCfg()
cfg.HiddenUsers = tt.hiddenUsers
permissions, err := guardian.GetHiddenACL(cfg)
require.NoError(t, err)
var hiddenUserNames []string
@ -587,21 +579,24 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) {
}
}
func setupAccessControlGuardianTest(t *testing.T, dashID int64, permissions []*accesscontrol.Permission) *AccessControlDashboardGuardian {
func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []*accesscontrol.Permission) (*AccessControlDashboardGuardian, *models.Dashboard) {
t.Helper()
store := sqlstore.InitTestDB(t)
toSave := models.NewDashboard(uid)
toSave.SetUid(uid)
// seed dashboard
_, err := dashdb.ProvideDashboardStore(store).SaveDashboard(models.SaveDashboardCommand{
Dashboard: &simplejson.Json{},
dash, err := dashdb.ProvideDashboardStore(store).SaveDashboard(models.SaveDashboardCommand{
Dashboard: toSave.Data,
UserId: 1,
OrgId: 1,
FolderId: 0,
})
require.NoError(t, err)
ac := accesscontrolmock.New().WithPermissions(permissions)
services, err := ossaccesscontrol.ProvidePermissionsServices(setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store))
require.NoError(t, err)
return NewAccessControlDashboardGuardian(context.Background(), dashID, &models.SignedInUser{OrgId: 1}, store, ac, services)
return NewAccessControlDashboardGuardian(context.Background(), dash.Id, &models.SignedInUser{OrgId: 1}, store, ac, services), dash
}

View File

@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"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/dashboards"
"github.com/grafana/grafana/pkg/util"
)
@ -311,6 +312,9 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv
dash.Overwrite = true
dash.OrgId = cfg.OrgID
// set dashboard folderUid if given
if cfg.FolderUID == accesscontrol.GeneralFolderUID {
return 0, models.ErrFolderInvalidUID
}
dash.Dashboard.SetUid(cfg.FolderUID)
dbDash, err := service.SaveFolderForProvisionedDashboards(ctx, dash)
if err != nil {

View File

@ -390,6 +390,26 @@ func TestDashboardFileReader(t *testing.T) {
require.NoError(t, err)
})
t.Run("should not create dashboard folder with uid general", func(t *testing.T) {
setup()
cfg := &config{
Name: "DefaultB",
Type: "file",
OrgID: 1,
Folder: "TEAM B",
FolderUID: "general",
Options: map[string]interface{}{
"folder": defaultDashboards,
},
}
r, err := NewDashboardFileReader(cfg, logger, nil)
require.NoError(t, err)
_, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder)
require.ErrorIs(t, err, models.ErrFolderInvalidUID)
})
t.Run("Walking the folder with dashboards", func(t *testing.T) {
setup()
noFiles := map[string]os.FileInfo{}

View File

@ -21,7 +21,6 @@ var dashboardPermissionTranslation = map[models.PermissionType][]string{
models.PERMISSION_EDIT: {
ac.ActionDashboardsRead,
ac.ActionDashboardsWrite,
ac.ActionDashboardsCreate,
ac.ActionDashboardsDelete,
},
models.PERMISSION_ADMIN: {
@ -39,6 +38,7 @@ var folderPermissionTranslation = map[models.PermissionType][]string{
dashboards.ActionFoldersRead,
}...),
models.PERMISSION_EDIT: append(dashboardPermissionTranslation[models.PERMISSION_EDIT], []string{
ac.ActionDashboardsCreate,
dashboards.ActionFoldersRead,
dashboards.ActionFoldersWrite,
dashboards.ActionFoldersCreate,
@ -56,6 +56,7 @@ var folderPermissionTranslation = map[models.PermissionType][]string{
func AddDashboardPermissionsMigrator(mg *migrator.Migrator) {
mg.AddMigration("dashboard permissions", &dashboardPermissionsMigrator{})
mg.AddMigration("dashboard permissions uid scopes", &dashboardUidPermissionMigrator{})
}
var _ migrator.CodeMigration = new(dashboardPermissionsMigrator)
@ -219,3 +220,63 @@ func getRoleName(p models.DashboardAcl) string {
}
return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(string(*p.Role)))
}
var _ migrator.CodeMigration = new(dashboardUidPermissionMigrator)
type dashboardUidPermissionMigrator struct {
migrator.MigrationBase
}
func (d *dashboardUidPermissionMigrator) SQL(dialect migrator.Dialect) string {
return "code migration"
}
func (d *dashboardUidPermissionMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
if err := d.migrateWildcards(sess); err != nil {
return err
}
return d.migrateIdScopes(sess)
}
func (d *dashboardUidPermissionMigrator) migrateWildcards(sess *xorm.Session) error {
if _, err := sess.Exec("DELETE FROM permission WHERE action = 'dashboards:create' AND scope LIKE 'dashboards%'"); err != nil {
return err
}
if _, err := sess.Exec("UPDATE permission SET scope = 'dashboards:uid:*' WHERE scope = 'dashboards:id:*'"); err != nil {
return err
}
if _, err := sess.Exec("UPDATE permission SET scope = 'folders:uid:*' WHERE scope = 'folders:id:*'"); err != nil {
return err
}
return nil
}
func (d *dashboardUidPermissionMigrator) migrateIdScopes(sess *xorm.Session) error {
type dashboard struct {
ID int64 `xorm:"id"`
UID string `xorm:"uid"`
IsFolder bool
}
var dashboards []dashboard
if err := sess.SQL("SELECT id, uid, is_folder FROM dashboard").Find(&dashboards); err != nil {
return err
}
for _, d := range dashboards {
var idScope string
var uidScope string
if d.IsFolder {
idScope = ac.Scope("folders", "id", strconv.FormatInt(d.ID, 10))
uidScope = ac.Scope("folders", "uid", d.UID)
} else {
idScope = ac.Scope("dashboards", "id", strconv.FormatInt(d.ID, 10))
uidScope = ac.Scope("dashboards", "uid", d.UID)
}
if _, err := sess.Exec("UPDATE permission SET scope = ? WHERE scope = ?", uidScope, idScope); err != nil {
return err
}
}
return nil
}

View File

@ -110,15 +110,16 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
if len(f.dashboardActions) > 0 {
builder.WriteString("((")
dashFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "dashboards:id:", f.dashboardActions...)
dashFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeDashboardsPrefix, f.dashboardActions...)
builder.WriteString(dashFilter.Where)
args = append(args, dashFilter.Args...)
builder.WriteString(" OR ")
builder.WriteString(" OR dashboard.folder_id IN(SELECT id FROM dashboard WHERE ")
dashFolderFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeFoldersPrefix, f.dashboardActions...)
dashFolderFilter, _ := accesscontrol.Filter(f.User, "dashboard.folder_id", "folders:id:", f.dashboardActions...)
builder.WriteString(dashFolderFilter.Where)
builder.WriteString(") AND NOT dashboard.is_folder)")
builder.WriteString(")) AND NOT dashboard.is_folder)")
args = append(args, dashFolderFilter.Args...)
}
@ -127,12 +128,11 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
builder.WriteString(" OR ")
}
builder.WriteString("(")
folderFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "folders:id:", f.folderActions...)
folderFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeFoldersPrefix, f.folderActions...)
builder.WriteString(folderFilter.Where)
builder.WriteString(" AND dashboard.is_folder)")
args = append(args, folderFilter.Args...)
}
builder.WriteString(")")
return builder.String(), args
}

View File

@ -108,7 +108,7 @@ func TestAccessControlDashboardPermissionFilter_Where(t *testing.T) {
title: "folder and dashboard actions are defined",
dashboardActions: []string{"test"},
folderActions: []string{"test"},
expectedResult: "((( 1 = 0 OR 1 = 0) AND NOT dashboard.is_folder) OR ( 1 = 0 AND dashboard.is_folder))",
expectedResult: "((( 1 = 0 OR dashboard.folder_id IN(SELECT id FROM dashboard WHERE 1 = 0)) AND NOT dashboard.is_folder) OR ( 1 = 0 AND dashboard.is_folder))",
},
{
title: "folder actions are defined but not dashboard actions",
@ -120,7 +120,7 @@ func TestAccessControlDashboardPermissionFilter_Where(t *testing.T) {
title: "dashboard actions are defined but not folder actions",
dashboardActions: []string{"test"},
folderActions: nil,
expectedResult: "((( 1 = 0 OR 1 = 0) AND NOT dashboard.is_folder))",
expectedResult: "((( 1 = 0 OR dashboard.folder_id IN(SELECT id FROM dashboard WHERE 1 = 0)) AND NOT dashboard.is_folder))",
},
{
title: "dashboard actions are defined but not folder actions",