Access control: Use access control for dashboard and folder (#44702)

* Add actions and scopes

* add resource service for dashboard and folder

* Add dashboard guardian with fgac permission evaluation

* Add CanDelete function to guardian interface

* Add CanDelete property to folder and dashboard dto and set values

* change to correct function name

* Add accesscontrol to folder endpoints

* add access control to dashboard endpoints

* check access for nav links

* Add fixed roles for dashboard and folders

* use correct package

* add hack to override guardian Constructor if accesscontrol is enabled

* Add services

* Add function to handle api backward compatability

* Add permissionServices to HttpServer

* Set permission when new dashboard is created

* Add default permission when creating new dashboard

* Set default permission when creating folder and dashboard

* Add access control filter for dashboard search

* Add to accept list

* Add accesscontrol to dashboardimport

* Disable access control in tests

* Add check to see if user is allow to create a dashboard

* Use SetPermissions

* Use function to set several permissions at once

* remove permissions for folder and dashboard on delete

* update required permission

* set permission for provisioning

* Add CanCreate to dashboard guardian and set correct permisisons for
provisioning

* Dont set admin on folder / dashboard creation

* Add dashboard and folder permission migrations

* Add tests for CanCreate

* Add roles and update descriptions

* Solve uid to id for dashboard and folder permissions

* Add folder and dashboard actions to permission filter

* Handle viewer_can_edit flag

* set folder and dashboard permissions services

* Add dashboard permissions when importing a new dashboard

* Set access control permissions on provisioning

* Pass feature flags and only set permissions if access control is enabled

* only add default permissions for folders and dashboards without folders

* Batch create permissions in migrations


* Remove `dashboards:edit` action

* Remove unused function from interface

* Update pkg/services/guardian/accesscontrol_guardian_test.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Karl Persson 2022-03-03 15:05:47 +01:00 committed by GitHub
parent 4caf5dbbd9
commit 4982ca3b1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 2074 additions and 319 deletions

View File

@ -262,10 +262,111 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{string(models.ROLE_VIEWER)}, Grants: []string{string(models.ROLE_VIEWER)},
} }
dashboardsCreatorRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 1,
Name: "fixed:dashboards:creator",
DisplayName: "Dashboard creator",
Description: "Create dashboard in general folder.",
Group: "Dashboards",
Permissions: []ac.Permission{
{Action: ac.ActionFoldersRead, Scope: ac.Scope("folders", "id", "0")},
{Action: ac.ActionDashboardsCreate, Scope: ac.Scope("folders", "id", "0")},
},
},
Grants: []string{"Editor"},
}
dashboardsReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 1,
Name: "fixed:dashboards:reader",
DisplayName: "Dashboard reader",
Description: "Read all dashboards.",
Group: "Dashboards",
Permissions: []ac.Permission{
{Action: ac.ActionDashboardsRead, Scope: ac.ScopeDashboardsAll},
},
},
Grants: []string{"Admin"},
}
dashboardsWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 1,
Name: "fixed:dashboards:writer",
DisplayName: "Dashboard writer",
Group: "Dashboards",
Description: "Create, read, write or delete all dashboards and their permissions.",
Permissions: ac.ConcatPermissions(dashboardsReaderRole.Role.Permissions, []ac.Permission{
{Action: ac.ActionDashboardsWrite, Scope: ac.ScopeDashboardsAll},
{Action: ac.ActionDashboardsDelete, Scope: ac.ScopeDashboardsAll},
{Action: ac.ActionDashboardsCreate, Scope: ac.ScopeFoldersAll},
{Action: ac.ActionDashboardsPermissionsRead, Scope: ac.ScopeDashboardsAll},
{Action: ac.ActionDashboardsPermissionsWrite, Scope: ac.ScopeDashboardsAll},
}),
},
Grants: []string{"Admin"},
}
foldersCreatorRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 1,
Name: "fixed:folders:creator",
DisplayName: "Folder creator",
Description: "Create folders.",
Group: "Folders",
Permissions: []ac.Permission{
{Action: ac.ActionFoldersCreate},
},
},
Grants: []string{"Editor"},
}
foldersReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 1,
Name: "fixed:folders:reader",
DisplayName: "Folder reader",
Description: "Read all folders and dashboards.",
Group: "Folders",
Permissions: []ac.Permission{
{Action: ac.ActionFoldersRead, Scope: ac.ScopeFoldersAll},
{Action: ac.ActionDashboardsRead, Scope: ac.ScopeFoldersAll},
},
},
Grants: []string{"Admin"},
}
foldersWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Version: 1,
Name: "fixed:folders:writer",
DisplayName: "Folder writer",
Description: "Create, read, write or delete all folders and dashboards and their permissions.",
Group: "Folders",
Permissions: ac.ConcatPermissions(
foldersReaderRole.Role.Permissions,
[]ac.Permission{
{Action: ac.ActionFoldersCreate},
{Action: ac.ActionFoldersWrite, Scope: ac.ScopeFoldersAll},
{Action: ac.ActionFoldersDelete, Scope: ac.ScopeFoldersAll},
{Action: ac.ActionDashboardsWrite, Scope: ac.ScopeFoldersAll},
{Action: ac.ActionDashboardsDelete, Scope: ac.ScopeFoldersAll},
{Action: ac.ActionDashboardsCreate, Scope: ac.ScopeFoldersAll},
{Action: ac.ActionDashboardsPermissionsRead, Scope: ac.ScopeFoldersAll},
{Action: ac.ActionDashboardsPermissionsWrite, Scope: ac.ScopeFoldersAll},
}),
},
Grants: []string{"Admin"},
}
return hs.AccessControl.DeclareFixedRoles( return hs.AccessControl.DeclareFixedRoles(
provisioningWriterRole, datasourcesReaderRole, datasourcesWriterRole, datasourcesIdReaderRole, provisioningWriterRole, datasourcesReaderRole, datasourcesWriterRole, datasourcesIdReaderRole,
datasourcesCompatibilityReaderRole, orgReaderRole, orgWriterRole, orgMaintainerRole, teamsCreatorRole, datasourcesCompatibilityReaderRole, orgReaderRole, orgWriterRole,
teamsWriterRole, datasourcesExplorerRole, annotationsReaderRole, orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole, annotationsReaderRole,
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
foldersCreatorRole, foldersReaderRole, foldersWriterRole,
) )
} }

View File

@ -313,26 +313,26 @@ func (hs *HTTPServer) registerRoutes() {
// Folders // Folders
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) { apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
folderRoute.Get("/", routing.Wrap(hs.GetFolders)) folderRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
folderRoute.Get("/id/:id", routing.Wrap(hs.GetFolderByID)) folderRoute.Get("/id/:id", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersRead, ac.ScopeFolderID)), routing.Wrap(hs.GetFolderByID))
folderRoute.Post("/", routing.Wrap(hs.CreateFolder)) folderRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", routing.Wrap(hs.GetFolderByUID)) folderUidRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersRead)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Put("/", routing.Wrap(hs.UpdateFolder)) folderUidRoute.Put("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersWrite)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Delete("/", routing.Wrap(hs.DeleteFolder)) folderUidRoute.Delete("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersDelete)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) { folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", routing.Wrap(hs.GetFolderPermissionList)) folderPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersPermissionsRead)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", routing.Wrap(hs.UpdateFolderPermissions)) folderPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersPermissionsWrite)), routing.Wrap(hs.UpdateFolderPermissions))
}) })
}) })
}) })
// Dashboard // Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) { apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {
dashboardRoute.Get("/uid/:uid", routing.Wrap(hs.GetDashboard)) dashboardRoute.Get("/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsRead)), routing.Wrap(hs.GetDashboard))
dashboardRoute.Delete("/uid/:uid", routing.Wrap(hs.DeleteDashboardByUID)) dashboardRoute.Delete("/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsDelete)), routing.Wrap(hs.DeleteDashboardByUID))
if hs.ThumbService != nil { if hs.ThumbService != nil {
dashboardRoute.Get("/uid/:uid/img/:kind/:theme", hs.ThumbService.GetImage) dashboardRoute.Get("/uid/:uid/img/:kind/:theme", hs.ThumbService.GetImage)
@ -343,21 +343,21 @@ func (hs *HTTPServer) registerRoutes() {
} }
} }
dashboardRoute.Post("/calculate-diff", routing.Wrap(hs.CalculateDashboardDiff)) dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard)) dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
dashboardRoute.Post("/db", routing.Wrap(hs.PostDashboard)) dashboardRoute.Post("/db", authorize(reqSignedIn, ac.EvalAny(ac.EvalPermission(ac.ActionDashboardsCreate), ac.EvalPermission(ac.ActionDashboardsWrite))), routing.Wrap(hs.PostDashboard))
dashboardRoute.Get("/home", routing.Wrap(hs.GetHomeDashboard)) dashboardRoute.Get("/home", routing.Wrap(hs.GetHomeDashboard))
dashboardRoute.Get("/tags", hs.GetDashboardTags) dashboardRoute.Get("/tags", hs.GetDashboardTags)
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) { dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
dashIdRoute.Get("/versions", routing.Wrap(hs.GetDashboardVersions)) dashIdRoute.Get("/versions", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions))
dashIdRoute.Get("/versions/:id", routing.Wrap(hs.GetDashboardVersion)) dashIdRoute.Get("/versions/:id", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion))
dashIdRoute.Post("/restore", routing.Wrap(hs.RestoreDashboardVersion)) dashIdRoute.Post("/restore", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", routing.Wrap(hs.GetDashboardPermissionList)) dashboardPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", routing.Wrap(hs.UpdateDashboardPermissions)) dashboardPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsPermissionsWrite)), routing.Wrap(hs.UpdateDashboardPermissions))
}) })
}) })
}) })

View File

