diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 5215869edf8..d6f4bbeb512 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -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, ) } diff --git a/pkg/api/api.go b/pkg/api/api.go index 8f352f87300..3b4544e9fb3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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)) }) }) }) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 8568a8b4615..95354c42889 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -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} diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index da6833dc395..9ce4d46db06 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -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 { diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 9d40c5529a6..7c17b7def6b 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -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) { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index ea37a7da2ba..7344604a315 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -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) diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 22dea05b499..c7f14f12ab7 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -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"` diff --git a/pkg/api/dtos/folder.go b/pkg/api/dtos/folder.go index 469656c6f8f..f6576a053b9 100644 --- a/pkg/api/dtos/folder.go +++ b/pkg/api/dtos/folder.go @@ -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"` diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 1c08a0403da..8ab9d95390a 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -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 { - hs.log.Error("Could not make user admin", "folder", folder.Title, "user", - c.SignedInUser.UserId, "error", err) - } + 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, diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index ca03d2497e8..554de386224 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -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 diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 1405b75ba2a..5538bc8c693 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -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() diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index cf10dc46952..e61f232c1f3 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -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) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 7e6602d46db..ff64c079f33 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -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") diff --git a/pkg/api/index.go b/pkg/api/index.go index 3f7a17e5764..50b13d23741 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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", }) } - 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", - }) + + 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) { - hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} - if err := hs.SQLStore.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { - return nil, err - } - hasEditPerm := hasEditPermissionInFoldersQuery.Result + 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 false + } + return hasEditPermissionInFoldersQuery.Result + }, ac.EvalAny(ac.EvalPermission(ac.ActionDashboardsCreate), ac.EvalPermission(ac.ActionFoldersCreate))) settings, err := hs.getFrontendSettingsMap(c) if err != nil { diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go index cb6f7ec27e4..58028e80448 100644 --- a/pkg/server/backgroundsvcs/background_services.go +++ b/pkg/server/backgroundsvcs/background_services.go @@ -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, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 80569c670ed..2fc15c7cb07 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -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( diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 37c66778ded..81b21fad048 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -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( diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 015dcd2ab90..14bfa63e55c 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -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 { diff --git a/pkg/services/accesscontrol/filter.go b/pkg/services/accesscontrol/filter.go index 01e2a83184a..635350982fa 100644 --- a/pkg/services/accesscontrol/filter.go +++ b/pkg/services/accesscontrol/filter.go @@ -10,13 +10,15 @@ import ( ) var sqlIDAcceptList = map[string]struct{}{ - "org_user.user_id": {}, - "role.id": {}, - "t.id": {}, - "team.id": {}, - "u.id": {}, - "\"user\".\"id\"": {}, // For Postgres - "`user`.`id`": {}, // For MySQL and SQLite + "org_user.user_id": {}, + "role.id": {}, + "t.id": {}, + "team.id": {}, + "u.id": {}, + "\"user\".\"id\"": {}, // For Postgres + "`user`.`id`": {}, // For MySQL and SQLite + "dashboard.id": {}, + "dashboard.folder_id": {}, } var ( diff --git a/pkg/services/accesscontrol/mock/permissions_services_mock.go b/pkg/services/accesscontrol/mock/permissions_services_mock.go index b5d084bdce9..2d3d9d41304 100644 --- a/pkg/services/accesscontrol/mock/permissions_services_mock.go +++ b/pkg/services/accesscontrol/mock/permissions_services_mock.go @@ -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 } diff --git a/pkg/services/accesscontrol/mock/service_mock.go b/pkg/services/accesscontrol/mock/service_mock.go index fae65edeb72..085257689c7 100644 --- a/pkg/services/accesscontrol/mock/service_mock.go +++ b/pkg/services/accesscontrol/mock/service_mock.go @@ -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) +} diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 99236f7ee36..851ad067d0d 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -323,17 +323,41 @@ const ( ScopeTeamsAll = "teams:*" // Annotations related actions - ActionAnnotationsRead = "annotations:read" - + 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" diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go index 51a928495c7..4ac29c682e6 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go @@ -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 diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index a0babb86346..1ee650a5fb0 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -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 "" +} diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 78b6f6c0a34..2e2e18d837c 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -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) diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index f0acd494b67..2d09abf3325 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -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 diff --git a/pkg/services/dashboardimport/api/api.go b/pkg/services/dashboardimport/api/api.go index 9a9249bae33..5e721ff6d1e 100644 --- a/pkg/services/dashboardimport/api/api.go +++ b/pkg/services/dashboardimport/api/api.go @@ -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) } diff --git a/pkg/services/dashboardimport/api/api_test.go b/pkg/services/dashboardimport/api/api_test.go index 857811da650..539ecb8f5e1 100644 --- a/pkg/services/dashboardimport/api/api_test.go +++ b/pkg/services/dashboardimport/api/api_test.go @@ -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) diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index eead5a7b046..b1c10d73fd1 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -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{ - pluginDashboardManager: pluginDashboardManager, - dashboardService: dashboardService, - libraryPanelService: libraryPanelService, + 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 { - pluginDashboardManager plugins.PluginDashboardManager - dashboardService dashboards.DashboardService - libraryPanelService librarypanels.Service + 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 +} diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index e72a80e682a..e168c1d1fe3 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -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, } diff --git a/pkg/services/dashboards/manager/dashboard_service.go b/pkg/services/dashboards/manager/dashboard_service.go index a48829ee8c8..3b1dacfed3e 100644 --- a/pkg/services/dashboards/manager/dashboard_service.go +++ b/pkg/services/dashboards/manager/dashboard_service.go @@ -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,11 +119,20 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } guard := guardian.New(ctx, dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User) - if canSave, err := guard.CanSave(); err != nil || !canSave { - if err != nil { - return nil, err + if dash.Id == 0 { + if canCreate, err := guard.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canCreate { + if err != nil { + return nil, err + } + return nil, models.ErrDashboardUpdateAccessDenied + } + } else { + if canSave, err := guard.CanSave(); err != nil || !canSave { + if err != nil { + return nil, err + } + return nil, models.ErrDashboardUpdateAccessDenied } - return nil, models.ErrDashboardUpdateAccessDenied } cmd := &models.SaveDashboardCommand{ @@ -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) @@ -216,8 +238,9 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *m.SaveDashboardDTO) (*models.Dashboard, error) { dto.User = &models.SignedInUser{ - UserId: 0, - OrgRole: models.ROLE_ADMIN, + 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 { diff --git a/pkg/services/dashboards/manager/folder_service.go b/pkg/services/dashboards/manager/folder_service.go index 2876496bb12..b1424176336 100644 --- a/pkg/services/dashboards/manager/folder_service.go +++ b/pkg/services/dashboards/manager/folder_service.go @@ -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) } diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go new file mode 100644 index 00000000000..976d11034ea --- /dev/null +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -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)) +} diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go new file mode 100644 index 00000000000..65a69e07242 --- /dev/null +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -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) +} diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 27221e96644..1722827021e 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -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 } diff --git a/pkg/services/guardian/provider.go b/pkg/services/guardian/provider.go new file mode 100644 index 00000000000..d72b402cad5 --- /dev/null +++ b/pkg/services/guardian/provider.go @@ -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{} +} diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index e34c3a0b25e..07143d26452 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -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) } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 59755ecd14e..12bae81eb77 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -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 } diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_linux_test.go index c8f288d52f4..5050eefaaa1 100644 --- a/pkg/services/provisioning/dashboards/file_reader_linux_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_linux_test.go @@ -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") } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 1c2def3a342..eb5cb2c4e56 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -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) diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index e571ca2dbfe..eea41e13fe1 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -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) diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index dd23fd48e92..6fa9e322b5f 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -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) } diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index 4493d6569dd..9bac4f2f7b7 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -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, diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 6241d347274..4816d98d6d4 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -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 { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go new file mode 100644 index 00000000000..378ee381785 --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go @@ -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))) +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/permission_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/permission_migrator.go new file mode 100644 index 00000000000..9cd98f98e38 --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/permission_migrator.go @@ -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") +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go index 8e1cd545ce1..71218ed2674 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go @@ -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 } } } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index bad451879d8..8877988e1ab 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -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) diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index f72336a298e..2b0b221a72a 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -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" ) @@ -52,7 +54,7 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) { -- include default permissions --> da.org_id = -1 AND ( (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR - (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) ) ) WHERE @@ -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...)...) +}