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)},
}
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(
provisioningWriterRole, datasourcesReaderRole, datasourcesWriterRole, datasourcesIdReaderRole,
datasourcesCompatibilityReaderRole, orgReaderRole, orgWriterRole, orgMaintainerRole, teamsCreatorRole,
teamsWriterRole, datasourcesExplorerRole, annotationsReaderRole,
datasourcesCompatibilityReaderRole, orgReaderRole, orgWriterRole,
orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole, annotationsReaderRole,
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
foldersCreatorRole, foldersReaderRole, foldersWriterRole,
)
}

View File

@ -313,26 +313,26 @@ func (hs *HTTPServer) registerRoutes() {
// Folders
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
folderRoute.Get("/", routing.Wrap(hs.GetFolders))
folderRoute.Get("/id/:id", routing.Wrap(hs.GetFolderByID))
folderRoute.Post("/", routing.Wrap(hs.CreateFolder))
folderRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
folderRoute.Get("/id/:id", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersRead, ac.ScopeFolderID)), routing.Wrap(hs.GetFolderByID))
folderRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Put("/", routing.Wrap(hs.UpdateFolder))
folderUidRoute.Delete("/", routing.Wrap(hs.DeleteFolder))
folderUidRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersRead)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Put("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersWrite)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Delete("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersDelete)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", routing.Wrap(hs.UpdateFolderPermissions))
folderPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersPermissionsRead)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionFoldersPermissionsWrite)), routing.Wrap(hs.UpdateFolderPermissions))
})
})
})
// Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {
dashboardRoute.Get("/uid/:uid", routing.Wrap(hs.GetDashboard))
dashboardRoute.Delete("/uid/:uid", routing.Wrap(hs.DeleteDashboardByUID))
dashboardRoute.Get("/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsRead)), routing.Wrap(hs.GetDashboard))
dashboardRoute.Delete("/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsDelete)), routing.Wrap(hs.DeleteDashboardByUID))
if hs.ThumbService != nil {
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("/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("/tags", hs.GetDashboardTags)
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
dashIdRoute.Get("/versions", routing.Wrap(hs.GetDashboardVersions))
dashIdRoute.Get("/versions/:id", routing.Wrap(hs.GetDashboardVersion))
dashIdRoute.Post("/restore", routing.Wrap(hs.RestoreDashboardVersion))
dashIdRoute.Get("/versions", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions))
dashIdRoute.Get("/versions/:id", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion))
dashIdRoute.Post("/restore", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", routing.Wrap(hs.GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", routing.Wrap(hs.UpdateDashboardPermissions))
dashboardPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList))
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/infra/metrics"
"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/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@ -100,6 +102,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
canEdit, _ := guardian.CanEdit()
canSave, _ := guardian.CanSave()
canAdmin, _ := guardian.CanAdmin()
canDelete, _ := guardian.CanDelete()
isStarred, err := hs.isDashboardStarredByUser(c, dash.Id)
if err != nil {
@ -122,6 +125,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CanDelete: canDelete,
Created: dash.Created,
Updated: dash.Updated,
UpdatedBy: updater,
@ -223,7 +227,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
return rsp
}
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)
}
@ -356,10 +360,8 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, err)
}
if hs.Cfg.EditorsCanAdmin && newDashboard {
inFolder := cmd.FolderId > 0
err := hs.dashboardService.MakeUserAdmin(ctx, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
if err != nil {
if newDashboard {
if err := hs.setDashboardPermissions(c, cmd, dashboard); err != nil {
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.
func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response {
prefsQuery := models.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}

View File

@ -1,6 +1,7 @@
package api
import (
"context"
"errors"
"net/http"
"strconv"
@ -9,6 +10,8 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"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/web"
)
@ -70,7 +73,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
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 {
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)
}
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 errors.Is(err, models.ErrDashboardAclInfoMissing) ||
errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) {
@ -123,6 +137,64 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
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 {
for _, item := range apiCmd.Items {
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/services/dashboards/database"
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/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
@ -31,6 +32,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
Cfg: settings,
dashboardService: dashboardservice.ProvideDashboardService(dashboardStore, nil),
SQLStore: mockSQLStore,
Features: featuremgmt.WithFeatures(),
}
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(),
pluginStore: &fakePluginStore{},
SQLStore: mockSQLStore,
Features: featuremgmt.WithFeatures(),
}
hs.SQLStore = mockSQLStore
@ -1026,6 +1027,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
LibraryElementService: &mockLibraryElementService{},
dashboardService: dashboardService,
folderService: folderService,
Features: featuremgmt.WithFeatures(),
}
sc := setupScenarioContext(t, url)
@ -1096,6 +1098,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
LibraryElementService: &mockLibraryElementService{},
dashboardService: mock,
SQLStore: sqlStore,
Features: featuremgmt.WithFeatures(),
}
sc := setupScenarioContext(t, url)

View File

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

View File

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

View File

@ -11,6 +11,8 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"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/libraryelements"
"github.com/grafana/grafana/pkg/util"
@ -71,17 +73,36 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
return apierrors.ToFolderErrorResponse(err)
}
if hs.Cfg.EditorsCanAdmin {
if err := hs.folderService.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil {
if err := hs.setFolderPermission(c, folder.Id); err != nil {
hs.log.Error("Could not make user admin", "folder", folder.Title, "user",
c.SignedInUser.UserId, "error", err)
}
}
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
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 {
cmd := models.UpdateFolderCommand{}
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()
canSave, _ := g.CanSave()
canAdmin, _ := g.CanAdmin()
canDelete, _ := g.CanDelete()
// Finding creator and last updater of the folder
updater, creator := anonString, anonString
@ -140,6 +162,7 @@ func (hs *HTTPServer) toFolderDto(ctx context.Context, g guardian.DashboardGuard
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CanDelete: canDelete,
CreatedBy: creator,
Created: folder.Created,
UpdatedBy: updater,

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"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/util"
"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)
}
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 errors.Is(err, models.ErrDashboardAclInfoMissing) {
err = models.ErrFolderAclInfoMissing

View File

@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
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/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
@ -24,13 +25,14 @@ import (
func TestFolderPermissionAPIEndpoint(t *testing.T) {
settings := setting.NewCfg()
folderService := &dashboards.FakeFolderService{}
defer folderService.AssertExpectations(t)
dashboardStore := &database.FakeDashboardStore{}
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) {
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/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -143,6 +144,7 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st
Bus: bus.GetBus(),
Cfg: setting.NewCfg(),
folderService: folderService,
Features: featuremgmt.WithFeatures(),
}
sc := setupScenarioContext(t, url)

View File

@ -132,6 +132,7 @@ type HTTPServer struct {
serviceAccountsService serviceaccounts.Service
authInfoService login.AuthInfoService
teamPermissionsService accesscontrol.PermissionsService
permissionServices accesscontrol.PermissionsServices
NotificationService *notifications.NotificationService
dashboardService dashboards.DashboardService
dashboardProvisioningService dashboards.DashboardProvisioningService
@ -241,6 +242,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
AlertNotificationService: alertNotificationService,
DashboardsnapshotsService: dashboardsnapshotsService,
PluginSettings: pluginSettings,
permissionServices: permissionsServices,
}
if hs.Listener != nil {
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 {
children := []*dtos.NavLink{
{Text: "Dashboard", Icon: "apps", Url: hs.Cfg.AppSubURL + "/dashboard/new", Id: "create-dashboard"},
hasAccess := ac.HasAccess(hs.AccessControl, c)
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{
Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder",
Icon: "folder-plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new",
})
}
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(ac.ActionDashboardsCreate)) {
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]
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) {
hasAccess := ac.HasAccess(hs.AccessControl, c)
hasEditPerm := hasAccess(func(context *models.ReqContext) bool {
hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
if err := hs.SQLStore.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil {
return nil, err
return false
}
hasEditPerm := hasEditPermissionInFoldersQuery.Result
return hasEditPermissionInFoldersQuery.Result
}, ac.EvalAny(ac.EvalPermission(ac.ActionDashboardsCreate), ac.EvalPermission(ac.ActionFoldersCreate)))
settings, err := hs.getFrontendSettingsMap(c)
if err != nil {

View File

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

View File

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

View File

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

View File

@ -39,6 +39,8 @@ type PermissionsProvider interface {
type PermissionsServices interface {
GetTeamService() PermissionsService
GetFolderService() PermissionsService
GetDashboardService() PermissionsService
GetDataSourceService() PermissionsService
}
@ -53,6 +55,8 @@ type PermissionsService interface {
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(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 {
@ -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 {
return c.IsGrafanaAdmin
}
@ -109,6 +117,10 @@ var ReqOrgAdmin = func(c *models.ReqContext) bool {
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 {
permissionsMap := make(map[string]bool)
for _, p := range permissions {

View File

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

View File

@ -9,12 +9,16 @@ var _ accesscontrol.PermissionsServices = new(PermissionsServicesMock)
func NewPermissionsServicesMock() *PermissionsServicesMock {
return &PermissionsServicesMock{
teams: &MockPermissionsService{},
folders: &MockPermissionsService{},
dashboards: &MockPermissionsService{},
datasources: &MockPermissionsService{},
}
}
type PermissionsServicesMock struct {
teams *MockPermissionsService
folders *MockPermissionsService
dashboards *MockPermissionsService
datasources *MockPermissionsService
}
@ -22,6 +26,14 @@ func (p PermissionsServicesMock) GetTeamService() accesscontrol.PermissionsServi
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 {
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)
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

@ -324,16 +324,40 @@ const (
// Annotations related actions
ActionAnnotationsRead = "annotations:read"
ActionAnnotationsTagsRead = "annotations.tags:read"
ScopeAnnotationsAll = "annotations:*"
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 (
// Team scope
ScopeTeamsID = Scope("teams", "id", Parameter(":teamId"))
// Folder scopes
ScopeFolderID = Scope("folders", "id", Parameter(":id"))
)
const RoleGrafanaAdmin = "Grafana Admin"

View File

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

View File

@ -2,6 +2,7 @@ package ossaccesscontrol
import (
"context"
"errors"
"fmt"
"strconv"
@ -12,25 +13,48 @@ import (
"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)
if err != nil {
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
folder accesscontrol.PermissionsService
dashboard accesscontrol.PermissionsService
datasources accesscontrol.PermissionsService
}
func (s *PermissionsService) GetTeamService() accesscontrol.PermissionsService {
func (s *PermissionsServices) GetTeamService() accesscontrol.PermissionsService {
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
}
@ -105,6 +129,107 @@ func ProvideTeamPermissions(router routing.RouteRegister, sql *sqlstore.SQLStore
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 {
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) {
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))
for _, p := range permissions {
if permission := a.service.mapActions(p); permission != "" {
if permission := a.service.MapActions(p); permission != "" {
teamAvatarUrl := ""
if p.TeamId != 0 {
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 {
if permission.Contains(s.options.PermissionsToActions[p]) {
return p

View File

@ -10,6 +10,8 @@ import (
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"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/web"
)
@ -19,21 +21,28 @@ type ImportDashboardAPI struct {
quotaService QuotaService
schemaLoaderService SchemaLoaderService
pluginStore plugins.Store
ac accesscontrol.AccessControl
}
func New(dashboardImportService dashboardimport.Service, quotaService QuotaService,
schemaLoaderService SchemaLoaderService, pluginStore plugins.Store) *ImportDashboardAPI {
schemaLoaderService SchemaLoaderService, pluginStore plugins.Store, ac accesscontrol.AccessControl) *ImportDashboardAPI {
return &ImportDashboardAPI{
dashboardImportService: dashboardImportService,
quotaService: quotaService,
schemaLoaderService: schemaLoaderService,
pluginStore: pluginStore,
ac: ac,
}
}
func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteRegister) {
authorize := acmiddleware.Middleware(api.ac)
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)
}

View File

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

View File

@ -2,14 +2,17 @@ package service
import (
"context"
"strconv"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/models"
"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/api"
"github.com/grafana/grafana/pkg/services/dashboardimport/utils"
"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/quota"
"github.com/grafana/grafana/pkg/services/schemaloader"
@ -18,23 +21,29 @@ import (
func ProvideService(routeRegister routing.RouteRegister,
quotaService *quota.QuotaService, schemaLoaderService *schemaloader.SchemaLoaderService,
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{
features: features,
pluginDashboardManager: pluginDashboardManager,
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)
return s
}
type ImportDashboardService struct {
features featuremgmt.FeatureToggles
pluginDashboardManager plugins.PluginDashboardManager
dashboardService dashboards.DashboardService
libraryPanelService librarypanels.Service
dashboardPermissionsService accesscontrol.PermissionsService
}
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
}
if s.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if err := s.setDashboardPermissions(ctx, req.User, savedDash); err != nil {
return nil, err
}
}
return &dashboardimport.ImportDashboardResponse{
UID: savedDash.Uid,
PluginId: req.PluginId,
@ -100,3 +115,24 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb
Slug: savedDash.Slug,
}, 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/services/dashboardimport"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/stretchr/testify/require"
)
@ -55,6 +56,7 @@ func TestImportDashboardService(t *testing.T) {
pluginDashboardManager: pluginDashboardManager,
dashboardService: dashboardService,
libraryPanelService: libraryPanelService,
features: featuremgmt.WithFeatures(),
}
req := &dashboardimport.ImportDashboardRequest{
@ -104,6 +106,7 @@ func TestImportDashboardService(t *testing.T) {
}
libraryPanelService := &libraryPanelServiceMock{}
s := &ImportDashboardService{
features: featuremgmt.WithFeatures(),
dashboardService: dashboardService,
libraryPanelService: libraryPanelService,
}

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting"
m "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
@ -18,6 +19,15 @@ import (
"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 {
dashboardStore m.Store
dashAlertExtractor alerting.DashAlertExtractor
@ -109,12 +119,21 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
}
guard := guardian.New(ctx, dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
if dash.Id == 0 {
if canCreate, err := guard.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canCreate {
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
}
}
cmd := &models.SaveDashboardCommand{
Dashboard: dash.Data,
@ -181,6 +200,9 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
UserId: 0,
OrgRole: models.ROLE_ADMIN,
OrgId: dto.OrgId,
Permissions: map[int64]map[string][]string{
dto.OrgId: provisionerPermissions,
},
}
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, true, false)
@ -218,6 +240,7 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C
dto.User = &models.SignedInUser{
UserId: 0,
OrgRole: models.ROLE_ADMIN,
Permissions: map[int64]map[string][]string{dto.OrgId: provisionerPermissions},
}
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, false)
if err != nil {

View File

@ -196,8 +196,8 @@ func (f *FolderServiceImpl) DeleteFolder(ctx context.Context, user *models.Signe
return nil, toFolderError(err)
}
guardian := guardian.New(ctx, dashFolder.Id, orgID, user)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
guard := guardian.New(ctx, dashFolder.Id, orgID, user)
if canSave, err := guard.CanDelete(); err != nil || !canSave {
if err != nil {
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)
CanView() (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)
// GetAcl returns ACL.
@ -45,6 +46,7 @@ type dashboardGuardianImpl struct {
}
// New factory for creating a new dashboard guardian instance
// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned
var New = func(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian {
return &dashboardGuardianImpl{
user: user,
@ -75,6 +77,16 @@ func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
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) {
if g.user.OrgRole == models.ROLE_ADMIN {
return g.logHasPermissionResult(permission, true, nil)
@ -325,6 +337,14 @@ func (g *FakeDashboardGuardian) CanAdmin() (bool, error) {
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) {
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/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"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/util/errutil"
)
@ -23,7 +25,10 @@ type DashboardProvisioner interface {
}
// 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.
type Provisioner struct {
@ -35,7 +40,10 @@ type Provisioner struct {
}
// 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")
cfgReader := &configReader{path: configDirectory, log: logger, orgStore: orgStore}
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)
}
fileReaders, err := getFileReaders(configs, logger, provisioner)
fileReaders, err := getFileReaders(configs, logger, provisioner, features, permissionsServices)
if err != nil {
return nil, errutil.Wrap("Failed to initialize file readers", err)
}
@ -122,13 +130,16 @@ func (provider *Provisioner) GetAllowUIUpdatesFromConfig(name string) bool {
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
for _, config := range configs {
switch config.Type {
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 {
return nil, errutil.Wrapf(err, "Failed to create file reader for config %v", config.Name)
}

View File

@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@ -15,7 +16,9 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
)
@ -37,10 +40,15 @@ type FileReader struct {
mux sync.RWMutex
usageTracker *usageTracker
dbWriteAccessRestricted bool
permissionsServices accesscontrol.PermissionsServices
features featuremgmt.FeatureToggles
}
// 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
path, ok := cfg.Options["path"].(string)
if !ok {
@ -64,6 +72,8 @@ func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.Dash
dashboardProvisioningService: service,
FoldersFromFilesStructure: foldersFromFilesStructure,
usageTracker: newUsageTracker(),
features: features,
permissionsServices: permissionsServices,
}, nil
}
@ -264,9 +274,24 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i
Updated: resolvedFileInfo.ModTime().Unix(),
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
}
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 {
fr.log.Warn("Not saving new dashboard due to restricted database access", "provisioner", fr.Cfg.Name,
"file", path, "folderId", dash.Dashboard.FolderId)
@ -316,6 +341,19 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv
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
}

View File

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

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/stretchr/testify/mock"
"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) {
const folderName = "duplicates-validator-folder"
r, err := NewDashboardFileReader(cfg, logger, nil)
r, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6)
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},
}
reader1, err := NewDashboardFileReader(cfg1, logger, nil)
reader1, err := NewDashboardFileReader(cfg1, logger, nil, featuremgmt.WithFeatures(), nil)
reader1.dashboardProvisioningService = fakeService
require.NoError(t, err)
reader2, err := NewDashboardFileReader(cfg2, logger, nil)
reader2, err := NewDashboardFileReader(cfg2, logger, nil, featuremgmt.WithFeatures(), nil)
reader2.dashboardProvisioningService = fakeService
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) {
const folderName = "duplicates-validator-folder"
r, err := NewDashboardFileReader(cfg, logger, nil)
r, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err)
folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName)
require.NoError(t, err)
@ -105,11 +108,11 @@ func TestDuplicatesValidator(t *testing.T) {
Options: map[string]interface{}{"path": dashboardContainingUID},
}
reader1, err := NewDashboardFileReader(cfg1, logger, nil)
reader1, err := NewDashboardFileReader(cfg1, logger, nil, featuremgmt.WithFeatures(), nil)
reader1.dashboardProvisioningService = fakeService
require.NoError(t, err)
reader2, err := NewDashboardFileReader(cfg2, logger, nil)
reader2, err := NewDashboardFileReader(cfg2, logger, nil, featuremgmt.WithFeatures(), nil)
reader2.dashboardProvisioningService = fakeService
require.NoError(t, err)
@ -165,16 +168,15 @@ func TestDuplicatesValidator(t *testing.T) {
Name: "third", Type: "file", OrgID: 2, Folder: "duplicates-validator-folder",
Options: map[string]interface{}{"path": twoDashboardsWithUID},
}
reader1, err := NewDashboardFileReader(cfg1, logger, nil)
reader1, err := NewDashboardFileReader(cfg1, logger, nil, featuremgmt.WithFeatures(), nil)
reader1.dashboardProvisioningService = fakeService
require.NoError(t, err)
reader2, err := NewDashboardFileReader(cfg2, logger, nil)
reader2, err := NewDashboardFileReader(cfg2, logger, nil, featuremgmt.WithFeatures(), nil)
reader2.dashboardProvisioningService = fakeService
require.NoError(t, err)
reader3, err := NewDashboardFileReader(cfg3, logger, nil)
reader3, err := NewDashboardFileReader(cfg3, logger, nil, featuremgmt.WithFeatures(), nil)
reader3.dashboardProvisioningService = fakeService
require.NoError(t, err)
@ -191,7 +193,7 @@ func TestDuplicatesValidator(t *testing.T) {
duplicates := duplicateValidator.getDuplicates()
r, err := NewDashboardFileReader(cfg, logger, nil)
r, err := NewDashboardFileReader(cfg, logger, nil, featuremgmt.WithFeatures(), nil)
require.NoError(t, err)
folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, cfg1.Folder)
require.NoError(t, err)
@ -208,7 +210,7 @@ func TestDuplicatesValidator(t *testing.T) {
sort.Strings(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)
folderID, err = r.getOrCreateFolderID(context.Background(), cfg3, fakeService, cfg3.Folder)
require.NoError(t, err)

View File

@ -8,10 +8,12 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
plugifaces "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources"
"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/pluginsettings"
"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,
dashboardService dashboardservice.DashboardProvisioningService,
datasourceService datasourceservice.DataSourceService,
alertingService *alerting.AlertNotificationService,
pluginSettings pluginsettings.Service,
alertingService *alerting.AlertNotificationService, pluginSettings pluginsettings.Service,
features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices,
) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{
Cfg: cfg,
@ -46,6 +48,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p
datasourceService: datasourceService,
alertingService: alertingService,
pluginsSettings: pluginSettings,
features: features,
permissionsServices: permissionsServices,
}
return s, nil
}
@ -106,6 +110,8 @@ type ProvisioningServiceImpl struct {
datasourceService datasourceservice.DataSourceService
alertingService *alerting.AlertNotificationService
pluginsSettings pluginsettings.Service
features featuremgmt.FeatureToggles
permissionsServices accesscontrol.PermissionsServices
}
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 {
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 {
return errutil.Wrap("Failed to create provisioner", err)
}

View File

@ -6,7 +6,9 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/services/accesscontrol"
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/utils"
"github.com/grafana/grafana/pkg/setting"
@ -93,7 +95,7 @@ func setup() *serviceTestStruct {
}
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
},
nil,

View File

@ -3,15 +3,18 @@ package sqlstore
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/bus"
"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/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/util"
"github.com/prometheus/client_golang/prometheus"
)
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 {
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 {
childrenDeletes := []string{
"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 {

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 (
"fmt"
"strconv"
"strings"
"time"
"xorm.io/xorm"
@ -11,12 +10,10 @@ import (
"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/util"
)
const (
TeamsMigrationID = "teams permissions migration"
batchSize = 500
)
func AddTeamMembershipMigrations(mg *migrator.Migrator) {
@ -26,14 +23,8 @@ func AddTeamMembershipMigrations(mg *migrator.Migrator) {
var _ migrator.CodeMigration = new(teamPermissionMigrator)
type teamPermissionMigrator struct {
migrator.MigrationBase
permissionMigrator
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 {
@ -46,168 +37,6 @@ func (p *teamPermissionMigrator) Exec(sess *xorm.Session, migrator *migrator.Mig
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.
func (p *teamPermissionMigrator) setRolePermissions(roleID int64, permissions []accesscontrol.Permission) error {
// First drop existing permissions
@ -316,8 +145,7 @@ func (p *teamPermissionMigrator) migrateMemberships() error {
// Populate rolesMap with the newly created roles
for i := range createdRoles {
roleKey := p.getAssignmentKey(createdRoles[i].OrgID, createdRoles[i].Name)
rolesByOrg[roleKey] = createdRoles[i]
rolesByOrg[createdRoles[i].OrgID][createdRoles[i].Name] = createdRoles[i]
}
// Assign newly created roles
@ -329,14 +157,12 @@ func (p *teamPermissionMigrator) migrateMemberships() error {
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 userID, permissions := range userPermissions {
key := p.getAssignmentKey(orgID, fmt.Sprintf("managed:users:%d:permissions", userID))
role, ok := rolesByOrg[key]
role, ok := rolesByOrg[orgID][fmt.Sprintf("managed:users:%d:permissions", userID)]
if !ok {
return &ErrUnknownRole{key}
return &ErrUnknownRole{fmt.Sprintf("managed:users:%d:permissions", userID)}
}
if errSettingPerms := p.setRolePermissions(role.ID, permissions); errSettingPerms != nil {
@ -347,12 +173,12 @@ func (p *teamPermissionMigrator) setRolePermissionsForOrgs(userPermissionsByOrg
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
assignments := map[int64]map[string]struct{}{}
rolesByOrg := map[string]*accesscontrol.Role{}
rolesByOrg := map[int64]map[string]*accesscontrol.Role{}
for orgID, userPermissions := range userPermissionsByOrg {
for userID := range userPermissions {
roleName := fmt.Sprintf("managed:users:%d:permissions", userID)
@ -361,10 +187,12 @@ func (p *teamPermissionMigrator) sortRolesToAssign(userPermissionsByOrg map[int6
return nil, nil, nil, errFindingRoles
}
roleKey := p.getAssignmentKey(orgID, roleName)
if rolesByOrg[orgID] == nil {
rolesByOrg[orgID] = map[string]*accesscontrol.Role{}
}
if role.ID != 0 {
rolesByOrg[roleKey] = &role
rolesByOrg[orgID][roleName] = &role
} else {
roleToCreate := &accesscontrol.Role{
Name: roleName,
@ -372,13 +200,13 @@ func (p *teamPermissionMigrator) sortRolesToAssign(userPermissionsByOrg map[int6
}
rolesToCreate = append(rolesToCreate, roleToCreate)
userAssignments, initialized := assignments[userID]
userAssignments, initialized := assignments[orgID]
if !initialized {
userAssignments = map[string]struct{}{}
}
userAssignments[roleKey] = struct{}{}
assignments[userID] = userAssignments
userAssignments[roleName] = struct{}{}
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.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
accesscontrol.AddTeamMembershipMigrations(mg)
accesscontrol.AddDashboardPermissionsMigrator(mg)
}
}
addQueryHistoryStarMigrations(mg)

View File

@ -1,9 +1,11 @@
package permissions
import (
"context"
"strings"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
@ -73,3 +75,29 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) {
params = append(params, okRoles...)
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...)...)
}