@ -17,8 +17,10 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"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/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
@ -100,6 +102,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
canEdit, _ := guardian.CanEdit() canEdit, _ := guardian.CanEdit()
canSave, _ := guardian.CanSave() canSave, _ := guardian.CanSave()
canAdmin, _ := guardian.CanAdmin() canAdmin, _ := guardian.CanAdmin()
canDelete, _ := guardian.CanDelete()
isStarred, err := hs.isDashboardStarredByUser(c, dash.Id) isStarred, err := hs.isDashboardStarredByUser(c, dash.Id)
if err != nil { if err != nil {
@ -122,6 +125,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
CanSave: canSave, CanSave: canSave,
CanEdit: canEdit, CanEdit: canEdit,
CanAdmin: canAdmin, CanAdmin: canAdmin,
CanDelete: canDelete,
Created: dash.Created, Created: dash.Created,
Updated: dash.Updated, Updated: dash.Updated,
UpdatedBy: updater, UpdatedBy: updater,
@ -223,7 +227,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
return rsp return rsp
} }
guardian := guardian.New(c.Req.Context(), dash.Id, c.OrgId, c.SignedInUser) guardian := guardian.New(c.Req.Context(), dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave { if canDelete, err := guardian.CanDelete(); err != nil || !canDelete {
return dashboardGuardianResponse(err) return dashboardGuardianResponse(err)
} }
@ -356,10 +360,8 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, err) return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, err)
} }
if hs.Cfg.EditorsCanAdmin && newDashboard { if newDashboard {
inFolder := cmd.FolderId > 0 if err := hs.setDashboardPermissions(c, cmd, dashboard); err != nil {
err := hs.dashboardService.MakeUserAdmin(ctx, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
if err != nil {
hs.log.Error("Could not make user admin", "dashboard", dashboard.Title, "user", c.SignedInUser.UserId, "error", err) hs.log.Error("Could not make user admin", "dashboard", dashboard.Title, "user", c.SignedInUser.UserId, "error", err)
} }
} }
@ -381,6 +383,35 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
}) })
} }
func (hs *HTTPServer) setDashboardPermissions(c *models.ReqContext, cmd models.SaveDashboardCommand, dash *models.Dashboard) error {
inFolder := dash.FolderId > 0
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
resourceID := strconv.FormatInt(dash.Id, 10)
svc := hs.permissionServices.GetDashboardService()
permissions := []accesscontrol.SetResourcePermissionCommand{
{UserID: c.UserId, Permission: models.PERMISSION_ADMIN.String()},
}
if !inFolder {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()},
{BuiltinRole: string(models.ROLE_VIEWER), Permission: models.PERMISSION_VIEW.String()},
}...)
}
_, err := svc.SetPermissions(c.Req.Context(), c.OrgId, resourceID, permissions...)
if err != nil {
return err
}
} else if hs.Cfg.EditorsCanAdmin {
if err := hs.dashboardService.MakeUserAdmin(c.Req.Context(), cmd.OrgId, cmd.UserId, dash.Id, !inFolder); err != nil {
return err
}
}
return nil
}
// GetHomeDashboard returns the home dashboard. // GetHomeDashboard returns the home dashboard.
func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response {
prefsQuery := models.GetPreferencesWithDefaultsQuery{User: c.SignedInUser} prefsQuery := models.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
@ -9,6 +10,8 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"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/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@ -70,7 +73,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
return response.Error(http.StatusBadRequest, "dashboardId is invalid", err) return response.Error(http.StatusBadRequest, "dashboardId is invalid", err)
} }
_, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgId, dashID, "") dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgId, dashID, "")
if rsp != nil { if rsp != nil {
return rsp return rsp
} }
@ -112,6 +115,17 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
return response.Error(403, "Cannot remove own admin permission for a folder", nil) return response.Error(403, "Cannot remove own admin permission for a folder", nil)
} }
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
old, err := g.GetAcl()
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 {
return response.Error(500, "Failed to update permissions", err)
}
return response.Success("Dashboard permissions updated")
}
if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil { if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil {
if errors.Is(err, models.ErrDashboardAclInfoMissing) || if errors.Is(err, models.ErrDashboardAclInfoMissing) ||
errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) { errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) {
@ -123,6 +137,64 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
return response.Success("Dashboard permissions updated") return response.Success("Dashboard permissions updated")
} }
// 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 {
commands := []accesscontrol.SetResourcePermissionCommand{}
for _, item := range items {
permissions := item.Permission.String()
role := ""
if item.Role != nil {
role = string(*item.Role)
}
commands = append(commands, accesscontrol.SetResourcePermissionCommand{
UserID: item.UserID,
TeamID: item.TeamID,
BuiltinRole: role,
Permission: permissions,
})
}
for _, o := range old {
shouldRemove := true
for _, item := range items {
if item.UserID != 0 && item.UserID == o.UserId {
shouldRemove = false
break
}
if item.TeamID != 0 && item.TeamID == o.TeamId {
shouldRemove = false
break
}
if item.Role != nil && o.Role != nil && *item.Role == *o.Role {
shouldRemove = false
break
}
}
if shouldRemove {
role := ""
if o.Role != nil {
role = string(*o.Role)
}
commands = append(commands, accesscontrol.SetResourcePermissionCommand{
UserID: o.UserId,
TeamID: o.TeamId,
BuiltinRole: role,
Permission: "",
})
}
}
svc := hs.permissionServices.GetDashboardService()
if isFolder {
svc = hs.permissionServices.GetFolderService()
}
_, err := svc.SetPermissions(ctx, orgID, strconv.FormatInt(dashID, 10), commands...)
return err
}
func validatePermissionsUpdate(apiCmd dtos.UpdateDashboardAclCommand) error { func validatePermissionsUpdate(apiCmd dtos.UpdateDashboardAclCommand) error {
for _, item := range apiCmd.Items { for _, item := range apiCmd.Items {
if item.UserID > 0 && item.TeamID > 0 { if item.UserID > 0 && item.TeamID > 0 {

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -31,6 +32,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
Cfg: settings, Cfg: settings,
dashboardService: dashboardservice.ProvideDashboardService(dashboardStore, nil), dashboardService: dashboardservice.ProvideDashboardService(dashboardStore, nil),
SQLStore: mockSQLStore, SQLStore: mockSQLStore,
Features: featuremgmt.WithFeatures(),
} }
t.Run("Given user has no admin permissions", func(t *testing.T) { t.Run("Given user has no admin permissions", func(t *testing.T) {

View File

@ -118,6 +118,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
Cfg: setting.NewCfg(), Cfg: setting.NewCfg(),
pluginStore: &fakePluginStore{}, pluginStore: &fakePluginStore{},
SQLStore: mockSQLStore, SQLStore: mockSQLStore,
Features: featuremgmt.WithFeatures(),
} }
hs.SQLStore = mockSQLStore hs.SQLStore = mockSQLStore
@ -1026,6 +1027,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
LibraryElementService: &mockLibraryElementService{}, LibraryElementService: &mockLibraryElementService{},
dashboardService: dashboardService, dashboardService: dashboardService,
folderService: folderService, folderService: folderService,
Features: featuremgmt.WithFeatures(),
} }
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
@ -1096,6 +1098,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
LibraryElementService: &mockLibraryElementService{}, LibraryElementService: &mockLibraryElementService{},
dashboardService: mock, dashboardService: mock,
SQLStore: sqlStore, SQLStore: sqlStore,
Features: featuremgmt.WithFeatures(),
} }
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)

View File

@ -15,6 +15,7 @@ type DashboardMeta struct {
CanEdit bool `json:"canEdit"` CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"` CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"` CanStar bool `json:"canStar"`
CanDelete bool `json:"canDelete"`
Slug string `json:"slug"` Slug string `json:"slug"`
Url string `json:"url"` Url string `json:"url"`
Expires time.Time `json:"expires"` Expires time.Time `json:"expires"`

View File

@ -11,6 +11,7 @@ type Folder struct {
CanSave bool `json:"canSave"` CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"` CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"` CanAdmin bool `json:"canAdmin"`
CanDelete bool `json:"canDelete"`
CreatedBy string `json:"createdBy"` CreatedBy string `json:"createdBy"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
UpdatedBy string `json:"updatedBy"` UpdatedBy string `json:"updatedBy"`

View File

@ -11,6 +11,8 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"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/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
@ -71,17 +73,36 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
return apierrors.ToFolderErrorResponse(err) return apierrors.ToFolderErrorResponse(err)
} }
if hs.Cfg.EditorsCanAdmin { if err := hs.setFolderPermission(c, folder.Id); err != nil {
if err := hs.folderService.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil { hs.log.Error("Could not make user admin", "folder", folder.Title, "user",
hs.log.Error("Could not make user admin", "folder", folder.Title, "user", c.SignedInUser.UserId, "error", err)
c.SignedInUser.UserId, "error", err)
}
} }
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser) g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
return response.JSON(200, hs.toFolderDto(c.Req.Context(), g, folder)) return response.JSON(200, hs.toFolderDto(c.Req.Context(), g, folder))
} }
func (hs *HTTPServer) setFolderPermission(c *models.ReqContext, folderID int64) error {
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
resourceID := strconv.FormatInt(folderID, 10)
svc := hs.permissionServices.GetFolderService()
_, err := svc.SetPermissions(c.Req.Context(), c.OrgId, resourceID, []accesscontrol.SetResourcePermissionCommand{
{UserID: c.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()},
}...)
if err != nil {
return err
}
} else if hs.Cfg.EditorsCanAdmin {
if err := hs.folderService.MakeUserAdmin(c.Req.Context(), c.OrgId, c.UserId, folderID, true); err != nil {
return err
}
}
return nil
}
func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
cmd := models.UpdateFolderCommand{} cmd := models.UpdateFolderCommand{}
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
@ -121,6 +142,7 @@ func (hs *HTTPServer) toFolderDto(ctx context.Context, g guardian.DashboardGuard
canEdit, _ := g.CanEdit() canEdit, _ := g.CanEdit()
canSave, _ := g.CanSave() canSave, _ := g.CanSave()
canAdmin, _ := g.CanAdmin() canAdmin, _ := g.CanAdmin()
canDelete, _ := g.CanDelete()
// Finding creator and last updater of the folder // Finding creator and last updater of the folder
updater, creator := anonString, anonString updater, creator := anonString, anonString
@ -140,6 +162,7 @@ func (hs *HTTPServer) toFolderDto(ctx context.Context, g guardian.DashboardGuard
CanSave: canSave, CanSave: canSave,
CanEdit: canEdit, CanEdit: canEdit,
CanAdmin: canAdmin, CanAdmin: canAdmin,
CanDelete: canDelete,
CreatedBy: creator, CreatedBy: creator,
Created: folder.Created, Created: folder.Created,
UpdatedBy: updater, UpdatedBy: updater,

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
@ -114,6 +115,17 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res
return response.Error(403, "Cannot remove own admin permission for a folder", nil) return response.Error(403, "Cannot remove own admin permission for a folder", nil)
} }
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
old, err := g.GetAcl()
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 {
return response.Error(500, "Failed to create permission", err)
}
return response.Success("Dashboard permissions updated")
}
if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil { if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil {
if errors.Is(err, models.ErrDashboardAclInfoMissing) { if errors.Is(err, models.ErrDashboardAclInfoMissing) {
err = models.ErrFolderAclInfoMissing err = models.ErrFolderAclInfoMissing

View File

@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/dashboards/database"
service "github.com/grafana/grafana/pkg/services/dashboards/manager" service "github.com/grafana/grafana/pkg/services/dashboards/manager"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -24,13 +25,14 @@ import (
func TestFolderPermissionAPIEndpoint(t *testing.T) { func TestFolderPermissionAPIEndpoint(t *testing.T) {
settings := setting.NewCfg() settings := setting.NewCfg()
folderService := &dashboards.FakeFolderService{} folderService := &dashboards.FakeFolderService{}
defer folderService.AssertExpectations(t) defer folderService.AssertExpectations(t)
dashboardStore := &database.FakeDashboardStore{} dashboardStore := &database.FakeDashboardStore{}
defer dashboardStore.AssertExpectations(t) defer dashboardStore.AssertExpectations(t)
hs := &HTTPServer{Cfg: settings, folderService: folderService, dashboardService: service.ProvideDashboardService(dashboardStore, nil)} hs := &HTTPServer{Cfg: settings, folderService: folderService, dashboardService: service.ProvideDashboardService(dashboardStore, nil), Features: featuremgmt.WithFeatures()}
t.Run("Given folder not exists", func(t *testing.T) { t.Run("Given folder not exists", func(t *testing.T) {
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderNotFound).Twice() folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderNotFound).Twice()

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
@ -143,6 +144,7 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st
Bus: bus.GetBus(), Bus: bus.GetBus(),
Cfg: setting.NewCfg(), Cfg: setting.NewCfg(),
folderService: folderService, folderService: folderService,
Features: featuremgmt.WithFeatures(),
} }
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)

View File

@ -132,6 +132,7 @@ type HTTPServer struct {
serviceAccountsService serviceaccounts.Service serviceAccountsService serviceaccounts.Service
authInfoService login.AuthInfoService authInfoService login.AuthInfoService
teamPermissionsService accesscontrol.PermissionsService teamPermissionsService accesscontrol.PermissionsService
permissionServices accesscontrol.PermissionsServices
NotificationService *notifications.NotificationService NotificationService *notifications.NotificationService
dashboardService dashboards.DashboardService dashboardService dashboards.DashboardService
dashboardProvisioningService dashboards.DashboardProvisioningService dashboardProvisioningService dashboards.DashboardProvisioningService
@ -241,6 +242,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
AlertNotificationService: alertNotificationService, AlertNotificationService: alertNotificationService,
DashboardsnapshotsService: dashboardsnapshotsService, DashboardsnapshotsService: dashboardsnapshotsService,
PluginSettings: pluginSettings, PluginSettings: pluginSettings,
permissionServices: permissionsServices,
} }
if hs.Listener != nil { if hs.Listener != nil {
hs.log.Debug("Using provided listener") hs.log.Debug("Using provided listener")

View File

@ -473,19 +473,26 @@ func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext, uaVisibleForOrg b
} }
func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink { func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink {
children := []*dtos.NavLink{ hasAccess := ac.HasAccess(hs.AccessControl, c)
{Text: "Dashboard", Icon: "apps", Url: hs.Cfg.AppSubURL + "/dashboard/new", Id: "create-dashboard"}, var children []*dtos.NavLink
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(ac.ActionDashboardsCreate)) {
children = append(children, &dtos.NavLink{Text: "Dashboard", Icon: "apps", Url: hs.Cfg.AppSubURL + "/dashboard/new", Id: "create-dashboard"})
} }
if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalPermission(ac.ActionFoldersCreate)) {
children = append(children, &dtos.NavLink{ children = append(children, &dtos.NavLink{
Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder",
Icon: "folder-plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new", Icon: "folder-plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new",
}) })
} }
children = append(children, &dtos.NavLink{
Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "import", if hasAccess(ac.ReqSignedIn, ac.EvalPermission(ac.ActionDashboardsCreate)) {
Url: hs.Cfg.AppSubURL + "/dashboard/import", children = append(children, &dtos.NavLink{
}) Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "import",
Url: hs.Cfg.AppSubURL + "/dashboard/import",
})
}
_, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgId] _, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgId]
uaVisibleForOrg := hs.Cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg uaVisibleForOrg := hs.Cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg
@ -539,11 +546,14 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
} }
func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) { func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) {
hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} hasAccess := ac.HasAccess(hs.AccessControl, c)
if err := hs.SQLStore.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { hasEditPerm := hasAccess(func(context *models.ReqContext) bool {
return nil, err hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
} if err := hs.SQLStore.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil {
hasEditPerm := hasEditPermissionInFoldersQuery.Result return false
}
return hasEditPermissionInFoldersQuery.Result
}, ac.EvalAny(ac.EvalPermission(ac.ActionDashboardsCreate), ac.EvalPermission(ac.ActionFoldersCreate)))
settings, err := hs.getFrontendSettingsMap(c) settings, err := hs.getFrontendSettingsMap(c)
if err != nil { if err != nil {

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/live/pushhttp" "github.com/grafana/grafana/pkg/services/live/pushhttp"
"github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert"
@ -35,7 +36,7 @@ func ProvideBackgroundServiceRegistry(
remoteCache *remotecache.RemoteCache, thumbnailsService thumbs.Service, remoteCache *remotecache.RemoteCache, thumbnailsService thumbs.Service,
// Need to make sure these are initialized, is there a better place to put them? // Need to make sure these are initialized, is there a better place to put them?
_ *plugindashboards.Service, _ *dashboardsnapshots.Service, _ *plugindashboards.Service, _ *dashboardsnapshots.Service,
_ *alerting.AlertNotificationService, _ serviceaccounts.Service, _ *alerting.AlertNotificationService, _ serviceaccounts.Service, _ *guardian.Provider,
) *BackgroundServiceRegistry { ) *BackgroundServiceRegistry {
return NewBackgroundServiceRegistry( return NewBackgroundServiceRegistry(
httpServer, httpServer,

View File

@ -43,6 +43,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/librarypanels"
@ -219,6 +220,7 @@ var wireBasicSet = wire.NewSet(
alerting.ProvideDashAlertExtractorService, alerting.ProvideDashAlertExtractorService,
wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)), wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)),
comments.ProvideService, comments.ProvideService,
guardian.ProvideService,
) )
var wireSet = wire.NewSet( var wireSet = wire.NewSet(

View File

@ -78,7 +78,7 @@ var wireExtsBasicSet = wire.NewSet(
permissions.ProvideDatasourcePermissionsService, permissions.ProvideDatasourcePermissionsService,
wire.Bind(new(permissions.DatasourcePermissionsService), new(*permissions.OSSDatasourcePermissionsService)), wire.Bind(new(permissions.DatasourcePermissionsService), new(*permissions.OSSDatasourcePermissionsService)),
ossaccesscontrol.ProvidePermissionsServices, ossaccesscontrol.ProvidePermissionsServices,
wire.Bind(new(accesscontrol.PermissionsServices), new(*ossaccesscontrol.PermissionsService)), wire.Bind(new(accesscontrol.PermissionsServices), new(*ossaccesscontrol.PermissionsServices)),
) )
var wireExtsSet = wire.NewSet( var wireExtsSet = wire.NewSet(

View File

@ -39,6 +39,8 @@ type PermissionsProvider interface {
type PermissionsServices interface { type PermissionsServices interface {
GetTeamService() PermissionsService GetTeamService() PermissionsService
GetFolderService() PermissionsService
GetDashboardService() PermissionsService
GetDataSourceService() PermissionsService GetDataSourceService() PermissionsService
} }
@ -53,6 +55,8 @@ type PermissionsService interface {
SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*ResourcePermission, error) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*ResourcePermission, error)
// SetPermissions sets several permissions on resource for either built-in role, team or user // SetPermissions sets several permissions on resource for either built-in role, team or user
SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...SetResourcePermissionCommand) ([]ResourcePermission, error) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...SetResourcePermissionCommand) ([]ResourcePermission, error)
// MapActions will map actions for a ResourcePermissions to it's "friendly" name configured in PermissionsToActions map.
MapActions(permission ResourcePermission) string
} }
type User struct { type User struct {
@ -101,6 +105,10 @@ func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*model
} }
} }
var ReqSignedIn = func(c *models.ReqContext) bool {
return c.IsSignedIn
}
var ReqGrafanaAdmin = func(c *models.ReqContext) bool { var ReqGrafanaAdmin = func(c *models.ReqContext) bool {
return c.IsGrafanaAdmin return c.IsGrafanaAdmin
} }
@ -109,6 +117,10 @@ var ReqOrgAdmin = func(c *models.ReqContext) bool {
return c.OrgRole == models.ROLE_ADMIN return c.OrgRole == models.ROLE_ADMIN
} }
var ReqOrgAdminOrEditor = func(c *models.ReqContext) bool {
return c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR
}
func BuildPermissionsMap(permissions []*Permission) map[string]bool { func BuildPermissionsMap(permissions []*Permission) map[string]bool {
permissionsMap := make(map[string]bool) permissionsMap := make(map[string]bool)
for _, p := range permissions { for _, p := range permissions {

View File

@ -10,13 +10,15 @@ 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.folder_id": {},
} }
var ( var (

View File

@ -9,12 +9,16 @@ var _ accesscontrol.PermissionsServices = new(PermissionsServicesMock)
func NewPermissionsServicesMock() *PermissionsServicesMock { func NewPermissionsServicesMock() *PermissionsServicesMock {
return &PermissionsServicesMock{ return &PermissionsServicesMock{
teams: &MockPermissionsService{}, teams: &MockPermissionsService{},
folders: &MockPermissionsService{},
dashboards: &MockPermissionsService{},
datasources: &MockPermissionsService{}, datasources: &MockPermissionsService{},
} }
} }
type PermissionsServicesMock struct { type PermissionsServicesMock struct {
teams *MockPermissionsService teams *MockPermissionsService
folders *MockPermissionsService
dashboards *MockPermissionsService
datasources *MockPermissionsService datasources *MockPermissionsService
} }
@ -22,6 +26,14 @@ func (p PermissionsServicesMock) GetTeamService() accesscontrol.PermissionsServi
return p.teams return p.teams
} }
func (p PermissionsServicesMock) GetFolderService() accesscontrol.PermissionsService {
return p.folders
}
func (p PermissionsServicesMock) GetDashboardService() accesscontrol.PermissionsService {
return p.dashboards
}
func (p PermissionsServicesMock) GetDataSourceService() accesscontrol.PermissionsService { func (p PermissionsServicesMock) GetDataSourceService() accesscontrol.PermissionsService {
return p.datasources return p.datasources
} }

View File

@ -39,3 +39,8 @@ func (m *MockPermissionsService) SetPermissions(ctx context.Context, orgID int64
mockedArgs := m.Called(ctx, orgID, resourceID, commands) mockedArgs := m.Called(ctx, orgID, resourceID, commands)
return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1) return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1)
} }
func (m *MockPermissionsService) MapActions(permission accesscontrol.ResourcePermission) string {
mockedArgs := m.Called(permission)
return mockedArgs.Get(0).(string)
}

View File

@ -323,17 +323,41 @@ const (
ScopeTeamsAll = "teams:*" ScopeTeamsAll = "teams:*"
// Annotations related actions // Annotations related actions
ActionAnnotationsRead = "annotations:read" ActionAnnotationsRead = "annotations:read"
ActionAnnotationsTagsRead = "annotations.tags:read" ActionAnnotationsTagsRead = "annotations.tags:read"
ScopeAnnotationsAll = "annotations:*" ScopeAnnotationsAll = "annotations:*"
ScopeAnnotationsTagsAll = "annotations:tags:*" ScopeAnnotationsTagsAll = "annotations:tags:*"
// Dashboard actions
ActionDashboardsCreate = "dashboards:create"
ActionDashboardsRead = "dashboards:read"
ActionDashboardsWrite = "dashboards:write"
ActionDashboardsDelete = "dashboards:delete"
ActionDashboardsPermissionsRead = "dashboards.permissions:read"
ActionDashboardsPermissionsWrite = "dashboards.permissions:write"
// Dashboard scopes
ScopeDashboardsAll = "dashboards:*"
// Folder actions
ActionFoldersCreate = "folders:create"
ActionFoldersRead = "folders:read"
ActionFoldersWrite = "folders:write"
ActionFoldersDelete = "folders:delete"
ActionFoldersPermissionsRead = "folders.permissions:read"
ActionFoldersPermissionsWrite = "folders.permissions:write"
// Folder scopes
ScopeFoldersAll = "folders:*"
) )
var ( var (
// Team scope // Team scope
ScopeTeamsID = Scope("teams", "id", Parameter(":teamId")) ScopeTeamsID = Scope("teams", "id", Parameter(":teamId"))
// Folder scopes
ScopeFolderID = Scope("folders", "id", Parameter(":id"))
) )
const RoleGrafanaAdmin = "Grafana Admin" const RoleGrafanaAdmin = "Grafana Admin"

View File

@ -115,7 +115,7 @@ func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user
OrgID: user.OrgId, OrgID: user.OrgId,
UserID: user.UserId, UserID: user.UserId,
Roles: ac.GetUserBuiltInRoles(user), Roles: ac.GetUserBuiltInRoles(user),
Actions: TeamAdminActions, Actions: append(TeamAdminActions, append(DashboardAdminActions, FolderAdminActions...)...),
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -2,6 +2,7 @@ package ossaccesscontrol
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strconv" "strconv"
@ -12,25 +13,48 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
) )
func ProvidePermissionsServices(router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, store resourcepermissions.Store) (*PermissionsService, error) { func ProvidePermissionsServices(router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, store resourcepermissions.Store) (*PermissionsServices, error) {
teamPermissions, err := ProvideTeamPermissions(router, sql, ac, store) teamPermissions, err := ProvideTeamPermissions(router, sql, ac, store)
if err != nil { if err != nil {
return nil, err return nil, err
} }
folderPermissions, err := provideFolderService(router, sql, ac, store)
if err != nil {
return nil, err
}
dashboardPermissions, err := provideDashboardService(router, sql, ac, store)
if err != nil {
return nil, err
}
return &PermissionsService{teams: teamPermissions, datasources: provideEmptyPermissionsService()}, nil return &PermissionsServices{
teams: teamPermissions,
folder: folderPermissions,
dashboard: dashboardPermissions,
datasources: provideEmptyPermissionsService(),
}, nil
} }
type PermissionsService struct { type PermissionsServices struct {
teams accesscontrol.PermissionsService teams accesscontrol.PermissionsService
folder accesscontrol.PermissionsService
dashboard accesscontrol.PermissionsService
datasources accesscontrol.PermissionsService datasources accesscontrol.PermissionsService
} }
func (s *PermissionsService) GetTeamService() accesscontrol.PermissionsService { func (s *PermissionsServices) GetTeamService() accesscontrol.PermissionsService {
return s.teams return s.teams
} }
func (s *PermissionsService) GetDataSourceService() accesscontrol.PermissionsService { func (s *PermissionsServices) GetFolderService() accesscontrol.PermissionsService {
return s.folder
}
func (s *PermissionsServices) GetDashboardService() accesscontrol.PermissionsService {
return s.dashboard
}
func (s *PermissionsServices) GetDataSourceService() accesscontrol.PermissionsService {
return s.datasources return s.datasources
} }
@ -105,6 +129,107 @@ func ProvideTeamPermissions(router routing.RouteRegister, sql *sqlstore.SQLStore
return resourcepermissions.New(options, router, ac, store, sql) return resourcepermissions.New(options, router, ac, store, sql)
} }
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{accesscontrol.ActionFoldersRead}
var FolderEditActions = append(FolderViewActions, []string{accesscontrol.ActionFoldersWrite, accesscontrol.ActionFoldersDelete, accesscontrol.ActionDashboardsCreate}...)
var FolderAdminActions = append(FolderEditActions, []string{accesscontrol.ActionFoldersPermissionsRead, accesscontrol.ActionFoldersPermissionsWrite}...)
func provideDashboardService(router routing.RouteRegister, sql *sqlstore.SQLStore, accesscontrol accesscontrol.AccessControl, store resourcepermissions.Store) (*resourcepermissions.Service, error) {
options := resourcepermissions.Options{
Resource: "dashboards",
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}
if err := sql.GetDashboard(ctx, query); err != nil {
return err
}
if query.Result.IsFolder {
return errors.New("not found")
}
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,
BuiltInRoles: true,
},
PermissionsToActions: map[string][]string{
"View": DashboardViewActions,
"Edit": DashboardEditActions,
"Admin": DashboardAdminActions,
},
ReaderRoleName: "Dashboard permission reader",
WriterRoleName: "Dashboard permission writer",
RoleGroup: "Dashboards",
}
return resourcepermissions.New(options, router, accesscontrol, store, sql)
}
func provideFolderService(router routing.RouteRegister, sql *sqlstore.SQLStore, accesscontrol accesscontrol.AccessControl, store resourcepermissions.Store) (*resourcepermissions.Service, error) {
options := resourcepermissions.Options{
Resource: "folders",
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}
if err := sql.GetDashboard(ctx, query); err != nil {
return err
}
if !query.Result.IsFolder {
return errors.New("not found")
}
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,
BuiltInRoles: true,
},
PermissionsToActions: map[string][]string{
"View": append(DashboardViewActions, FolderViewActions...),
"Edit": append(DashboardEditActions, FolderEditActions...),
"Admin": append(DashboardAdminActions, FolderAdminActions...),
},
ReaderRoleName: "Folder permission reader",
WriterRoleName: "Folder permission writer",
RoleGroup: "Folders",
}
return resourcepermissions.New(options, router, accesscontrol, store, sql)
}
func provideEmptyPermissionsService() accesscontrol.PermissionsService { func provideEmptyPermissionsService() accesscontrol.PermissionsService {
return &emptyPermissionsService{} return &emptyPermissionsService{}
} }
@ -132,3 +257,7 @@ func (e emptyPermissionsService) SetBuiltInRolePermission(ctx context.Context, o
func (e emptyPermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) { func (e emptyPermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) {
return nil, nil return nil, nil
} }
func (e emptyPermissionsService) MapActions(permission accesscontrol.ResourcePermission) string {
return ""
}

View File

@ -89,7 +89,7 @@ func (a *api) getPermissions(c *models.ReqContext) response.Response {
dto := make([]resourcePermissionDTO, 0, len(permissions)) dto := make([]resourcePermissionDTO, 0, len(permissions))
for _, p := range permissions { for _, p := range permissions {
if permission := a.service.mapActions(p); permission != "" { if permission := a.service.MapActions(p); permission != "" {
teamAvatarUrl := "" teamAvatarUrl := ""
if p.TeamId != 0 { if p.TeamId != 0 {
teamAvatarUrl = dtos.GetGravatarUrlWithDefault(p.TeamEmail, p.Team) teamAvatarUrl = dtos.GetGravatarUrlWithDefault(p.TeamEmail, p.Team)

View File

@ -223,7 +223,7 @@ func (s *Service) SetPermissions(
}) })
} }
func (s *Service) mapActions(permission accesscontrol.ResourcePermission) string { func (s *Service) MapActions(permission accesscontrol.ResourcePermission) string {
for _, p := range s.permissions { for _, p := range s.permissions {
if permission.Contains(s.options.PermissionsToActions[p]) { if permission.Contains(s.options.PermissionsToActions[p]) {
return p return p

View File

@ -10,6 +10,8 @@ import (
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
"github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@ -19,21 +21,28 @@ type ImportDashboardAPI struct {
quotaService QuotaService quotaService QuotaService
schemaLoaderService SchemaLoaderService schemaLoaderService SchemaLoaderService
pluginStore plugins.Store pluginStore plugins.Store
ac accesscontrol.AccessControl
} }
func New(dashboardImportService dashboardimport.Service, quotaService QuotaService, func New(dashboardImportService dashboardimport.Service, quotaService QuotaService,
schemaLoaderService SchemaLoaderService, pluginStore plugins.Store) *ImportDashboardAPI { schemaLoaderService SchemaLoaderService, pluginStore plugins.Store, ac accesscontrol.AccessControl) *ImportDashboardAPI {
return &ImportDashboardAPI{ return &ImportDashboardAPI{
dashboardImportService: dashboardImportService, dashboardImportService: dashboardImportService,
quotaService: quotaService, quotaService: quotaService,
schemaLoaderService: schemaLoaderService, schemaLoaderService: schemaLoaderService,
pluginStore: pluginStore, pluginStore: pluginStore,
ac: ac,
} }
} }
func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteRegister) { func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteRegister) {
authorize := acmiddleware.Middleware(api.ac)
routeRegister.Group("/api/dashboards", func(route routing.RouteRegister) { routeRegister.Group("/api/dashboards", func(route routing.RouteRegister) {
route.Post("/import", routing.Wrap(api.ImportDashboard)) route.Post(
"/import",
authorize(middleware.ReqSignedIn, accesscontrol.EvalPermission(accesscontrol.ActionDashboardsCreate)),
routing.Wrap(api.ImportDashboard),
)
}, middleware.ReqSignedIn) }, middleware.ReqSignedIn)
} }

View File

@ -10,6 +10,7 @@ import (
"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/components/simplejson"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/web/webtest" "github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -33,7 +34,7 @@ func TestImportDashboardAPI(t *testing.T) {
}, },
} }
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), schemaLoaderService, nil) importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), schemaLoaderService, nil, acmock.New().WithDisabled())
routeRegister := routing.NewRouteRegister() routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister) importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister) s := webtest.NewServer(t, routeRegister)
@ -124,7 +125,7 @@ func TestImportDashboardAPI(t *testing.T) {
}, },
} }
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), schemaLoaderService, nil) importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), schemaLoaderService, nil, acmock.New().WithDisabled())
routeRegister := routing.NewRouteRegister() routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister) importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister) s := webtest.NewServer(t, routeRegister)
@ -152,7 +153,7 @@ func TestImportDashboardAPI(t *testing.T) {
t.Run("Quota reached", func(t *testing.T) { t.Run("Quota reached", func(t *testing.T) {
service := &serviceMock{} service := &serviceMock{}
schemaLoaderService := &schemaLoaderServiceMock{} schemaLoaderService := &schemaLoaderServiceMock{}
importDashboardAPI := New(service, quotaServiceFunc(quotaReached), schemaLoaderService, nil) importDashboardAPI := New(service, quotaServiceFunc(quotaReached), schemaLoaderService, nil, acmock.New().WithDisabled())
routeRegister := routing.NewRouteRegister() routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister) importDashboardAPI.RegisterAPIEndpoints(routeRegister)

View File

@ -2,14 +2,17 @@ package service
import ( import (
"context" "context"
"strconv"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/services/dashboardimport/api" "github.com/grafana/grafana/pkg/services/dashboardimport/api"
"github.com/grafana/grafana/pkg/services/dashboardimport/utils" "github.com/grafana/grafana/pkg/services/dashboardimport/utils"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/schemaloader" "github.com/grafana/grafana/pkg/services/schemaloader"
@ -18,23 +21,29 @@ import (
func ProvideService(routeRegister routing.RouteRegister, func ProvideService(routeRegister routing.RouteRegister,
quotaService *quota.QuotaService, schemaLoaderService *schemaloader.SchemaLoaderService, quotaService *quota.QuotaService, schemaLoaderService *schemaloader.SchemaLoaderService,
pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store, pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store,
libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService) *ImportDashboardService { libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService,
ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, features featuremgmt.FeatureToggles,
) *ImportDashboardService {
s := &ImportDashboardService{ s := &ImportDashboardService{
pluginDashboardManager: pluginDashboardManager, features: features,
dashboardService: dashboardService, pluginDashboardManager: pluginDashboardManager,
libraryPanelService: libraryPanelService, dashboardService: dashboardService,
libraryPanelService: libraryPanelService,
dashboardPermissionsService: permissionsServices.GetDashboardService(),
} }
dashboardImportAPI := api.New(s, quotaService, schemaLoaderService, pluginStore) dashboardImportAPI := api.New(s, quotaService, schemaLoaderService, pluginStore, ac)
dashboardImportAPI.RegisterAPIEndpoints(routeRegister) dashboardImportAPI.RegisterAPIEndpoints(routeRegister)
return s return s
} }
type ImportDashboardService struct { type ImportDashboardService struct {
pluginDashboardManager plugins.PluginDashboardManager features featuremgmt.FeatureToggles
dashboardService dashboards.DashboardService pluginDashboardManager plugins.PluginDashboardManager
libraryPanelService librarypanels.Service dashboardService dashboards.DashboardService
libraryPanelService librarypanels.Service
dashboardPermissionsService accesscontrol.PermissionsService
} }
func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) { func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
@ -85,6 +94,12 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb
return nil, err return nil, err
} }
if s.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if err := s.setDashboardPermissions(ctx, req.User, savedDash); err != nil {
return nil, err
}
}
return &dashboardimport.ImportDashboardResponse{ return &dashboardimport.ImportDashboardResponse{
UID: savedDash.Uid, UID: savedDash.Uid,
PluginId: req.PluginId, PluginId: req.PluginId,
@ -100,3 +115,24 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb
Slug: savedDash.Slug, Slug: savedDash.Slug,
}, nil }, nil
} }
func (s *ImportDashboardService) setDashboardPermissions(ctx context.Context, user *models.SignedInUser, dashboard *models.Dashboard) error {
resourceID := strconv.FormatInt(dashboard.Id, 10)
permissions := []accesscontrol.SetResourcePermissionCommand{
{UserID: user.UserId, Permission: models.PERMISSION_ADMIN.String()},
}
if dashboard.FolderId == 0 {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()},
{BuiltinRole: string(models.ROLE_VIEWER), Permission: models.PERMISSION_VIEW.String()},
}...)
}
_, err := s.dashboardPermissionsService.SetPermissions(ctx, user.OrgId, resourceID, permissions...)
if err != nil {
return err
}
return nil
}

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -55,6 +56,7 @@ func TestImportDashboardService(t *testing.T) {
pluginDashboardManager: pluginDashboardManager, pluginDashboardManager: pluginDashboardManager,
dashboardService: dashboardService, dashboardService: dashboardService,
libraryPanelService: libraryPanelService, libraryPanelService: libraryPanelService,
features: featuremgmt.WithFeatures(),
} }
req := &dashboardimport.ImportDashboardRequest{ req := &dashboardimport.ImportDashboardRequest{
@ -104,6 +106,7 @@ func TestImportDashboardService(t *testing.T) {
} }
libraryPanelService := &libraryPanelServiceMock{} libraryPanelService := &libraryPanelServiceMock{}
s := &ImportDashboardService{ s := &ImportDashboardService{
features: featuremgmt.WithFeatures(),
dashboardService: dashboardService, dashboardService: dashboardService,
libraryPanelService: libraryPanelService, libraryPanelService: libraryPanelService,
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"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/alerting" "github.com/grafana/grafana/pkg/services/alerting"
m "github.com/grafana/grafana/pkg/services/dashboards" m "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
@ -18,6 +19,15 @@ import (
"github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/util/errutil"
) )
var (
provisionerPermissions = map[string][]string{
accesscontrol.ActionFoldersCreate: {},
accesscontrol.ActionFoldersWrite: {accesscontrol.ScopeFoldersAll},
accesscontrol.ActionDashboardsCreate: {accesscontrol.ScopeFoldersAll},
accesscontrol.ActionDashboardsWrite: {accesscontrol.ScopeFoldersAll},
}
)
type DashboardServiceImpl struct { type DashboardServiceImpl struct {
dashboardStore m.Store dashboardStore m.Store
dashAlertExtractor alerting.DashAlertExtractor dashAlertExtractor alerting.DashAlertExtractor
@ -109,11 +119,20 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
} }
guard := guardian.New(ctx, dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User) guard := guardian.New(ctx, dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
if canSave, err := guard.CanSave(); err != nil || !canSave { if dash.Id == 0 {
if err != nil { if canCreate, err := guard.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canCreate {
return nil, err if err != nil {
return nil, err
}
return nil, models.ErrDashboardUpdateAccessDenied
}
} else {
if canSave, err := guard.CanSave(); err != nil || !canSave {
if err != nil {
return nil, err
}
return nil, models.ErrDashboardUpdateAccessDenied
} }
return nil, models.ErrDashboardUpdateAccessDenied
} }
cmd := &models.SaveDashboardCommand{ cmd := &models.SaveDashboardCommand{
@ -181,6 +200,9 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
UserId: 0, UserId: 0,
OrgRole: models.ROLE_ADMIN, OrgRole: models.ROLE_ADMIN,
OrgId: dto.OrgId, OrgId: dto.OrgId,
Permissions: map[int64]map[string][]string{
dto.OrgId: provisionerPermissions,
},
} }
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, true, false) cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, true, false)
@ -216,8 +238,9 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *m.SaveDashboardDTO) (*models.Dashboard, error) { func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *m.SaveDashboardDTO) (*models.Dashboard, error) {
dto.User = &models.SignedInUser{ dto.User = &models.SignedInUser{
UserId: 0, UserId: 0,
OrgRole: models.ROLE_ADMIN, OrgRole: models.ROLE_ADMIN,
Permissions: map[int64]map[string][]string{dto.OrgId: provisionerPermissions},
} }
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, false) cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, false)
if err != nil { if err != nil {

View File

@ -196,8 +196,8 @@ func (f *FolderServiceImpl) DeleteFolder(ctx context.Context, user *models.Signe
return nil, toFolderError(err) return nil, toFolderError(err)
} }
guardian := guardian.New(ctx, dashFolder.Id, orgID, user) guard := guardian.New(ctx, dashFolder.Id, orgID, user)
if canSave, err := guardian.CanSave(); err != nil || !canSave { if canSave, err := guard.CanDelete(); err != nil || !canSave {
if err != nil { if err != nil {
return nil, toFolderError(err) return nil, toFolderError(err)
} }

View File

@ -0,0 +1,227 @@
package guardian
import (
"context"
"strconv"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
var permissionMap = map[string]models.PermissionType{
"View": models.PERMISSION_VIEW,
"Edit": models.PERMISSION_EDIT,
"Admin": models.PERMISSION_ADMIN,
}
var _ DashboardGuardian = new(AccessControlDashboardGuardian)
func NewAccessControlDashboardGuardian(
ctx context.Context, dashboardId int64, user *models.SignedInUser,
store *sqlstore.SQLStore, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices,
) *AccessControlDashboardGuardian {
return &AccessControlDashboardGuardian{
ctx: ctx,
dashboardID: dashboardId,
user: user,
store: store,
ac: ac,
permissionServices: permissionsServices,
}
}
type AccessControlDashboardGuardian struct {
ctx context.Context
dashboardID int64
dashboard *models.Dashboard
user *models.SignedInUser
store *sqlstore.SQLStore
ac accesscontrol.AccessControl
permissionServices accesscontrol.PermissionsServices
}
func (a *AccessControlDashboardGuardian) CanSave() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
}
if a.dashboard.IsFolder {
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersWrite, folderScope(a.dashboardID)))
}
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.dashboard.FolderId)),
))
}
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.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersWrite, folderScope(a.dashboardID)))
}
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.dashboard.FolderId)),
))
}
func (a *AccessControlDashboardGuardian) CanView() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
}
if a.dashboard.IsFolder {
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersRead, folderScope(a.dashboardID)))
}
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, folderScope(a.dashboard.FolderId)),
))
}
func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
}
if a.dashboard.IsFolder {
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionFoldersPermissionsRead, folderScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionFoldersPermissionsWrite, folderScope(a.dashboard.Id)),
))
}
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, dashboardScope(a.dashboard.Id)),
),
accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, folderScope(a.dashboard.FolderId)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, folderScope(a.dashboard.FolderId)),
),
))
}
func (a *AccessControlDashboardGuardian) CanDelete() (bool, error) {
if err := a.loadDashboard(); err != nil {
return false, err
}
if a.dashboard.IsFolder {
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersDelete, folderScope(a.dashboard.Id)))
}
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, dashboardScope(a.dashboard.Id)),
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, folderScope(a.dashboard.FolderId)),
))
}
func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool) (bool, error) {
if isFolder {
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersCreate))
}
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionDashboardsCreate, folderScope(folderID)))
}
func (a *AccessControlDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
// always true for access control
return true, nil
}
// GetAcl translate access control permissions to dashboard acl info
func (a *AccessControlDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
if err := a.loadDashboard(); err != nil {
return nil, err
}
svc := a.permissionServices.GetDashboardService()
if a.dashboard.IsFolder {
svc = a.permissionServices.GetFolderService()
}
permissions, err := svc.GetPermissions(a.ctx, a.user, strconv.FormatInt(a.dashboard.Id, 10))
if err != nil {
return nil, err
}
acl := make([]*models.DashboardAclInfoDTO, 0, len(permissions))
for _, p := range permissions {
if !p.IsManaged() {
continue
}
var role *models.RoleType
if p.BuiltInRole != "" {
tmp := models.RoleType(p.BuiltInRole)
role = &tmp
}
acl = append(acl, &models.DashboardAclInfoDTO{
OrgId: a.dashboard.OrgId,
DashboardId: a.dashboard.Id,
FolderId: a.dashboard.FolderId,
Created: p.Created,
Updated: p.Updated,
UserId: p.UserId,
UserLogin: p.UserLogin,
UserEmail: p.UserEmail,
TeamId: p.TeamId,
TeamEmail: p.TeamEmail,
Team: p.Team,
Role: role,
Permission: permissionMap[svc.MapActions(p)],
PermissionName: permissionMap[svc.MapActions(p)].String(),
Uid: a.dashboard.Uid,
Title: a.dashboard.Title,
Slug: a.dashboard.Slug,
IsFolder: a.dashboard.IsFolder,
Url: a.dashboard.GetUrl(),
Inherited: false,
})
}
return acl, nil
}
func (a *AccessControlDashboardGuardian) GetACLWithoutDuplicates() ([]*models.DashboardAclInfoDTO, error) {
return a.GetAcl()
}
func (a *AccessControlDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*models.DashboardAcl, error) {
// not used with access control
return nil, nil
}
func (a *AccessControlDashboardGuardian) loadDashboard() error {
if a.dashboard == nil {
query := &models.GetDashboardQuery{Id: a.dashboardID, OrgId: a.user.OrgId}
if err := a.store.GetDashboard(a.ctx, query); err != nil {
return err
}
a.dashboard = query.Result
}
return nil
}
func dashboardScope(dashboardID int64) string {
return accesscontrol.Scope("dashboards", "id", strconv.FormatInt(dashboardID, 10))
}
func folderScope(folderID int64) string {
return accesscontrol.Scope("folders", "id", strconv.FormatInt(folderID, 10))
}

View File

@ -0,0 +1,557 @@
package guardian
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"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"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
dashdb "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
type accessControlGuardianTestCase struct {
desc string
dashboardID int64
permissions []*accesscontrol.Permission
viewersCanEdit bool
expected bool
}
func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to save with dashboard wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:*",
},
},
expected: true,
},
{
desc: "should be able to save with folder wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:*",
},
},
expected: true,
},
{
desc: "should be able to save with dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:1",
},
},
expected: true,
},
{
desc: "should be able to save with folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:0",
},
},
expected: true,
},
{
desc: "should not be able to save with incorrect dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:10",
},
},
expected: false,
},
{
desc: "should not be able to save with incorrect folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:10",
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
can, err := guardian.CanSave()
require.NoError(t, err)
assert.Equal(t, tt.expected, can)
})
}
}
func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to edit with dashboard wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:*",
},
},
expected: true,
},
{
desc: "should be able to edit with folder wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:*",
},
},
expected: true,
},
{
desc: "should be able to edit with dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:1",
},
},
expected: true,
},
{
desc: "should be able to edit with folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:0",
},
},
expected: true,
},
{
desc: "should not be able to edit with incorrect dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "dashboards:id:10",
},
},
expected: false,
},
{
desc: "should not be able to edit with incorrect folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsWrite,
Scope: "folders:id:10",
},
},
expected: false,
},
{
desc: "should be able to edit with read action when viewer_can_edit is true",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:1",
},
},
viewersCanEdit: true,
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, 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,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:*",
},
},
expected: true,
},
{
desc: "should be able to view with folder wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "folders:*",
},
},
expected: true,
},
{
desc: "should be able to view with dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:1",
},
},
expected: true,
},
{
desc: "should be able to view with folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "folders:id:0",
},
},
expected: true,
},
{
desc: "should not be able to view with incorrect dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "dashboards:id:10",
},
},
expected: false,
},
{
desc: "should not be able to view with incorrect folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsRead,
Scope: "folders:id:10",
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
can, err := guardian.CanView()
require.NoError(t, err)
assert.Equal(t, tt.expected, can)
})
}
}
func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to admin with dashboard wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "dashboards:*",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "dashboards:*",
},
},
expected: true,
},
{
desc: "should be able to admin with folder wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "folders:*",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "folders:*",
},
},
expected: true,
},
{
desc: "should be able to admin with dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "dashboards:id:1",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "dashboards:id:1",
},
},
expected: true,
},
{
desc: "should be able to admin with folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "folders:id:0",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "folders:id:0",
},
},
expected: true,
},
{
desc: "should not be able to admin with incorrect dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "dashboards:id:10",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "dashboards:id:10",
},
},
expected: false,
},
{
desc: "should not be able to admin with incorrect folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsPermissionsRead,
Scope: "folders:id:10",
},
{
Action: accesscontrol.ActionDashboardsPermissionsWrite,
Scope: "folders:id:10",
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
can, err := guardian.CanAdmin()
require.NoError(t, err)
assert.Equal(t, tt.expected, can)
})
}
}
func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
tests := []accessControlGuardianTestCase{
{
desc: "should be able to delete with dashboard wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "dashboards:*",
},
},
expected: true,
},
{
desc: "should be able to delete with folder wildcard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "folders:*",
},
},
expected: true,
},
{
desc: "should be able to delete with dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "dashboards:id:1",
},
},
expected: true,
},
{
desc: "should be able to delete with folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "folders:id:0",
},
},
expected: true,
},
{
desc: "should not be able to delete with incorrect dashboard scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "dashboards:id:10",
},
},
expected: false,
},
{
desc: "should not be able to delete with incorrect folder scope",
dashboardID: 1,
permissions: []*accesscontrol.Permission{
{
Action: accesscontrol.ActionDashboardsDelete,
Scope: "folders:id:10",
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
can, err := guardian.CanDelete()
require.NoError(t, err)
assert.Equal(t, tt.expected, can)
})
}
}
type accessControlGuardianCanCreateTestCase struct {
desc string
isFolder bool
folderID int64
permissions []*accesscontrol.Permission
expected bool
}
func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
tests := []accessControlGuardianCanCreateTestCase{
{
desc: "should be able to create dashboard in folder 0",
isFolder: false,
folderID: 0,
permissions: []*accesscontrol.Permission{
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:id:0"},
},
expected: true,
},
{
desc: "should be able to create dashboard in any folder",
isFolder: false,
folderID: 100,
permissions: []*accesscontrol.Permission{
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:*"},
},
expected: true,
},
{
desc: "should not be able to create dashboard without permissions",
isFolder: false,
folderID: 100,
permissions: []*accesscontrol.Permission{},
expected: false,
},
{
desc: "should be able to create folder with correct permissions",
isFolder: true,
folderID: 0,
permissions: []*accesscontrol.Permission{
{Action: accesscontrol.ActionFoldersCreate},
},
expected: true,
},
{
desc: "should not be able to create folders without permissions",
isFolder: true,
folderID: 100,
permissions: []*accesscontrol.Permission{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
guardian := setupAccessControlGuardianTest(t, 0, tt.permissions)
can, err := guardian.CanCreate(tt.folderID, tt.isFolder)
require.NoError(t, err)
assert.Equal(t, tt.expected, can)
})
}
}
func setupAccessControlGuardianTest(t *testing.T, dashID int64, permissions []*accesscontrol.Permission) *AccessControlDashboardGuardian {
t.Helper()
store := sqlstore.InitTestDB(t)
// seed dashboard
_, err := dashdb.ProvideDashboardStore(store).SaveDashboard(models.SaveDashboardCommand{
Dashboard: &simplejson.Json{},
UserId: 1,
OrgId: 1,
FolderId: 0,
})
require.NoError(t, err)
ac := accesscontrolmock.New().WithPermissions(permissions)
services, err := ossaccesscontrol.ProvidePermissionsServices(routing.NewRouteRegister(), store, ac, database.ProvideService(store))
require.NoError(t, err)
return NewAccessControlDashboardGuardian(context.Background(), dashID, &models.SignedInUser{OrgId: 1}, store, ac, services)
}

View File

@ -21,7 +21,8 @@ type DashboardGuardian interface {
CanEdit() (bool, error) CanEdit() (bool, error)
CanView() (bool, error) CanView() (bool, error)
CanAdmin() (bool, error) CanAdmin() (bool, error)
HasPermission(permission models.PermissionType) (bool, error) CanDelete() (bool, error)
CanCreate(folderID int64, isFolder bool) (bool, error)
CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error)
// GetAcl returns ACL. // GetAcl returns ACL.
@ -45,6 +46,7 @@ type dashboardGuardianImpl struct {
} }
// New factory for creating a new dashboard guardian instance // New factory for creating a new dashboard guardian instance
// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned
var New = func(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian { var New = func(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian {
return &dashboardGuardianImpl{ return &dashboardGuardianImpl{
user: user, user: user,
@ -75,6 +77,16 @@ func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
return g.HasPermission(models.PERMISSION_ADMIN) return g.HasPermission(models.PERMISSION_ADMIN)
} }
func (g *dashboardGuardianImpl) CanDelete() (bool, error) {
// when using dashboard guardian without access control a user can delete a dashboard if they can save it
return g.CanSave()
}
func (g *dashboardGuardianImpl) CanCreate(_ int64, _ bool) (bool, error) {
// when using dashboard guardian without access control a user can create a dashboard if they can save it
return g.CanSave()
}
func (g *dashboardGuardianImpl) HasPermission(permission models.PermissionType) (bool, error) { func (g *dashboardGuardianImpl) HasPermission(permission models.PermissionType) (bool, error) {
if g.user.OrgRole == models.ROLE_ADMIN { if g.user.OrgRole == models.ROLE_ADMIN {
return g.logHasPermissionResult(permission, true, nil) return g.logHasPermissionResult(permission, true, nil)
@ -325,6 +337,14 @@ func (g *FakeDashboardGuardian) CanAdmin() (bool, error) {
return g.CanAdminValue, nil return g.CanAdminValue, nil
} }
func (g *FakeDashboardGuardian) CanDelete() (bool, error) {
return g.CanSaveValue, nil
}
func (g *FakeDashboardGuardian) CanCreate(_ int64, _ bool) (bool, error) {
return g.CanSaveValue, nil
}
func (g *FakeDashboardGuardian) HasPermission(permission models.PermissionType) (bool, error) { func (g *FakeDashboardGuardian) HasPermission(permission models.PermissionType) (bool, error) {
return g.HasPermissionValue, nil return g.HasPermissionValue, nil
} }

View File

@ -0,0 +1,22 @@
package guardian
import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
type Provider struct{}
func ProvideService(store *sqlstore.SQLStore, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, features featuremgmt.FeatureToggles) *Provider {
if features.IsEnabled(featuremgmt.FlagAccesscontrol) {
// TODO: Fix this hack, see https://github.com/grafana/grafana-enterprise/issues/2935
New = func(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian {
return NewAccessControlDashboardGuardian(ctx, dashId, user, store, ac, permissionsServices)
}
}
return &Provider{}
}

View File

@ -7,7 +7,9 @@ import (
"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/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/services/provisioning/utils"
"github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/util/errutil"
) )
@ -23,7 +25,10 @@ type DashboardProvisioner interface {
} }
// DashboardProvisionerFactory creates DashboardProvisioners based on input // DashboardProvisionerFactory creates DashboardProvisioners based on input
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, utils.OrgStore) (DashboardProvisioner, error) type DashboardProvisionerFactory func(
context.Context, string, dashboards.DashboardProvisioningService, utils.OrgStore,
featuremgmt.FeatureToggles, accesscontrol.PermissionsServices,
) (DashboardProvisioner, error)
// Provisioner is responsible for syncing dashboard from disk to Grafana's database. // Provisioner is responsible for syncing dashboard from disk to Grafana's database.
type Provisioner struct { type Provisioner struct {
@ -35,7 +40,10 @@ type Provisioner struct {
} }
// New returns a new DashboardProvisioner // New returns a new DashboardProvisioner
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgStore utils.OrgStore) (DashboardProvisioner, error) { func New(
ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgStore utils.OrgStore,
features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices,
) (DashboardProvisioner, error) {
logger := log.New("provisioning.dashboard") logger := log.New("provisioning.dashboard")
cfgReader := &configReader{path: configDirectory, log: logger, orgStore: orgStore} cfgReader := &configReader{path: configDirectory, log: logger, orgStore: orgStore}
configs, err := cfgReader.readConfig(ctx) configs, err := cfgReader.readConfig(ctx)
@ -43,7 +51,7 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
return nil, errutil.Wrap("Failed to read dashboards config", err) return nil, errutil.Wrap("Failed to read dashboards config", err)
} }
fileReaders, err := getFileReaders(configs, logger, provisioner) fileReaders, err := getFileReaders(configs, logger, provisioner, features, permissionsServices)
if err != nil { if err != nil {
return nil, errutil.Wrap("Failed to initialize file readers", err) return nil, errutil.Wrap("Failed to initialize file readers", err)
} }
@ -122,13 +130,16 @@ func (provider *Provisioner) GetAllowUIUpdatesFromConfig(name string) bool {
return false return false
} }
func getFileReaders(configs []*config, logger log.Logger, service dashboards.DashboardProvisioningService) ([]*FileReader, error) { func getFileReaders(
configs []*config, logger log.Logger, service dashboards.DashboardProvisioningService,
features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices,
) ([]*FileReader, error) {
var readers []*FileReader var readers []*FileReader
for _, config := range configs { for _, config := range configs {
switch config.Type { switch config.Type {
case "file": case "file":
fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name), service) fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name), service, features, permissionsServices)
if err != nil { if err != nil {
return nil, errutil.Wrapf(err, "Failed to create file reader for config %v", config.Name) return nil, errutil.Wrapf(err, "Failed to create file reader for config %v", config.Name)
} }

View File

@ -7,6 +7,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -15,7 +16,9 @@ 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/services/featuremgmt"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -37,10 +40,15 @@ type FileReader struct {
mux sync.RWMutex mux sync.RWMutex
usageTracker *usageTracker usageTracker *usageTracker
dbWriteAccessRestricted bool dbWriteAccessRestricted bool
permissionsServices accesscontrol.PermissionsServices
features featuremgmt.FeatureToggles
} }
// NewDashboardFileReader returns a new filereader based on `config` // NewDashboardFileReader returns a new filereader based on `config`
func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.DashboardProvisioningService) (*FileReader, error) { func NewDashboardFileReader(
cfg *config, log log.Logger, service dashboards.DashboardProvisioningService,
features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices,
) (*FileReader, error) {
var path string var path string
path, ok := cfg.Options["path"].(string) path, ok := cfg.Options["path"].(string)
if !ok { if !ok {
@ -64,6 +72,8 @@ func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.Dash
dashboardProvisioningService: service, dashboardProvisioningService: service,
FoldersFromFilesStructure: foldersFromFilesStructure, FoldersFromFilesStructure: foldersFromFilesStructure,
usageTracker: newUsageTracker(), usageTracker: newUsageTracker(),
features: features,
permissionsServices: permissionsServices,
}, nil }, nil
} }
@ -264,9 +274,24 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i
Updated: resolvedFileInfo.ModTime().Unix(), Updated: resolvedFileInfo.ModTime().Unix(),
CheckSum: jsonFile.checkSum, CheckSum: jsonFile.checkSum,
} }
if _, err := fr.dashboardProvisioningService.SaveProvisionedDashboard(ctx, dash, dp); err != nil { savedDash, err := fr.dashboardProvisioningService.SaveProvisionedDashboard(ctx, dash, dp)
if err != nil {
return provisioningMetadata, err return provisioningMetadata, err
} }
if !alreadyProvisioned && fr.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
svc := fr.permissionsServices.GetDashboardService()
_, err := svc.SetPermissions(ctx, savedDash.OrgId, strconv.FormatInt(savedDash.Id, 10), accesscontrol.SetResourcePermissionCommand{
BuiltinRole: "Viewer",
Permission: "View",
}, accesscontrol.SetResourcePermissionCommand{
BuiltinRole: "Editor",
Permission: "Edit",
})
if err != nil {
fr.log.Warn("failed to set permissions for provisioned dashboard", "dashboardId", savedDash.Id, "err", err)
}
}
} else { } else {
fr.log.Warn("Not saving new dashboard due to restricted database access", "provisioner", fr.Cfg.Name, fr.log.Warn("Not saving new dashboard due to restricted database access", "provisioner", fr.Cfg.Name,
"file", path, "folderId", dash.Dashboard.FolderId) "file", path, "folderId", dash.Dashboard.FolderId)
@ -316,6 +341,19 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv
return 0, err return 0, err
} }
if fr.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
_, err = fr.permissionsServices.GetFolderService().SetPermissions(ctx, dbDash.OrgId, strconv.FormatInt(dbDash.Id, 10), accesscontrol.SetResourcePermissionCommand{
BuiltinRole: "Viewer",
Permission: "View",
}, accesscontrol.SetResourcePermissionCommand{
BuiltinRole: "Editor",
Permission: "Edit",
})
if err != nil {
return 0, err
}
}
return dbDash.Id, nil return dbDash.Id, nil
} }

View File

@ -8,6 +8,7 @@ import (
"testing" "testing"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -25,7 +26,7 @@ func TestProvisionedSymlinkedFolder(t *testing.T) {
Options: map[string]interface{}{"path": symlinkedFolder}, Options: map[string]interface{}{"path": symlinkedFolder},
} }
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, featuremgmt.WithFeatures(), nil)
if err != nil { if err != nil {
t.Error("expected err to be nil") t.Error("expected err to be nil")
} }

View File

@ -12,6 +12,7 @@ import (
"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/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
@ -42,7 +43,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
t.Run("using path parameter", func(t *testing.T) { t.Run("using path parameter", func(t *testing.T) {
cfg := setup() cfg := setup()
cfg.Options["path"] = defaultDashboards cfg.Options["path"] = defaultDashboards
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
require.NotEqual(t, reader.Path, "") require.NotEqual(t, reader.Path, "")
}) })
@ -50,7 +51,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
t.Run("using folder as options", func(t *testing.T) { t.Run("using folder as options", func(t *testing.T) {
cfg := setup() cfg := setup()
cfg.Options["folder"] = defaultDashboards cfg.Options["folder"] = defaultDashboards
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
require.NotEqual(t, reader.Path, "") require.NotEqual(t, reader.Path, "")
}) })
@ -59,7 +60,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
cfg := setup() cfg := setup()
cfg.Options["path"] = foldersFromFilesStructure cfg.Options["path"] = foldersFromFilesStructure
cfg.Options["foldersFromFilesStructure"] = true cfg.Options["foldersFromFilesStructure"] = true
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
require.NotEqual(t, reader.Path, "") require.NotEqual(t, reader.Path, "")
}) })
@ -72,7 +73,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
} }
cfg.Options["folder"] = fullPath cfg.Options["folder"] = fullPath
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, reader.Path, fullPath) require.Equal(t, reader.Path, fullPath)
@ -82,7 +83,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
t.Run("using relative path", func(t *testing.T) { t.Run("using relative path", func(t *testing.T) {
cfg := setup() cfg := setup()
cfg.Options["folder"] = defaultDashboards cfg.Options["folder"] = defaultDashboards
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
resolvedPath := reader.resolvedPath() resolvedPath := reader.resolvedPath()
@ -91,12 +92,11 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
} }
func TestDashboardFileReader(t *testing.T) { func TestDashboardFileReader(t *testing.T) {
logger := log.New("test.logger") logger := log.New("test-logger")
cfg := &config{} cfg := &config{}
fakeService := &dashboards.FakeDashboardProvisioning{} fakeService := &dashboards.FakeDashboardProvisioning{}
defer fakeService.AssertExpectations(t) defer fakeService.AssertExpectations(t)
setup := func() { setup := func() {
bus.ClearBusHandlers() bus.ClearBusHandlers()
bus.AddHandler("test", mockGetDashboardQuery) bus.AddHandler("test", mockGetDashboardQuery)
@ -119,7 +119,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 2}, nil).Times(2) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 2}, nil).Times(2)
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -139,7 +139,7 @@ func TestDashboardFileReader(t *testing.T) {
inserted++ inserted++
}) })
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -176,7 +176,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once()
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -204,7 +204,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once()
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -239,7 +239,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once()
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -267,7 +267,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once() fakeService.On("GetProvisionedDashboardData", configName).Return(provisionedDashboard, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once()
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -282,7 +282,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("GetProvisionedDashboardData", configName).Return(nil, nil).Once() fakeService.On("GetProvisionedDashboardData", configName).Return(nil, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once()
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -299,7 +299,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3)
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -316,7 +316,7 @@ func TestDashboardFileReader(t *testing.T) {
Folder: "", Folder: "",
} }
_, err := NewDashboardFileReader(cfg, logger, nil) _, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NotNil(t, err) require.NotNil(t, err)
}) })
@ -324,7 +324,7 @@ func TestDashboardFileReader(t *testing.T) {
setup() setup()
cfg.Options["path"] = brokenDashboards cfg.Options["path"] = brokenDashboards
_, err := NewDashboardFileReader(cfg, logger, nil) _, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
}) })
@ -337,14 +337,14 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2)
reader1, err := NewDashboardFileReader(cfg1, logger, nil) reader1, err := NewDashboardFileReader(cfg1, logger, nil, featuremgmt.WithFeatures(), nil)
reader1.dashboardProvisioningService = fakeService reader1.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
err = reader1.walkDisk(context.Background()) err = reader1.walkDisk(context.Background())
require.NoError(t, err) require.NoError(t, err)
reader2, err := NewDashboardFileReader(cfg2, logger, nil) reader2, err := NewDashboardFileReader(cfg2, logger, nil, featuremgmt.WithFeatures(), nil)
reader2.dashboardProvisioningService = fakeService reader2.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -364,7 +364,7 @@ func TestDashboardFileReader(t *testing.T) {
"folder": defaultDashboards, "folder": defaultDashboards,
}, },
} }
r, err := NewDashboardFileReader(cfg, logger, nil) r, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
_, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder)
@ -384,7 +384,7 @@ func TestDashboardFileReader(t *testing.T) {
} }
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once()
r, err := NewDashboardFileReader(cfg, logger, nil) r, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
_, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder) _, err = r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg.Folder)
@ -439,7 +439,7 @@ func TestDashboardFileReader(t *testing.T) {
cfg.DisableDeletion = true cfg.DisableDeletion = true
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -454,7 +454,7 @@ func TestDashboardFileReader(t *testing.T) {
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once()
fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
reader, err := NewDashboardFileReader(cfg, logger, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
reader.dashboardProvisioningService = fakeService reader.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)

View File

@ -9,6 +9,7 @@ import (
"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/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -35,7 +36,8 @@ func TestDuplicatesValidator(t *testing.T) {
t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) { t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) {
const folderName = "duplicates-validator-folder" const folderName = "duplicates-validator-folder"
r, err := NewDashboardFileReader(cfg, logger, nil)
r, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6) fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6)
fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(4) fakeService.On("GetProvisionedDashboardData", mock.Anything).Return([]*models.DashboardProvisioning{}, nil).Times(4)
@ -54,11 +56,11 @@ func TestDuplicatesValidator(t *testing.T) {
Options: map[string]interface{}{"path": dashboardContainingUID}, Options: map[string]interface{}{"path": dashboardContainingUID},
} }
reader1, err := NewDashboardFileReader(cfg1, logger, nil) reader1, err := NewDashboardFileReader(cfg1, logger, nil, featuremgmt.WithFeatures(), nil)
reader1.dashboardProvisioningService = fakeService reader1.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
reader2, err := NewDashboardFileReader(cfg2, logger, nil) reader2, err := NewDashboardFileReader(cfg2, logger, nil, featuremgmt.WithFeatures(), nil)
reader2.dashboardProvisioningService = fakeService reader2.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -89,7 +91,8 @@ func TestDuplicatesValidator(t *testing.T) {
t.Run("Duplicates validator should not collect info about duplicate UIDs and titles within folders for different orgs", func(t *testing.T) { t.Run("Duplicates validator should not collect info about duplicate UIDs and titles within folders for different orgs", func(t *testing.T) {
const folderName = "duplicates-validator-folder" const folderName = "duplicates-validator-folder"
r, err := NewDashboardFileReader(cfg, logger, nil)
r, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName)
require.NoError(t, err) require.NoError(t, err)
@ -105,11 +108,11 @@ func TestDuplicatesValidator(t *testing.T) {
Options: map[string]interface{}{"path": dashboardContainingUID}, Options: map[string]interface{}{"path": dashboardContainingUID},
} }
reader1, err := NewDashboardFileReader(cfg1, logger, nil) reader1, err := NewDashboardFileReader(cfg1, logger, nil, featuremgmt.WithFeatures(), nil)
reader1.dashboardProvisioningService = fakeService reader1.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
reader2, err := NewDashboardFileReader(cfg2, logger, nil) reader2, err := NewDashboardFileReader(cfg2, logger, nil, featuremgmt.WithFeatures(), nil)
reader2.dashboardProvisioningService = fakeService reader2.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -165,16 +168,15 @@ func TestDuplicatesValidator(t *testing.T) {
Name: "third", Type: "file", OrgID: 2, Folder: "duplicates-validator-folder", Name: "third", Type: "file", OrgID: 2, Folder: "duplicates-validator-folder",
Options: map[string]interface{}{"path": twoDashboardsWithUID}, Options: map[string]interface{}{"path": twoDashboardsWithUID},
} }
reader1, err := NewDashboardFileReader(cfg1, logger, nil, featuremgmt.WithFeatures(), nil)
reader1, err := NewDashboardFileReader(cfg1, logger, nil)
reader1.dashboardProvisioningService = fakeService reader1.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
reader2, err := NewDashboardFileReader(cfg2, logger, nil) reader2, err := NewDashboardFileReader(cfg2, logger, nil, featuremgmt.WithFeatures(), nil)
reader2.dashboardProvisioningService = fakeService reader2.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
reader3, err := NewDashboardFileReader(cfg3, logger, nil) reader3, err := NewDashboardFileReader(cfg3, logger, nil, featuremgmt.WithFeatures(), nil)
reader3.dashboardProvisioningService = fakeService reader3.dashboardProvisioningService = fakeService
require.NoError(t, err) require.NoError(t, err)
@ -191,7 +193,7 @@ func TestDuplicatesValidator(t *testing.T) {
duplicates := duplicateValidator.getDuplicates() duplicates := duplicateValidator.getDuplicates()
r, err := NewDashboardFileReader(cfg, logger, nil) r, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg1.Folder) folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg1.Folder)
require.NoError(t, err) require.NoError(t, err)
@ -208,7 +210,7 @@ func TestDuplicatesValidator(t *testing.T) {
sort.Strings(titleUsageReaders) sort.Strings(titleUsageReaders)
require.Equal(t, []string{"first"}, titleUsageReaders) require.Equal(t, []string{"first"}, titleUsageReaders)
r, err = NewDashboardFileReader(cfg3, logger, nil) r, err = NewDashboardFileReader(cfg3, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err) require.NoError(t, err)
folderID, err = r.getOrCreateFolderID(context.Background(), cfg3, fakeService, cfg3.Folder) folderID, err = r.getOrCreateFolderID(context.Background(), cfg3, fakeService, cfg3.Folder)
require.NoError(t, err) require.NoError(t, err)

View File

@ -8,10 +8,12 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
plugifaces "github.com/grafana/grafana/pkg/plugins" plugifaces "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources" datasourceservice "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/dashboards"
@ -28,8 +30,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p
encryptionService encryption.Internal, notificatonService *notifications.NotificationService, encryptionService encryption.Internal, notificatonService *notifications.NotificationService,
dashboardService dashboardservice.DashboardProvisioningService, dashboardService dashboardservice.DashboardProvisioningService,
datasourceService datasourceservice.DataSourceService, datasourceService datasourceservice.DataSourceService,
alertingService *alerting.AlertNotificationService, alertingService *alerting.AlertNotificationService, pluginSettings pluginsettings.Service,
pluginSettings pluginsettings.Service, features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices,
) (*ProvisioningServiceImpl, error) { ) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{ s := &ProvisioningServiceImpl{
Cfg: cfg, Cfg: cfg,
@ -46,6 +48,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p
datasourceService: datasourceService, datasourceService: datasourceService,
alertingService: alertingService, alertingService: alertingService,
pluginsSettings: pluginSettings, pluginsSettings: pluginSettings,
features: features,
permissionsServices: permissionsServices,
} }
return s, nil return s, nil
} }
@ -106,6 +110,8 @@ type ProvisioningServiceImpl struct {
datasourceService datasourceservice.DataSourceService datasourceService datasourceservice.DataSourceService
alertingService *alerting.AlertNotificationService alertingService *alerting.AlertNotificationService
pluginsSettings pluginsettings.Service pluginsSettings pluginsettings.Service
features featuremgmt.FeatureToggles
permissionsServices accesscontrol.PermissionsServices
} }
func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error { func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error {
@ -188,7 +194,7 @@ func (ps *ProvisioningServiceImpl) ProvisionNotifications(ctx context.Context) e
func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) error { func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) error {
dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards") dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards")
dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardService, ps.SQLStore) dashProvisioner, err := ps.newDashboardProvisioner(ctx, dashboardPath, ps.dashboardService, ps.SQLStore, ps.features, ps.permissionsServices)
if err != nil { if err != nil {
return errutil.Wrap("Failed to create provisioner", err) return errutil.Wrap("Failed to create provisioner", err)
} }

View File

@ -6,7 +6,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/services/accesscontrol"
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards" dashboardstore "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/services/provisioning/utils"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -93,7 +95,7 @@ func setup() *serviceTestStruct {
} }
serviceTest.service = newProvisioningServiceImpl( serviceTest.service = newProvisioningServiceImpl(
func(context.Context, string, dashboardstore.DashboardProvisioningService, utils.OrgStore) (dashboards.DashboardProvisioner, error) { func(context.Context, string, dashboardstore.DashboardProvisioningService, utils.OrgStore, featuremgmt.FeatureToggles, accesscontrol.PermissionsServices) (dashboards.DashboardProvisioner, error) {
return serviceTest.mock, nil return serviceTest.mock, nil
}, },
nil, nil,

View File

@ -3,15 +3,18 @@ package sqlstore
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/prometheus/client_golang/prometheus"
) )
var shadowSearchCounter = prometheus.NewCounterVec( var shadowSearchCounter = prometheus.NewCounterVec(
@ -89,6 +92,12 @@ func (ss *SQLStore) FindDashboards(ctx context.Context, query *search.FindPersis
}, },
} }
if ss.Cfg.IsFeatureToggleEnabled("accesscontrol") {
filters = []interface{}{
permissions.AccessControlDashboardPermissionFilter{User: query.SignedInUser},
}
}
for _, filter := range query.Sort.Filter { for _, filter := range query.Sort.Filter {
filters = append(filters, filter) filters = append(filters, filter)
} }
@ -270,6 +279,20 @@ func deleteDashboard(cmd *models.DeleteDashboardCommand, sess *DBSession) error
} }
} }
// remove all access control permission with folder scope
_, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.Scope("folders", "id", strconv.FormatInt(dashboard.Id, 10)))
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)))
if err != nil {
return err
}
}
if len(dashIds) > 0 { if len(dashIds) > 0 {
childrenDeletes := []string{ childrenDeletes := []string{
"DELETE FROM dashboard_tag WHERE dashboard_id IN (SELECT id FROM dashboard WHERE org_id = ? AND folder_id = ?)", "DELETE FROM dashboard_tag WHERE dashboard_id IN (SELECT id FROM dashboard WHERE org_id = ? AND folder_id = ?)",
@ -310,6 +333,11 @@ func deleteDashboard(cmd *models.DeleteDashboardCommand, sess *DBSession) error
} }
} }
} }
} else {
_, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.Scope("dashboards", "id", strconv.FormatInt(dashboard.Id, 10)))
if err != nil {
return err
}
} }
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil { if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {

View File

@ -0,0 +1,230 @@
package accesscontrol
import (
"fmt"
"strconv"
"strings"
"time"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
var dashboardPermissionTranslation = map[models.PermissionType][]string{
models.PERMISSION_VIEW: {
ac.ActionDashboardsRead,
},
models.PERMISSION_EDIT: {
ac.ActionDashboardsRead,
ac.ActionDashboardsWrite,
ac.ActionDashboardsCreate,
ac.ActionDashboardsDelete,
},
models.PERMISSION_ADMIN: {
ac.ActionDashboardsRead,
ac.ActionDashboardsWrite,
ac.ActionDashboardsCreate,
ac.ActionDashboardsDelete,
ac.ActionDashboardsPermissionsRead,
ac.ActionDashboardsPermissionsWrite,
},
}
var folderPermissionTranslation = map[models.PermissionType][]string{
models.PERMISSION_VIEW: append(dashboardPermissionTranslation[models.PERMISSION_VIEW], []string{
ac.ActionFoldersRead,
}...),
models.PERMISSION_EDIT: append(dashboardPermissionTranslation[models.PERMISSION_EDIT], []string{
ac.ActionFoldersRead,
ac.ActionFoldersWrite,
ac.ActionFoldersCreate,
ac.ActionFoldersDelete,
}...),
models.PERMISSION_ADMIN: append(dashboardPermissionTranslation[models.PERMISSION_ADMIN], []string{
ac.ActionFoldersRead,
ac.ActionFoldersWrite,
ac.ActionFoldersCreate,
ac.ActionFoldersDelete,
ac.ActionFoldersPermissionsRead,
ac.ActionFoldersPermissionsWrite,
}...),
}
func AddDashboardPermissionsMigrator(mg *migrator.Migrator) {
mg.AddMigration("dashboard permissions", &dashboardPermissionsMigrator{})
}
var _ migrator.CodeMigration = new(dashboardPermissionsMigrator)
type dashboardPermissionsMigrator struct {
permissionMigrator
}
type dashboard struct {
ID int64 `xorm:"id"`
FolderID int64 `xorm:"folder_id"`
OrgID int64 `xorm:"org_id"`
IsFolder bool
}
func (m dashboardPermissionsMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
m.sess = sess
m.dialect = migrator.Dialect
var dashboards []dashboard
if err := m.sess.SQL("SELECT id, is_folder, folder_id, org_id FROM dashboard").Find(&dashboards); err != nil {
return err
}
var acl []models.DashboardAcl
if err := m.sess.Find(&acl); err != nil {
return err
}
aclMap := make(map[int64][]models.DashboardAcl, len(acl))
for _, p := range acl {
aclMap[p.DashboardID] = append(aclMap[p.DashboardID], p)
}
if err := m.migratePermissions(dashboards, aclMap); err != nil {
return err
}
return nil
}
func (m dashboardPermissionsMigrator) migratePermissions(dashboards []dashboard, aclMap map[int64][]models.DashboardAcl) error {
permissionMap := map[int64]map[string][]*ac.Permission{}
for _, d := range dashboards {
if d.ID == -1 {
continue
}
acls := aclMap[d.ID]
if permissionMap[d.OrgID] == nil {
permissionMap[d.OrgID] = map[string][]*ac.Permission{}
}
if (d.IsFolder || d.FolderID == 0) && len(acls) == 0 {
permissionMap[d.OrgID]["managed:builtins:editor:permissions"] = append(
permissionMap[d.OrgID]["managed:builtins:editor:permissions"],
m.mapPermission(d.ID, models.PERMISSION_EDIT, d.IsFolder)...,
)
permissionMap[d.OrgID]["managed:builtins:viewer:permissions"] = append(
permissionMap[d.OrgID]["managed:builtins:viewer:permissions"],
m.mapPermission(d.ID, models.PERMISSION_VIEW, d.IsFolder)...,
)
} else {
for _, a := range acls {
permissionMap[d.OrgID][getRoleName(a)] = append(
permissionMap[d.OrgID][getRoleName(a)],
m.mapPermission(d.ID, a.Permission, d.IsFolder)...,
)
}
}
}
var allRoles []*ac.Role
rolesToCreate := []*ac.Role{}
assignments := map[int64]map[string]struct{}{}
for orgID, roles := range permissionMap {
for name := range roles {
role, err := m.findRole(orgID, name)
if err != nil {
return err
}
if role.ID == 0 {
rolesToCreate = append(rolesToCreate, &ac.Role{OrgID: orgID, Name: name})
if _, ok := assignments[orgID]; !ok {
assignments[orgID] = map[string]struct{}{}
}
assignments[orgID][name] = struct{}{}
} else {
allRoles = append(allRoles, &role)
}
}
}
createdRoles, err := m.bulkCreateRoles(rolesToCreate)
if err != nil {
return err
}
rolesToAssign := map[int64]map[string]*ac.Role{}
for i := range createdRoles {
if _, ok := rolesToAssign[createdRoles[i].OrgID]; !ok {
rolesToAssign[createdRoles[i].OrgID] = map[string]*ac.Role{}
}
rolesToAssign[createdRoles[i].OrgID][createdRoles[i].Name] = createdRoles[i]
allRoles = append(allRoles, createdRoles[i])
}
if err := m.bulkAssignRoles(rolesToAssign, assignments); err != nil {
return err
}
return m.setPermissions(allRoles, permissionMap)
}
func (m dashboardPermissionsMigrator) setPermissions(allRoles []*ac.Role, permissionMap map[int64]map[string][]*ac.Permission) error {
now := time.Now()
for _, role := range allRoles {
if _, err := m.sess.Exec("DELETE FROM permission WHERE role_id = ? AND (action LIKE ? OR action LIKE ?)", role.ID, "dashboards%", "folders%"); err != nil {
return err
}
var permissions []ac.Permission
mappedPermissions := permissionMap[role.OrgID][role.Name]
for _, p := range mappedPermissions {
permissions = append(permissions, ac.Permission{
RoleID: role.ID,
Action: p.Action,
Scope: p.Scope,
Updated: now,
Created: now,
})
}
err := batch(len(permissions), batchSize, func(start, end int) error {
if _, err := m.sess.InsertMulti(permissions[start:end]); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
}
return nil
}
func (m dashboardPermissionsMigrator) mapPermission(id int64, p models.PermissionType, isFolder bool) []*ac.Permission {
if isFolder {
actions := folderPermissionTranslation[p]
scope := ac.Scope("folders", "id", strconv.FormatInt(id, 10))
permissions := make([]*ac.Permission, 0, len(actions))
for _, action := range actions {
permissions = append(permissions, &ac.Permission{Action: action, Scope: scope})
}
return permissions
}
actions := dashboardPermissionTranslation[p]
scope := ac.Scope("dashboards", "id", strconv.FormatInt(id, 10))
permissions := make([]*ac.Permission, 0, len(actions))
for _, action := range actions {
permissions = append(permissions, &ac.Permission{Action: action, Scope: scope})
}
return permissions
}
func getRoleName(p models.DashboardAcl) string {
if p.UserID != 0 {
return fmt.Sprintf("managed:users:%d:permissions", p.UserID)
}
if p.TeamID != 0 {
return fmt.Sprintf("managed:teams:%d:permissions", p.TeamID)
}
return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(string(*p.Role)))
}

View File

@ -0,0 +1,233 @@
package accesscontrol
import (
"fmt"
"strconv"
"strings"
"time"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/util"
)
var (
batchSize = 500
)
type permissionMigrator struct {
sess *xorm.Session
dialect migrator.Dialect
migrator.MigrationBase
}
func (m *permissionMigrator) SQL(dialect migrator.Dialect) string {
return "code migration"
}
func (m *permissionMigrator) findRole(orgID int64, name string) (accesscontrol.Role, error) {
// check if role exists
var role accesscontrol.Role
_, err := m.sess.Table("role").Where("org_id = ? AND name = ?", orgID, name).Get(&role)
return role, err
}
func (m *permissionMigrator) bulkCreateRoles(allRoles []*accesscontrol.Role) ([]*accesscontrol.Role, error) {
if len(allRoles) == 0 {
return nil, nil
}
allCreatedRoles := make([]*accesscontrol.Role, 0, len(allRoles))
createRoles := m.createRoles
if m.dialect.DriverName() == migrator.MySQL {
createRoles = m.createRolesMySQL
}
// bulk role creations
err := batch(len(allRoles), batchSize, func(start, end int) error {
roles := allRoles[start:end]
createdRoles, err := createRoles(roles, start, end)
if err != nil {
return err
}
allCreatedRoles = append(allCreatedRoles, createdRoles...)
return nil
})
return allCreatedRoles, err
}
func (m *permissionMigrator) bulkAssignRoles(rolesMap map[int64]map[string]*accesscontrol.Role, assignments map[int64]map[string]struct{}) error {
if len(assignments) == 0 {
return nil
}
ts := time.Now()
userRoleAssignments := make([]accesscontrol.UserRole, 0)
teamRoleAssignments := make([]accesscontrol.TeamRole, 0)
builtInRoleAssignments := make([]accesscontrol.BuiltinRole, 0)
for orgID, roleNames := range assignments {
for name := range roleNames {
role, ok := rolesMap[orgID][name]
if !ok {
return &ErrUnknownRole{name}
}
if strings.HasPrefix(name, "managed:users") {
userID, err := strconv.ParseInt(strings.Split(name, ":")[2], 10, 64)
if err != nil {
return err
}
userRoleAssignments = append(userRoleAssignments, accesscontrol.UserRole{
OrgID: role.OrgID,
RoleID: role.ID,
UserID: userID,
Created: ts,
})
} else if strings.HasPrefix(name, "managed:teams") {
teamID, err := strconv.ParseInt(strings.Split(name, ":")[2], 10, 64)
if err != nil {
return err
}
teamRoleAssignments = append(teamRoleAssignments, accesscontrol.TeamRole{
OrgID: role.OrgID,
RoleID: role.ID,
TeamID: teamID,
Created: ts,
})
} else if strings.HasPrefix(name, "managed:builtins") {
builtIn := strings.Title(strings.Split(name, ":")[2])
builtInRoleAssignments = append(builtInRoleAssignments, accesscontrol.BuiltinRole{
OrgID: role.OrgID,
RoleID: role.ID,
Role: builtIn,
Created: ts,
Updated: ts,
})
}
}
}
err := batch(len(userRoleAssignments), batchSize, func(start, end int) error {
_, err := m.sess.Table("user_role").InsertMulti(userRoleAssignments[start:end])
return err
})
if err != nil {
return err
}
err = batch(len(teamRoleAssignments), batchSize, func(start, end int) error {
_, err := m.sess.Table("team_role").InsertMulti(teamRoleAssignments[start:end])
return err
})
if err != nil {
return err
}
return batch(len(builtInRoleAssignments), batchSize, func(start, end int) error {
_, err := m.sess.Table("builtin_role").InsertMulti(builtInRoleAssignments[start:end])
return err
})
}
// createRoles creates a list of roles and returns their id, orgID, name in a single query
func (m *permissionMigrator) createRoles(roles []*accesscontrol.Role, start int, end int) ([]*accesscontrol.Role, error) {
ts := time.Now()
createdRoles := make([]*accesscontrol.Role, 0, len(roles))
valueStrings := make([]string, len(roles))
args := make([]interface{}, 0, len(roles)*5)
for i, r := range roles {
uid, err := generateNewRoleUID(m.sess, r.OrgID)
if err != nil {
return nil, err
}
valueStrings[i] = "(?, ?, ?, 1, ?, ?)"
args = append(args, r.OrgID, uid, r.Name, ts, ts)
}
// Insert and fetch at once
valueString := strings.Join(valueStrings, ",")
sql := fmt.Sprintf("INSERT INTO role (org_id, uid, name, version, created, updated) VALUES %s RETURNING id, org_id, name", valueString)
if errCreate := m.sess.SQL(sql, args...).Find(&createdRoles); errCreate != nil {
return nil, errCreate
}
return createdRoles, nil
}
// createRolesMySQL creates a list of roles then fetches them
func (m *permissionMigrator) createRolesMySQL(roles []*accesscontrol.Role, start int, end int) ([]*accesscontrol.Role, error) {
ts := time.Now()
createdRoles := make([]*accesscontrol.Role, 0, len(roles))
where := make([]string, len(roles))
args := make([]interface{}, 0, len(roles)*2)
for i := range roles {
uid, err := generateNewRoleUID(m.sess, roles[i].OrgID)
if err != nil {
return nil, err
}
roles[i].UID = uid
roles[i].Created = ts
roles[i].Updated = ts
where[i] = ("(org_id = ? AND uid = ?)")
args = append(args, roles[i].OrgID, uid)
}
// Insert roles
if _, errCreate := m.sess.Table("role").Insert(&roles); errCreate != nil {
return nil, errCreate
}
// Fetch newly created roles
if errFindInsertions := m.sess.Table("role").
Where(strings.Join(where, " OR "), args...).
Find(&createdRoles); errFindInsertions != nil {
return nil, errFindInsertions
}
return createdRoles, nil
}
func batch(count, batchSize int, eachFn func(start, end int) error) error {
for i := 0; i < count; {
end := i + batchSize
if end > count {
end = count
}
if err := eachFn(i, end); err != nil {
return err
}
i = end
}
return nil
}
func generateNewRoleUID(sess *xorm.Session, orgID int64) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&accesscontrol.Role{})
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", fmt.Errorf("failed to generate uid")
}

View File

@ -3,7 +3,6 @@ package accesscontrol
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
"xorm.io/xorm" "xorm.io/xorm"
@ -11,12 +10,10 @@ import (
"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/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/util"
) )
const ( const (
TeamsMigrationID = "teams permissions migration" TeamsMigrationID = "teams permissions migration"
batchSize = 500
) )
func AddTeamMembershipMigrations(mg *migrator.Migrator) { func AddTeamMembershipMigrations(mg *migrator.Migrator) {
@ -26,14 +23,8 @@ func AddTeamMembershipMigrations(mg *migrator.Migrator) {
var _ migrator.CodeMigration = new(teamPermissionMigrator) var _ migrator.CodeMigration = new(teamPermissionMigrator)
type teamPermissionMigrator struct { type teamPermissionMigrator struct {
migrator.MigrationBase permissionMigrator
editorsCanAdmin bool editorsCanAdmin bool
sess *xorm.Session
dialect migrator.Dialect
}
func (p *teamPermissionMigrator) getAssignmentKey(orgID int64, name string) string {
return fmt.Sprint(orgID, "-", name)
} }
func (p *teamPermissionMigrator) SQL(dialect migrator.Dialect) string { func (p *teamPermissionMigrator) SQL(dialect migrator.Dialect) string {
@ -46,168 +37,6 @@ func (p *teamPermissionMigrator) Exec(sess *xorm.Session, migrator *migrator.Mig
return p.migrateMemberships() return p.migrateMemberships()
} }
func generateNewRoleUID(sess *xorm.Session, orgID int64) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&accesscontrol.Role{})
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", fmt.Errorf("failed to generate uid")
}
func (p *teamPermissionMigrator) findRole(orgID int64, name string) (accesscontrol.Role, error) {
// check if role exists
var role accesscontrol.Role
_, err := p.sess.Table("role").Where("org_id = ? AND name = ?", orgID, name).Get(&role)
return role, err
}
func batch(count, batchSize int, eachFn func(start, end int) error) error {
for i := 0; i < count; {
end := i + batchSize
if end > count {
end = count
}
if err := eachFn(i, end); err != nil {
return err
}
i = end
}
return nil
}
func (p *teamPermissionMigrator) bulkCreateRoles(allRoles []*accesscontrol.Role) ([]*accesscontrol.Role, error) {
if len(allRoles) == 0 {
return nil, nil
}
allCreatedRoles := make([]*accesscontrol.Role, 0, len(allRoles))
createRoles := p.createRoles
if p.dialect.DriverName() == migrator.MySQL {
createRoles = p.createRolesMySQL
}
// bulk role creations
err := batch(len(allRoles), batchSize, func(start, end int) error {
roles := allRoles[start:end]
createdRoles, err := createRoles(roles, start, end)
if err != nil {
return err
}
allCreatedRoles = append(allCreatedRoles, createdRoles...)
return nil
})
return allCreatedRoles, err
}
// createRoles creates a list of roles and returns their id, orgID, name in a single query
func (p *teamPermissionMigrator) createRoles(roles []*accesscontrol.Role, start int, end int) ([]*accesscontrol.Role, error) {
ts := time.Now()
createdRoles := make([]*accesscontrol.Role, 0, len(roles))
valueStrings := make([]string, len(roles))
args := make([]interface{}, 0, len(roles)*5)
for i, r := range roles {
uid, err := generateNewRoleUID(p.sess, r.OrgID)
if err != nil {
return nil, err
}
valueStrings[i] = "(?, ?, ?, 1, ?, ?)"
args = append(args, r.OrgID, uid, r.Name, ts, ts)
}
// Insert and fetch at once
valueString := strings.Join(valueStrings, ",")
sql := fmt.Sprintf("INSERT INTO role (org_id, uid, name, version, created, updated) VALUES %s RETURNING id, org_id, name", valueString)
if errCreate := p.sess.SQL(sql, args...).Find(&createdRoles); errCreate != nil {
return nil, errCreate
}
return createdRoles, nil
}
// createRolesMySQL creates a list of roles then fetches them
func (p *teamPermissionMigrator) createRolesMySQL(roles []*accesscontrol.Role, start int, end int) ([]*accesscontrol.Role, error) {
ts := time.Now()
createdRoles := make([]*accesscontrol.Role, 0, len(roles))
where := make([]string, len(roles))
args := make([]interface{}, 0, len(roles)*2)
for i := range roles {
uid, err := generateNewRoleUID(p.sess, roles[i].OrgID)
if err != nil {
return nil, err
}
roles[i].UID = uid
roles[i].Created = ts
roles[i].Updated = ts
where[i] = ("(org_id = ? AND uid = ?)")
args = append(args, roles[i].OrgID, uid)
}
// Insert roles
if _, errCreate := p.sess.Table("role").Insert(&roles); errCreate != nil {
return nil, errCreate
}
// Fetch newly created roles
if errFindInsertions := p.sess.Table("role").
Where(strings.Join(where, " OR "), args...).
Find(&createdRoles); errFindInsertions != nil {
return nil, errFindInsertions
}
return createdRoles, nil
}
func (p *teamPermissionMigrator) bulkAssignRoles(rolesMap map[string]*accesscontrol.Role, assignments map[int64]map[string]struct{}) error {
if len(assignments) == 0 {
return nil
}
ts := time.Now()
roleAssignments := make([]accesscontrol.UserRole, 0, len(assignments))
for userID, rolesByRoleKey := range assignments {
for key := range rolesByRoleKey {
role, ok := rolesMap[key]
if !ok {
return &ErrUnknownRole{key}
}
roleAssignments = append(roleAssignments, accesscontrol.UserRole{
OrgID: role.OrgID,
RoleID: role.ID,
UserID: userID,
Created: ts,
})
}
}
return batch(len(roleAssignments), batchSize, func(start, end int) error {
roleAssignmentsChunk := roleAssignments[start:end]
_, err := p.sess.Table("user_role").InsertMulti(roleAssignmentsChunk)
return err
})
}
// setRolePermissions sets the role permissions deleting any team related ones before inserting any. // setRolePermissions sets the role permissions deleting any team related ones before inserting any.
func (p *teamPermissionMigrator) setRolePermissions(roleID int64, permissions []accesscontrol.Permission) error { func (p *teamPermissionMigrator) setRolePermissions(roleID int64, permissions []accesscontrol.Permission) error {
// First drop existing permissions // First drop existing permissions
@ -316,8 +145,7 @@ func (p *teamPermissionMigrator) migrateMemberships() error {
// Populate rolesMap with the newly created roles // Populate rolesMap with the newly created roles
for i := range createdRoles { for i := range createdRoles {
roleKey := p.getAssignmentKey(createdRoles[i].OrgID, createdRoles[i].Name) rolesByOrg[createdRoles[i].OrgID][createdRoles[i].Name] = createdRoles[i]
rolesByOrg[roleKey] = createdRoles[i]
} }
// Assign newly created roles // Assign newly created roles
@ -329,14 +157,12 @@ func (p *teamPermissionMigrator) migrateMemberships() error {
return p.setRolePermissionsForOrgs(userPermissionsByOrg, rolesByOrg) return p.setRolePermissionsForOrgs(userPermissionsByOrg, rolesByOrg)
} }
func (p *teamPermissionMigrator) setRolePermissionsForOrgs(userPermissionsByOrg map[int64]map[int64][]accesscontrol.Permission, rolesByOrg map[string]*accesscontrol.Role) error { func (p *teamPermissionMigrator) setRolePermissionsForOrgs(userPermissionsByOrg map[int64]map[int64][]accesscontrol.Permission, rolesByOrg map[int64]map[string]*accesscontrol.Role) error {
for orgID, userPermissions := range userPermissionsByOrg { for orgID, userPermissions := range userPermissionsByOrg {
for userID, permissions := range userPermissions { for userID, permissions := range userPermissions {
key := p.getAssignmentKey(orgID, fmt.Sprintf("managed:users:%d:permissions", userID)) role, ok := rolesByOrg[orgID][fmt.Sprintf("managed:users:%d:permissions", userID)]
role, ok := rolesByOrg[key]
if !ok { if !ok {
return &ErrUnknownRole{key} return &ErrUnknownRole{fmt.Sprintf("managed:users:%d:permissions", userID)}
} }
if errSettingPerms := p.setRolePermissions(role.ID, permissions); errSettingPerms != nil { if errSettingPerms := p.setRolePermissions(role.ID, permissions); errSettingPerms != nil {
@ -347,12 +173,12 @@ func (p *teamPermissionMigrator) setRolePermissionsForOrgs(userPermissionsByOrg
return nil return nil
} }
func (p *teamPermissionMigrator) sortRolesToAssign(userPermissionsByOrg map[int64]map[int64][]accesscontrol.Permission) ([]*accesscontrol.Role, map[int64]map[string]struct{}, map[string]*accesscontrol.Role, error) { func (p *teamPermissionMigrator) sortRolesToAssign(userPermissionsByOrg map[int64]map[int64][]accesscontrol.Permission) ([]*accesscontrol.Role, map[int64]map[string]struct{}, map[int64]map[string]*accesscontrol.Role, error) {
var rolesToCreate []*accesscontrol.Role var rolesToCreate []*accesscontrol.Role
assignments := map[int64]map[string]struct{}{} assignments := map[int64]map[string]struct{}{}
rolesByOrg := map[string]*accesscontrol.Role{} rolesByOrg := map[int64]map[string]*accesscontrol.Role{}
for orgID, userPermissions := range userPermissionsByOrg { for orgID, userPermissions := range userPermissionsByOrg {
for userID := range userPermissions { for userID := range userPermissions {
roleName := fmt.Sprintf("managed:users:%d:permissions", userID) roleName := fmt.Sprintf("managed:users:%d:permissions", userID)
@ -361,10 +187,12 @@ func (p *teamPermissionMigrator) sortRolesToAssign(userPermissionsByOrg map[int6
return nil, nil, nil, errFindingRoles return nil, nil, nil, errFindingRoles
} }
roleKey := p.getAssignmentKey(orgID, roleName) if rolesByOrg[orgID] == nil {
rolesByOrg[orgID] = map[string]*accesscontrol.Role{}
}
if role.ID != 0 { if role.ID != 0 {
rolesByOrg[roleKey] = &role rolesByOrg[orgID][roleName] = &role
} else { } else {
roleToCreate := &accesscontrol.Role{ roleToCreate := &accesscontrol.Role{
Name: roleName, Name: roleName,
@ -372,13 +200,13 @@ func (p *teamPermissionMigrator) sortRolesToAssign(userPermissionsByOrg map[int6
} }
rolesToCreate = append(rolesToCreate, roleToCreate) rolesToCreate = append(rolesToCreate, roleToCreate)
userAssignments, initialized := assignments[userID] userAssignments, initialized := assignments[orgID]
if !initialized { if !initialized {
userAssignments = map[string]struct{}{} userAssignments = map[string]struct{}{}
} }
userAssignments[roleKey] = struct{}{} userAssignments[roleName] = struct{}{}
assignments[userID] = userAssignments assignments[orgID] = userAssignments
} }
} }
} }

View File

@ -76,6 +76,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil { if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
accesscontrol.AddTeamMembershipMigrations(mg) accesscontrol.AddTeamMembershipMigrations(mg)
accesscontrol.AddDashboardPermissionsMigrator(mg)
} }
} }
addQueryHistoryStarMigrations(mg) addQueryHistoryStarMigrations(mg)

View File

@ -1,9 +1,11 @@
package permissions package permissions
import ( import (
"context"
"strings" "strings"
"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/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
) )
@ -52,7 +54,7 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) {
-- include default permissions --> -- include default permissions -->
da.org_id = -1 AND ( da.org_id = -1 AND (
(folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
(folder.id IS NULL AND d.has_acl = ` + falseStr + `) (folder.id IS NULL AND d.has_acl = ` + falseStr + `)
) )
) )
WHERE WHERE
@ -73,3 +75,29 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) {
params = append(params, okRoles...) params = append(params, okRoles...)
return sql, params return sql, params
} }
type AccessControlDashboardPermissionFilter struct {
User *models.SignedInUser
}
func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{}) {
builder := strings.Builder{}
builder.WriteString("(((")
dashFilter, _ := accesscontrol.Filter(context.Background(), "dashboard.id", "dashboards", "dashboards:read", f.User)
builder.WriteString(dashFilter.Where)
builder.WriteString(" OR ")
dashFolderFilter, _ := accesscontrol.Filter(context.Background(), "dashboard.folder_id", "folders", "dashboards:read", f.User)
builder.WriteString(dashFolderFilter.Where)
builder.WriteString(") AND NOT dashboard.is_folder) OR (")
folderFilter, _ := accesscontrol.Filter(context.Background(), "dashboard.id", "folders", "folders:read", f.User)
builder.WriteString(folderFilter.Where)
builder.WriteString(" AND dashboard.is_folder))")
return builder.String(), append(dashFilter.Args, append(dashFolderFilter.Args, folderFilter.Args...)...)
}