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{ dashboardsCreatorRole := ac.RoleRegistration{
Role: ac.RoleDTO{ Role: ac.RoleDTO{
Version: 1, Version: 2,
Name: "fixed:dashboards:creator", Name: "fixed:dashboards:creator",
DisplayName: "Dashboard creator", DisplayName: "Dashboard creator",
Description: "Create dashboard in general folder.", Description: "Create dashboard in general folder.",
Group: "Dashboards", Group: "Dashboards",
Permissions: []ac.Permission{ Permissions: []ac.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScope("0")}, {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
{Action: ac.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScope("0")}, {Action: ac.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
}, },
}, },
Grants: []string{"Editor"}, Grants: []string{"Editor"},

View File

@ -329,18 +329,20 @@ func (hs *HTTPServer) registerRoutes() {
// Folders // Folders
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) { 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("/", 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.Post("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolderByUID)) folderUidRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Put("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersWrite)), routing.Wrap(hs.UpdateFolder)) folderUidRoute.Put("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Delete("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersDelete)), routing.Wrap(hs.DeleteFolder)) folderUidRoute.Delete("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) { folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersPermissionsRead)), routing.Wrap(hs.GetFolderPermissionList)) folderPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionFoldersPermissionsWrite)), routing.Wrap(hs.UpdateFolderPermissions)) 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 { if err != nil {
return response.Error(500, "Error while checking dashboard permissions", err) 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.Error(500, "Failed to update permissions", err)
} }
return response.Success("Dashboard permissions updated") 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 // 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{} commands := []accesscontrol.SetResourcePermissionCommand{}
for _, item := range items { for _, item := range items {
permissions := item.Permission.String() permissions := item.Permission.String()
@ -191,7 +191,7 @@ func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID, d
svc = hs.permissionServices.GetFolderService() svc = hs.permissionServices.GetFolderService()
} }
_, err := svc.SetPermissions(ctx, orgID, strconv.FormatInt(dashID, 10), commands...) _, err := svc.SetPermissions(ctx, orgID, uid, commands...)
return err return err
} }

View File

@ -120,7 +120,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res
if err != nil { if err != nil {
return response.Error(500, "Error while checking dashboard permissions", err) 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.Error(500, "Failed to create permission", err)
} }
return response.Success("Dashboard permissions updated") return response.Success("Dashboard permissions updated")

View File

@ -12,6 +12,7 @@ var (
ErrFolderVersionMismatch = errors.New("the folder has been changed by someone else") ErrFolderVersionMismatch = errors.New("the folder has been changed by someone else")
ErrFolderTitleEmpty = errors.New("folder title cannot be empty") ErrFolderTitleEmpty = errors.New("folder title cannot be empty")
ErrFolderWithSameUIDExists = errors.New("a folder/dashboard with the same uid already exists") 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") 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") ErrFolderFailedGenerateUniqueUid = errors.New("failed to generate unique folder ID")
ErrFolderAccessDenied = errors.New("access denied to folder") ErrFolderAccessDenied = errors.New("access denied to folder")

View File

@ -9,15 +9,14 @@ import (
) )
var sqlIDAcceptList = map[string]struct{}{ var sqlIDAcceptList = map[string]struct{}{
"org_user.user_id": {}, "org_user.user_id": {},
"role.id": {}, "role.id": {},
"t.id": {}, "t.id": {},
"team.id": {}, "team.id": {},
"u.id": {}, "u.id": {},
"\"user\".\"id\"": {}, // For Postgres "\"user\".\"id\"": {}, // For Postgres
"`user`.`id`": {}, // For MySQL and SQLite "`user`.`id`": {}, // For MySQL and SQLite
"dashboard.id": {}, "dashboard.uid": {},
"dashboard.folder_id": {},
} }
var ( var (

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"time" "time"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
@ -488,8 +487,7 @@ func saveDashboard(sess *sqlstore.DBSession, cmd *models.SaveDashboardCommand) e
} }
// delete existing tags // delete existing tags
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id) if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id); err != nil {
if err != nil {
return err return err
} }
@ -724,9 +722,10 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?") deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
var dashIds []struct { var dashIds []struct {
Id int64 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 { if err != nil {
return err return err
} }
@ -738,14 +737,14 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses
} }
// remove all access control permission with folder scope // 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 { if err != nil {
return err return err
} }
for _, dash := range dashIds { for _, dash := range dashIds {
// remove all access control permission with child dashboard scopes // 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 { if err != nil {
return err return err
} }
@ -792,7 +791,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses
} }
} }
} else { } 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 { if err != nil {
return err return err
} }

View File

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

View File

@ -3,7 +3,6 @@ package service
import ( import (
"context" "context"
"errors" "errors"
"strconv"
"strings" "strings"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
@ -34,7 +33,7 @@ func ProvideFolderService(
ac accesscontrol.AccessControl, sqlStore sqlstore.Store, ac accesscontrol.AccessControl, sqlStore sqlstore.Store,
) *FolderServiceImpl { ) *FolderServiceImpl {
ac.RegisterAttributeScopeResolver(dashboards.NewNameScopeResolver(dashboardStore)) ac.RegisterAttributeScopeResolver(dashboards.NewNameScopeResolver(dashboardStore))
ac.RegisterAttributeScopeResolver(dashboards.NewUidScopeResolver(dashboardStore)) ac.RegisterAttributeScopeResolver(dashboards.NewIDScopeResolver(dashboardStore))
return &FolderServiceImpl{ return &FolderServiceImpl{
cfg: cfg, 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) { func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) {
dashFolder := models.NewDashboardFolder(title) dashFolder := models.NewDashboardFolder(title)
dashFolder.OrgId = orgID 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 userID := user.UserId
if userID == 0 { if userID == 0 {
userID = -1 userID = -1
@ -167,8 +172,7 @@ func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.Signe
var permissionErr error var permissionErr error
if f.features.IsEnabled(featuremgmt.FlagAccesscontrol) { if f.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
resourceID := strconv.FormatInt(folder.Id, 10) _, permissionErr = f.permissions.SetPermissions(ctx, orgID, folder.Uid, []accesscontrol.SetResourcePermissionCommand{
_, permissionErr = f.permissions.SetPermissions(ctx, orgID, resourceID, []accesscontrol.SetResourcePermissionCommand{
{UserID: userID, Permission: models.PERMISSION_ADMIN.String()}, {UserID: userID, Permission: models.PERMISSION_ADMIN.String()},
{BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()}, {BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()},
{BuiltinRole: string(models.ROLE_VIEWER), Permission: models.PERMISSION_VIEW.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) 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) { t.Run("When updating folder should not return access denied error", func(t *testing.T) {
dashboardFolder := models.NewDashboardFolder("Folder") dashboardFolder := models.NewDashboardFolder("Folder")
dashboardFolder.Id = rand.Int63() dashboardFolder.Id = rand.Int63()

View File

@ -40,6 +40,7 @@ type AccessControlDashboardGuardian struct {
log log.Logger log log.Logger
dashboardID int64 dashboardID int64
dashboard *models.Dashboard dashboard *models.Dashboard
parentFolderUID string
user *models.SignedInUser user *models.SignedInUser
store sqlstore.Store store sqlstore.Store
ac accesscontrol.AccessControl ac accesscontrol.AccessControl
@ -52,12 +53,12 @@ func (a *AccessControlDashboardGuardian) CanSave() (bool, error) {
} }
if a.dashboard.IsFolder { 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( return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Id)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.dashboard.FolderId)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.parentFolderUID)),
)) ))
} }
@ -65,18 +66,17 @@ func (a *AccessControlDashboardGuardian) CanEdit() (bool, error) {
if err := a.loadDashboard(); err != nil { if err := a.loadDashboard(); err != nil {
return false, err return false, err
} }
if setting.ViewersCanEdit { if setting.ViewersCanEdit {
return a.CanView() return a.CanView()
} }
if a.dashboard.IsFolder { 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( return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Id)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.dashboard.FolderId)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.parentFolderUID)),
)) ))
} }
@ -86,12 +86,12 @@ func (a *AccessControlDashboardGuardian) CanView() (bool, error) {
} }
if a.dashboard.IsFolder { 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( return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, dashboardScope(a.dashboard.Id)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, folderScope(a.dashboard.FolderId)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, folderScope(a.parentFolderUID)),
)) ))
} }
@ -102,19 +102,19 @@ func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) {
if a.dashboard.IsFolder { if a.dashboard.IsFolder {
return a.evaluate(accesscontrol.EvalAll( return a.evaluate(accesscontrol.EvalAll(
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, folderScope(a.dashboard.Id)), accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, folderScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, folderScope(a.dashboard.Id)), accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, folderScope(a.dashboard.Uid)),
)) ))
} }
return a.evaluate(accesscontrol.EvalAny( return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalAll( accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, dashboardScope(a.dashboard.Id)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, dashboardScope(a.dashboard.Id)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, dashboardScope(a.dashboard.Uid)),
), ),
accesscontrol.EvalAll( accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, folderScope(a.dashboard.FolderId)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, folderScope(a.parentFolderUID)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, folderScope(a.dashboard.FolderId)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, folderScope(a.parentFolderUID)),
), ),
)) ))
} }
@ -125,12 +125,12 @@ func (a *AccessControlDashboardGuardian) CanDelete() (bool, error) {
} }
if a.dashboard.IsFolder { 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( return a.evaluate(accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, dashboardScope(a.dashboard.Id)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, dashboardScope(a.dashboard.Uid)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, folderScope(a.dashboard.FolderId)), accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, folderScope(a.parentFolderUID)),
)) ))
} }
@ -138,8 +138,11 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool
if isFolder { if isFolder {
return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)) return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate))
} }
folder, err := a.loadParentFolder(folderID)
return a.evaluate(accesscontrol.EvalPermission(accesscontrol.ActionDashboardsCreate, folderScope(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) { 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 { if err := a.store.GetDashboard(a.ctx, query); err != nil {
return err 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 a.dashboard = query.Result
} }
return nil return nil
} }
func dashboardScope(dashboardID int64) string { func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*models.Dashboard, error) {
return accesscontrol.Scope("dashboards", "id", strconv.FormatInt(dashboardID, 10)) 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 { func dashboardScope(uid string) string {
return dashboards.ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(folderID, 10)) 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/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing" "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/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database" "github.com/grafana/grafana/pkg/services/accesscontrol/database"
@ -24,7 +23,7 @@ import (
type accessControlGuardianTestCase struct { type accessControlGuardianTestCase struct {
desc string desc string
dashboardID int64 dashUID string
permissions []*accesscontrol.Permission permissions []*accesscontrol.Permission
viewersCanEdit bool viewersCanEdit bool
expected bool expected bool
@ -33,8 +32,8 @@ type accessControlGuardianTestCase struct {
func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
tests := []accessControlGuardianTestCase{ tests := []accessControlGuardianTestCase{
{ {
desc: "should be able to save with dashboard wildcard scope", desc: "should be able to save with dashboard wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
@ -44,8 +43,8 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to save with folder wildcard scope", desc: "should be able to save with folder wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
@ -55,45 +54,45 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to save with dashboard scope", desc: "should be able to save with dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:1", Scope: "dashboards:uid:1",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should be able to save with folder scope", desc: "should be able to save with folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:0", Scope: "folders:uid:general",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should not be able to save with incorrect dashboard scope", desc: "should not be able to save with incorrect dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:10", Scope: "dashboards:uid:10",
}, },
}, },
expected: false, expected: false,
}, },
{ {
desc: "should not be able to save with incorrect folder scope", desc: "should not be able to save with incorrect folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:10", Scope: "folders:uid:100",
}, },
}, },
expected: false, expected: false,
@ -102,7 +101,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) { 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() can, err := guardian.CanSave()
require.NoError(t, err) require.NoError(t, err)
@ -110,12 +109,11 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
}) })
} }
} }
func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
tests := []accessControlGuardianTestCase{ tests := []accessControlGuardianTestCase{
{ {
desc: "should be able to edit with dashboard wildcard scope", desc: "should be able to edit with dashboard wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
@ -125,8 +123,8 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to edit with folder wildcard scope", desc: "should be able to edit with folder wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
@ -136,56 +134,56 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to edit with dashboard scope", desc: "should be able to edit with dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:1", Scope: "dashboards:uid:1",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should be able to edit with folder scope", desc: "should be able to edit with folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:0", Scope: "folders:uid:general",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should not be able to edit with incorrect dashboard scope", desc: "should not be able to edit with incorrect dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:10", Scope: "dashboards:uid:10",
}, },
}, },
expected: false, expected: false,
}, },
{ {
desc: "should not be able to edit with incorrect folder scope", desc: "should not be able to edit with incorrect folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsWrite, Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:10", Scope: "folders:uid:10",
}, },
}, },
expected: false, expected: false,
}, },
{ {
desc: "should be able to edit with read action when viewer_can_edit is true", desc: "should be able to edit with read action when viewer_can_edit is true",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsRead, Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:1", Scope: "dashboards:uid:1",
}, },
}, },
viewersCanEdit: true, viewersCanEdit: true,
@ -195,25 +193,23 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) { 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 { if tt.viewersCanEdit {
setting.ViewersCanEdit = true setting.ViewersCanEdit = true
defer func() { setting.ViewersCanEdit = false }() defer func() { setting.ViewersCanEdit = false }()
} }
can, err := guardian.CanEdit() can, err := guardian.CanEdit()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tt.expected, can) assert.Equal(t, tt.expected, can)
}) })
} }
} }
func TestAccessControlDashboardGuardian_CanView(t *testing.T) { func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
tests := []accessControlGuardianTestCase{ tests := []accessControlGuardianTestCase{
{ {
desc: "should be able to view with dashboard wildcard scope", desc: "should be able to view with dashboard wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsRead, Action: accesscontrol.ActionDashboardsRead,
@ -223,8 +219,8 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to view with folder wildcard scope", desc: "should be able to view with folder wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsRead, Action: accesscontrol.ActionDashboardsRead,
@ -234,45 +230,45 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to view with dashboard scope", desc: "should be able to view with dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsRead, Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:1", Scope: "dashboards:uid:1",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should be able to view with folder scope", desc: "should be able to view with folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsRead, Action: accesscontrol.ActionDashboardsRead,
Scope: "folders:id:0", Scope: "folders:uid:general",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should not be able to view with incorrect dashboard scope", desc: "should not be able to view with incorrect dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsRead, Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:10", Scope: "dashboards:uid:10",
}, },
}, },
expected: false, expected: false,
}, },
{ {
desc: "should not be able to view with incorrect folder scope", desc: "should not be able to view with incorrect folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsRead, Action: accesscontrol.ActionDashboardsRead,
Scope: "folders:id:10", Scope: "folders:uid:10",
}, },
}, },
expected: false, expected: false,
@ -281,7 +277,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) { 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() can, err := guardian.CanView()
require.NoError(t, err) require.NoError(t, err)
@ -289,12 +285,11 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
}) })
} }
} }
func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) { func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
tests := []accessControlGuardianTestCase{ tests := []accessControlGuardianTestCase{
{ {
desc: "should be able to admin with dashboard wildcard scope", desc: "should be able to admin with dashboard wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsPermissionsRead, Action: accesscontrol.ActionDashboardsPermissionsRead,
@ -308,8 +303,8 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to admin with folder wildcard scope", desc: "should be able to admin with folder wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsPermissionsRead, Action: accesscontrol.ActionDashboardsPermissionsRead,
@ -323,61 +318,61 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to admin with dashboard scope", desc: "should be able to admin with dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsPermissionsRead, Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "dashboards:id:1", Scope: "dashboards:uid:1",
}, },
{ {
Action: accesscontrol.ActionDashboardsPermissionsWrite, Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "dashboards:id:1", Scope: "dashboards:uid:1",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should be able to admin with folder scope", desc: "should be able to admin with folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsPermissionsRead, Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "folders:id:0", Scope: "folders:uid:general",
}, },
{ {
Action: accesscontrol.ActionDashboardsPermissionsWrite, Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "folders:id:0", Scope: "folders:uid:general",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should not be able to admin with incorrect dashboard scope", desc: "should not be able to admin with incorrect dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsPermissionsRead, Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "dashboards:id:10", Scope: "dashboards:uid:10",
}, },
{ {
Action: accesscontrol.ActionDashboardsPermissionsWrite, Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "dashboards:id:10", Scope: "dashboards:uid:10",
}, },
}, },
expected: false, expected: false,
}, },
{ {
desc: "should not be able to admin with incorrect folder scope", desc: "should not be able to admin with incorrect folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsPermissionsRead, Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "folders:id:10", Scope: "folders:uid:10",
}, },
{ {
Action: accesscontrol.ActionDashboardsPermissionsWrite, Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "folders:id:10", Scope: "folders:uid:10",
}, },
}, },
expected: false, expected: false,
@ -386,7 +381,7 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) { 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() can, err := guardian.CanAdmin()
require.NoError(t, err) require.NoError(t, err)
@ -394,12 +389,11 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
}) })
} }
} }
func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) { func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
tests := []accessControlGuardianTestCase{ tests := []accessControlGuardianTestCase{
{ {
desc: "should be able to delete with dashboard wildcard scope", desc: "should be able to delete with dashboard wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsDelete, Action: accesscontrol.ActionDashboardsDelete,
@ -409,8 +403,8 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to delete with folder wildcard scope", desc: "should be able to delete with folder wildcard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsDelete, Action: accesscontrol.ActionDashboardsDelete,
@ -420,45 +414,45 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
expected: true, expected: true,
}, },
{ {
desc: "should be able to delete with dashboard scope", desc: "should be able to delete with dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsDelete, Action: accesscontrol.ActionDashboardsDelete,
Scope: "dashboards:id:1", Scope: "dashboards:uid:1",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should be able to delete with folder scope", desc: "should be able to delete with folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsDelete, Action: accesscontrol.ActionDashboardsDelete,
Scope: "folders:id:0", Scope: "folders:uid:general",
}, },
}, },
expected: true, expected: true,
}, },
{ {
desc: "should not be able to delete with incorrect dashboard scope", desc: "should not be able to delete with incorrect dashboard scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsDelete, Action: accesscontrol.ActionDashboardsDelete,
Scope: "dashboards:id:10", Scope: "dashboards:uid:10",
}, },
}, },
expected: false, expected: false,
}, },
{ {
desc: "should not be able to delete with incorrect folder scope", desc: "should not be able to delete with incorrect folder scope",
dashboardID: 1, dashUID: "1",
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{ {
Action: accesscontrol.ActionDashboardsDelete, Action: accesscontrol.ActionDashboardsDelete,
Scope: "folders:id:10", Scope: "folders:uid:10",
}, },
}, },
expected: false, expected: false,
@ -467,7 +461,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) { 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() can, err := guardian.CanDelete()
require.NoError(t, err) require.NoError(t, err)
@ -487,18 +481,18 @@ type accessControlGuardianCanCreateTestCase struct {
func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) { func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
tests := []accessControlGuardianCanCreateTestCase{ tests := []accessControlGuardianCanCreateTestCase{
{ {
desc: "should be able to create dashboard in folder 0", desc: "should be able to create dashboard in general folder",
isFolder: false, isFolder: false,
folderID: 0, folderID: 0,
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:id:0"}, {Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:uid:general"},
}, },
expected: true, expected: true,
}, },
{ {
desc: "should be able to create dashboard in any folder", desc: "should be able to create dashboard in any folder",
isFolder: false, isFolder: false,
folderID: 100, folderID: 0,
permissions: []*accesscontrol.Permission{ permissions: []*accesscontrol.Permission{
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:*"}, {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", desc: "should not be able to create dashboard without permissions",
isFolder: false, isFolder: false,
folderID: 100, folderID: 0,
permissions: []*accesscontrol.Permission{}, permissions: []*accesscontrol.Permission{},
expected: false, expected: false,
}, },
@ -523,7 +517,7 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
{ {
desc: "should not be able to create folders without permissions", desc: "should not be able to create folders without permissions",
isFolder: true, isFolder: true,
folderID: 100, folderID: 0,
permissions: []*accesscontrol.Permission{}, permissions: []*accesscontrol.Permission{},
expected: false, expected: false,
}, },
@ -531,7 +525,7 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) { 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) can, err := guardian.CanCreate(tt.folderID, tt.isFolder)
require.NoError(t, err) require.NoError(t, err)
@ -563,16 +557,14 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, 1, nil) guardian, _ := setupAccessControlGuardianTest(t, "1", nil)
mocked := accesscontrolmock.NewPermissionsServicesMock() mocked := accesscontrolmock.NewPermissionsServicesMock()
guardian.permissionServices = mocked guardian.permissionServices = mocked
mocked.Dashboards.On("MapActions", mock.Anything).Return("View") mocked.Dashboards.On("MapActions", mock.Anything).Return("View")
mocked.Dashboards.On("GetPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tt.permissions, nil) mocked.Dashboards.On("GetPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tt.permissions, nil)
cfg := setting.NewCfg() cfg := setting.NewCfg()
cfg.HiddenUsers = tt.hiddenUsers cfg.HiddenUsers = tt.hiddenUsers
permissions, err := guardian.GetHiddenACL(cfg) permissions, err := guardian.GetHiddenACL(cfg)
require.NoError(t, err) require.NoError(t, err)
var hiddenUserNames []string 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() t.Helper()
store := sqlstore.InitTestDB(t) store := sqlstore.InitTestDB(t)
toSave := models.NewDashboard(uid)
toSave.SetUid(uid)
// seed dashboard // seed dashboard
_, err := dashdb.ProvideDashboardStore(store).SaveDashboard(models.SaveDashboardCommand{ dash, err := dashdb.ProvideDashboardStore(store).SaveDashboard(models.SaveDashboardCommand{
Dashboard: &simplejson.Json{}, Dashboard: toSave.Data,
UserId: 1, UserId: 1,
OrgId: 1, OrgId: 1,
FolderId: 0, FolderId: 0,
}) })
require.NoError(t, err) require.NoError(t, err)
ac := accesscontrolmock.New().WithPermissions(permissions) ac := accesscontrolmock.New().WithPermissions(permissions)
services, err := ossaccesscontrol.ProvidePermissionsServices(setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store)) services, err := ossaccesscontrol.ProvidePermissionsServices(setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store))
require.NoError(t, err) 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/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "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/dashboards"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -311,6 +312,9 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv
dash.Overwrite = true dash.Overwrite = true
dash.OrgId = cfg.OrgID dash.OrgId = cfg.OrgID
// set dashboard folderUid if given // set dashboard folderUid if given
if cfg.FolderUID == accesscontrol.GeneralFolderUID {
return 0, models.ErrFolderInvalidUID
}
dash.Dashboard.SetUid(cfg.FolderUID) dash.Dashboard.SetUid(cfg.FolderUID)
dbDash, err := service.SaveFolderForProvisionedDashboards(ctx, dash) dbDash, err := service.SaveFolderForProvisionedDashboards(ctx, dash)
if err != nil { if err != nil {

View File

@ -390,6 +390,26 @@ func TestDashboardFileReader(t *testing.T) {
require.NoError(t, err) 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) { t.Run("Walking the folder with dashboards", func(t *testing.T) {
setup() setup()
noFiles := map[string]os.FileInfo{} noFiles := map[string]os.FileInfo{}

View File

@ -21,7 +21,6 @@ var dashboardPermissionTranslation = map[models.PermissionType][]string{
models.PERMISSION_EDIT: { models.PERMISSION_EDIT: {
ac.ActionDashboardsRead, ac.ActionDashboardsRead,
ac.ActionDashboardsWrite, ac.ActionDashboardsWrite,
ac.ActionDashboardsCreate,
ac.ActionDashboardsDelete, ac.ActionDashboardsDelete,
}, },
models.PERMISSION_ADMIN: { models.PERMISSION_ADMIN: {
@ -39,6 +38,7 @@ var folderPermissionTranslation = map[models.PermissionType][]string{
dashboards.ActionFoldersRead, dashboards.ActionFoldersRead,
}...), }...),
models.PERMISSION_EDIT: append(dashboardPermissionTranslation[models.PERMISSION_EDIT], []string{ models.PERMISSION_EDIT: append(dashboardPermissionTranslation[models.PERMISSION_EDIT], []string{
ac.ActionDashboardsCreate,
dashboards.ActionFoldersRead, dashboards.ActionFoldersRead,
dashboards.ActionFoldersWrite, dashboards.ActionFoldersWrite,
dashboards.ActionFoldersCreate, dashboards.ActionFoldersCreate,
@ -56,6 +56,7 @@ var folderPermissionTranslation = map[models.PermissionType][]string{
func AddDashboardPermissionsMigrator(mg *migrator.Migrator) { func AddDashboardPermissionsMigrator(mg *migrator.Migrator) {
mg.AddMigration("dashboard permissions", &dashboardPermissionsMigrator{}) mg.AddMigration("dashboard permissions", &dashboardPermissionsMigrator{})
mg.AddMigration("dashboard permissions uid scopes", &dashboardUidPermissionMigrator{})
} }
var _ migrator.CodeMigration = new(dashboardPermissionsMigrator) 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))) 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 { if len(f.dashboardActions) > 0 {
builder.WriteString("((") 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) builder.WriteString(dashFilter.Where)
args = append(args, dashFilter.Args...) 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(dashFolderFilter.Where)
builder.WriteString(") AND NOT dashboard.is_folder)") builder.WriteString(")) AND NOT dashboard.is_folder)")
args = append(args, dashFolderFilter.Args...) args = append(args, dashFolderFilter.Args...)
} }
@ -127,12 +128,11 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
builder.WriteString(" OR ") builder.WriteString(" OR ")
} }
builder.WriteString("(") 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(folderFilter.Where)
builder.WriteString(" AND dashboard.is_folder)") builder.WriteString(" AND dashboard.is_folder)")
args = append(args, folderFilter.Args...) args = append(args, folderFilter.Args...)
} }
builder.WriteString(")") builder.WriteString(")")
return builder.String(), args return builder.String(), args
} }

View File

@ -108,7 +108,7 @@ func TestAccessControlDashboardPermissionFilter_Where(t *testing.T) {
title: "folder and dashboard actions are defined", title: "folder and dashboard actions are defined",
dashboardActions: []string{"test"}, dashboardActions: []string{"test"},
folderActions: []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", 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", title: "dashboard actions are defined but not folder actions",
dashboardActions: []string{"test"}, dashboardActions: []string{"test"},
folderActions: nil, 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", title: "dashboard actions are defined but not folder actions",