mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access control: Use access control for dashboard and folder (#44702)
* Add actions and scopes * add resource service for dashboard and folder * Add dashboard guardian with fgac permission evaluation * Add CanDelete function to guardian interface * Add CanDelete property to folder and dashboard dto and set values * change to correct function name * Add accesscontrol to folder endpoints * add access control to dashboard endpoints * check access for nav links * Add fixed roles for dashboard and folders * use correct package * add hack to override guardian Constructor if accesscontrol is enabled * Add services * Add function to handle api backward compatability * Add permissionServices to HttpServer * Set permission when new dashboard is created * Add default permission when creating new dashboard * Set default permission when creating folder and dashboard * Add access control filter for dashboard search * Add to accept list * Add accesscontrol to dashboardimport * Disable access control in tests * Add check to see if user is allow to create a dashboard * Use SetPermissions * Use function to set several permissions at once * remove permissions for folder and dashboard on delete * update required permission * set permission for provisioning * Add CanCreate to dashboard guardian and set correct permisisons for provisioning * Dont set admin on folder / dashboard creation * Add dashboard and folder permission migrations * Add tests for CanCreate * Add roles and update descriptions * Solve uid to id for dashboard and folder permissions * Add folder and dashboard actions to permission filter * Handle viewer_can_edit flag * set folder and dashboard permissions services * Add dashboard permissions when importing a new dashboard * Set access control permissions on provisioning * Pass feature flags and only set permissions if access control is enabled * only add default permissions for folders and dashboards without folders * Batch create permissions in migrations * Remove `dashboards:edit` action * Remove unused function from interface * Update pkg/services/guardian/accesscontrol_guardian_test.go Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
parent
4caf5dbbd9
commit
4982ca3b1d
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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"`
|
||||
|
@ -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"`
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@ -71,17 +73,36 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
if hs.Cfg.EditorsCanAdmin {
|
||||
if err := hs.folderService.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil {
|
||||
if err := hs.setFolderPermission(c, folder.Id); err != nil {
|
||||
hs.log.Error("Could not make user admin", "folder", folder.Title, "user",
|
||||
c.SignedInUser.UserId, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
|
||||
return response.JSON(200, hs.toFolderDto(c.Req.Context(), g, folder))
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) setFolderPermission(c *models.ReqContext, folderID int64) error {
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
resourceID := strconv.FormatInt(folderID, 10)
|
||||
svc := hs.permissionServices.GetFolderService()
|
||||
|
||||
_, err := svc.SetPermissions(c.Req.Context(), c.OrgId, resourceID, []accesscontrol.SetResourcePermissionCommand{
|
||||
{UserID: c.UserId, Permission: models.PERMISSION_ADMIN.String()},
|
||||
{BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(models.ROLE_VIEWER), Permission: models.PERMISSION_VIEW.String()},
|
||||
}...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if hs.Cfg.EditorsCanAdmin {
|
||||
if err := hs.folderService.MakeUserAdmin(c.Req.Context(), c.OrgId, c.UserId, folderID, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
|
||||
cmd := models.UpdateFolderCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
@ -121,6 +142,7 @@ func (hs *HTTPServer) toFolderDto(ctx context.Context, g guardian.DashboardGuard
|
||||
canEdit, _ := g.CanEdit()
|
||||
canSave, _ := g.CanSave()
|
||||
canAdmin, _ := g.CanAdmin()
|
||||
canDelete, _ := g.CanDelete()
|
||||
|
||||
// Finding creator and last updater of the folder
|
||||
updater, creator := anonString, anonString
|
||||
@ -140,6 +162,7 @@ func (hs *HTTPServer) toFolderDto(ctx context.Context, g guardian.DashboardGuard
|
||||
CanSave: canSave,
|
||||
CanEdit: canEdit,
|
||||
CanAdmin: canAdmin,
|
||||
CanDelete: canDelete,
|
||||
CreatedBy: creator,
|
||||
Created: folder.Created,
|
||||
UpdatedBy: updater,
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -473,19 +473,26 @@ func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext, uaVisibleForOrg b
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink {
|
||||
children := []*dtos.NavLink{
|
||||
{Text: "Dashboard", Icon: "apps", Url: hs.Cfg.AppSubURL + "/dashboard/new", Id: "create-dashboard"},
|
||||
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||
var children []*dtos.NavLink
|
||||
|
||||
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(ac.ActionDashboardsCreate)) {
|
||||
children = append(children, &dtos.NavLink{Text: "Dashboard", Icon: "apps", Url: hs.Cfg.AppSubURL + "/dashboard/new", Id: "create-dashboard"})
|
||||
}
|
||||
if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
|
||||
|
||||
if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalPermission(ac.ActionFoldersCreate)) {
|
||||
children = append(children, &dtos.NavLink{
|
||||
Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder",
|
||||
Icon: "folder-plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new",
|
||||
})
|
||||
}
|
||||
|
||||
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(ac.ActionDashboardsCreate)) {
|
||||
children = append(children, &dtos.NavLink{
|
||||
Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "import",
|
||||
Url: hs.Cfg.AppSubURL + "/dashboard/import",
|
||||
})
|
||||
}
|
||||
|
||||
_, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgId]
|
||||
uaVisibleForOrg := hs.Cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg
|
||||
@ -539,11 +546,14 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) {
|
||||
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||
hasEditPerm := hasAccess(func(context *models.ReqContext) bool {
|
||||
hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
|
||||
if err := hs.SQLStore.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil {
|
||||
return nil, err
|
||||
return false
|
||||
}
|
||||
hasEditPerm := hasEditPermissionInFoldersQuery.Result
|
||||
return hasEditPermissionInFoldersQuery.Result
|
||||
}, ac.EvalAny(ac.EvalPermission(ac.ActionDashboardsCreate), ac.EvalPermission(ac.ActionFoldersCreate)))
|
||||
|
||||
settings, err := hs.getFrontendSettingsMap(c)
|
||||
if err != nil {
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -17,6 +17,8 @@ var sqlIDAcceptList = map[string]struct{}{
|
||||
"u.id": {},
|
||||
"\"user\".\"id\"": {}, // For Postgres
|
||||
"`user`.`id`": {}, // For MySQL and SQLite
|
||||
"dashboard.id": {},
|
||||
"dashboard.folder_id": {},
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -324,16 +324,40 @@ const (
|
||||
|
||||
// Annotations related actions
|
||||
ActionAnnotationsRead = "annotations:read"
|
||||
|
||||
ActionAnnotationsTagsRead = "annotations.tags:read"
|
||||
|
||||
ScopeAnnotationsAll = "annotations:*"
|
||||
ScopeAnnotationsTagsAll = "annotations:tags:*"
|
||||
|
||||
// Dashboard actions
|
||||
ActionDashboardsCreate = "dashboards:create"
|
||||
ActionDashboardsRead = "dashboards:read"
|
||||
ActionDashboardsWrite = "dashboards:write"
|
||||
ActionDashboardsDelete = "dashboards:delete"
|
||||
ActionDashboardsPermissionsRead = "dashboards.permissions:read"
|
||||
ActionDashboardsPermissionsWrite = "dashboards.permissions:write"
|
||||
|
||||
// Dashboard scopes
|
||||
ScopeDashboardsAll = "dashboards:*"
|
||||
|
||||
// Folder actions
|
||||
ActionFoldersCreate = "folders:create"
|
||||
ActionFoldersRead = "folders:read"
|
||||
ActionFoldersWrite = "folders:write"
|
||||
ActionFoldersDelete = "folders:delete"
|
||||
ActionFoldersPermissionsRead = "folders.permissions:read"
|
||||
ActionFoldersPermissionsWrite = "folders.permissions:write"
|
||||
|
||||
// Folder scopes
|
||||
ScopeFoldersAll = "folders:*"
|
||||
)
|
||||
|
||||
var (
|
||||
// Team scope
|
||||
ScopeTeamsID = Scope("teams", "id", Parameter(":teamId"))
|
||||
|
||||
// Folder scopes
|
||||
ScopeFolderID = Scope("folders", "id", Parameter(":id"))
|
||||
)
|
||||
|
||||
const RoleGrafanaAdmin = "Grafana Admin"
|
||||
|
@ -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
|
||||
|
@ -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 ""
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -2,14 +2,17 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport/api"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport/utils"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/schemaloader"
|
||||
@ -18,23 +21,29 @@ import (
|
||||
func ProvideService(routeRegister routing.RouteRegister,
|
||||
quotaService *quota.QuotaService, schemaLoaderService *schemaloader.SchemaLoaderService,
|
||||
pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store,
|
||||
libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService) *ImportDashboardService {
|
||||
libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService,
|
||||
ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, features featuremgmt.FeatureToggles,
|
||||
) *ImportDashboardService {
|
||||
s := &ImportDashboardService{
|
||||
features: features,
|
||||
pluginDashboardManager: pluginDashboardManager,
|
||||
dashboardService: dashboardService,
|
||||
libraryPanelService: libraryPanelService,
|
||||
dashboardPermissionsService: permissionsServices.GetDashboardService(),
|
||||
}
|
||||
|
||||
dashboardImportAPI := api.New(s, quotaService, schemaLoaderService, pluginStore)
|
||||
dashboardImportAPI := api.New(s, quotaService, schemaLoaderService, pluginStore, ac)
|
||||
dashboardImportAPI.RegisterAPIEndpoints(routeRegister)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
type ImportDashboardService struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
pluginDashboardManager plugins.PluginDashboardManager
|
||||
dashboardService dashboards.DashboardService
|
||||
libraryPanelService librarypanels.Service
|
||||
dashboardPermissionsService accesscontrol.PermissionsService
|
||||
}
|
||||
|
||||
func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
|
||||
@ -85,6 +94,12 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
if err := s.setDashboardPermissions(ctx, req.User, savedDash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &dashboardimport.ImportDashboardResponse{
|
||||
UID: savedDash.Uid,
|
||||
PluginId: req.PluginId,
|
||||
@ -100,3 +115,24 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb
|
||||
Slug: savedDash.Slug,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ImportDashboardService) setDashboardPermissions(ctx context.Context, user *models.SignedInUser, dashboard *models.Dashboard) error {
|
||||
resourceID := strconv.FormatInt(dashboard.Id, 10)
|
||||
|
||||
permissions := []accesscontrol.SetResourcePermissionCommand{
|
||||
{UserID: user.UserId, Permission: models.PERMISSION_ADMIN.String()},
|
||||
}
|
||||
|
||||
if dashboard.FolderId == 0 {
|
||||
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(models.ROLE_VIEWER), Permission: models.PERMISSION_VIEW.String()},
|
||||
}...)
|
||||
}
|
||||
_, err := s.dashboardPermissionsService.SetPermissions(ctx, user.OrgId, resourceID, permissions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
m "github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
@ -18,6 +19,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
var (
|
||||
provisionerPermissions = map[string][]string{
|
||||
accesscontrol.ActionFoldersCreate: {},
|
||||
accesscontrol.ActionFoldersWrite: {accesscontrol.ScopeFoldersAll},
|
||||
accesscontrol.ActionDashboardsCreate: {accesscontrol.ScopeFoldersAll},
|
||||
accesscontrol.ActionDashboardsWrite: {accesscontrol.ScopeFoldersAll},
|
||||
}
|
||||
)
|
||||
|
||||
type DashboardServiceImpl struct {
|
||||
dashboardStore m.Store
|
||||
dashAlertExtractor alerting.DashAlertExtractor
|
||||
@ -109,12 +119,21 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
|
||||
}
|
||||
|
||||
guard := guardian.New(ctx, dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
|
||||
if dash.Id == 0 {
|
||||
if canCreate, err := guard.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canCreate {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, models.ErrDashboardUpdateAccessDenied
|
||||
}
|
||||
} else {
|
||||
if canSave, err := guard.CanSave(); err != nil || !canSave {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, models.ErrDashboardUpdateAccessDenied
|
||||
}
|
||||
}
|
||||
|
||||
cmd := &models.SaveDashboardCommand{
|
||||
Dashboard: dash.Data,
|
||||
@ -181,6 +200,9 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
OrgId: dto.OrgId,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
dto.OrgId: provisionerPermissions,
|
||||
},
|
||||
}
|
||||
|
||||
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, true, false)
|
||||
@ -218,6 +240,7 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C
|
||||
dto.User = &models.SignedInUser{
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
Permissions: map[int64]map[string][]string{dto.OrgId: provisionerPermissions},
|
||||
}
|
||||
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, false)
|
||||
if err != nil {
|
||||
|
@ -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)
|
||||
}
|
||||
|
227
pkg/services/guardian/accesscontrol_guardian.go
Normal file
227
pkg/services/guardian/accesscontrol_guardian.go
Normal file
@ -0,0 +1,227 @@
|
||||
package guardian
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var permissionMap = map[string]models.PermissionType{
|
||||
"View": models.PERMISSION_VIEW,
|
||||
"Edit": models.PERMISSION_EDIT,
|
||||
"Admin": models.PERMISSION_ADMIN,
|
||||
}
|
||||
|
||||
var _ DashboardGuardian = new(AccessControlDashboardGuardian)
|
||||
|
||||
func NewAccessControlDashboardGuardian(
|
||||
ctx context.Context, dashboardId int64, user *models.SignedInUser,
|
||||
store *sqlstore.SQLStore, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices,
|
||||
) *AccessControlDashboardGuardian {
|
||||
return &AccessControlDashboardGuardian{
|
||||
ctx: ctx,
|
||||
dashboardID: dashboardId,
|
||||
user: user,
|
||||
store: store,
|
||||
ac: ac,
|
||||
permissionServices: permissionsServices,
|
||||
}
|
||||
}
|
||||
|
||||
type AccessControlDashboardGuardian struct {
|
||||
ctx context.Context
|
||||
dashboardID int64
|
||||
dashboard *models.Dashboard
|
||||
user *models.SignedInUser
|
||||
store *sqlstore.SQLStore
|
||||
ac accesscontrol.AccessControl
|
||||
permissionServices accesscontrol.PermissionsServices
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) CanSave() (bool, error) {
|
||||
if err := a.loadDashboard(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if a.dashboard.IsFolder {
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersWrite, folderScope(a.dashboardID)))
|
||||
}
|
||||
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Id)),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.dashboard.FolderId)),
|
||||
))
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) CanEdit() (bool, error) {
|
||||
if err := a.loadDashboard(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if setting.ViewersCanEdit {
|
||||
return a.CanView()
|
||||
}
|
||||
|
||||
if a.dashboard.IsFolder {
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersWrite, folderScope(a.dashboardID)))
|
||||
}
|
||||
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Id)),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.dashboard.FolderId)),
|
||||
))
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) CanView() (bool, error) {
|
||||
if err := a.loadDashboard(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if a.dashboard.IsFolder {
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersRead, folderScope(a.dashboardID)))
|
||||
}
|
||||
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, dashboardScope(a.dashboard.Id)),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, folderScope(a.dashboard.FolderId)),
|
||||
))
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) {
|
||||
if err := a.loadDashboard(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if a.dashboard.IsFolder {
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionFoldersPermissionsRead, folderScope(a.dashboard.Id)),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionFoldersPermissionsWrite, folderScope(a.dashboard.Id)),
|
||||
))
|
||||
}
|
||||
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
|
||||
accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, dashboardScope(a.dashboard.Id)),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, dashboardScope(a.dashboard.Id)),
|
||||
),
|
||||
accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, folderScope(a.dashboard.FolderId)),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, folderScope(a.dashboard.FolderId)),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) CanDelete() (bool, error) {
|
||||
if err := a.loadDashboard(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if a.dashboard.IsFolder {
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersDelete, folderScope(a.dashboard.Id)))
|
||||
}
|
||||
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, dashboardScope(a.dashboard.Id)),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, folderScope(a.dashboard.FolderId)),
|
||||
))
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool) (bool, error) {
|
||||
if isFolder {
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionFoldersCreate))
|
||||
}
|
||||
|
||||
return a.ac.Evaluate(a.ctx, a.user, accesscontrol.EvalPermission(accesscontrol.ActionDashboardsCreate, folderScope(folderID)))
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
|
||||
// always true for access control
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetAcl translate access control permissions to dashboard acl info
|
||||
func (a *AccessControlDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
|
||||
if err := a.loadDashboard(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := a.permissionServices.GetDashboardService()
|
||||
if a.dashboard.IsFolder {
|
||||
svc = a.permissionServices.GetFolderService()
|
||||
}
|
||||
|
||||
permissions, err := svc.GetPermissions(a.ctx, a.user, strconv.FormatInt(a.dashboard.Id, 10))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acl := make([]*models.DashboardAclInfoDTO, 0, len(permissions))
|
||||
for _, p := range permissions {
|
||||
if !p.IsManaged() {
|
||||
continue
|
||||
}
|
||||
|
||||
var role *models.RoleType
|
||||
if p.BuiltInRole != "" {
|
||||
tmp := models.RoleType(p.BuiltInRole)
|
||||
role = &tmp
|
||||
}
|
||||
|
||||
acl = append(acl, &models.DashboardAclInfoDTO{
|
||||
OrgId: a.dashboard.OrgId,
|
||||
DashboardId: a.dashboard.Id,
|
||||
FolderId: a.dashboard.FolderId,
|
||||
Created: p.Created,
|
||||
Updated: p.Updated,
|
||||
UserId: p.UserId,
|
||||
UserLogin: p.UserLogin,
|
||||
UserEmail: p.UserEmail,
|
||||
TeamId: p.TeamId,
|
||||
TeamEmail: p.TeamEmail,
|
||||
Team: p.Team,
|
||||
Role: role,
|
||||
Permission: permissionMap[svc.MapActions(p)],
|
||||
PermissionName: permissionMap[svc.MapActions(p)].String(),
|
||||
Uid: a.dashboard.Uid,
|
||||
Title: a.dashboard.Title,
|
||||
Slug: a.dashboard.Slug,
|
||||
IsFolder: a.dashboard.IsFolder,
|
||||
Url: a.dashboard.GetUrl(),
|
||||
Inherited: false,
|
||||
})
|
||||
}
|
||||
|
||||
return acl, nil
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) GetACLWithoutDuplicates() ([]*models.DashboardAclInfoDTO, error) {
|
||||
return a.GetAcl()
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*models.DashboardAcl, error) {
|
||||
// not used with access control
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *AccessControlDashboardGuardian) loadDashboard() error {
|
||||
if a.dashboard == nil {
|
||||
query := &models.GetDashboardQuery{Id: a.dashboardID, OrgId: a.user.OrgId}
|
||||
if err := a.store.GetDashboard(a.ctx, query); err != nil {
|
||||
return err
|
||||
}
|
||||
a.dashboard = query.Result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dashboardScope(dashboardID int64) string {
|
||||
return accesscontrol.Scope("dashboards", "id", strconv.FormatInt(dashboardID, 10))
|
||||
}
|
||||
|
||||
func folderScope(folderID int64) string {
|
||||
return accesscontrol.Scope("folders", "id", strconv.FormatInt(folderID, 10))
|
||||
}
|
557
pkg/services/guardian/accesscontrol_guardian_test.go
Normal file
557
pkg/services/guardian/accesscontrol_guardian_test.go
Normal file
@ -0,0 +1,557 @@
|
||||
package guardian
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||
dashdb "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type accessControlGuardianTestCase struct {
|
||||
desc string
|
||||
dashboardID int64
|
||||
permissions []*accesscontrol.Permission
|
||||
viewersCanEdit bool
|
||||
expected bool
|
||||
}
|
||||
|
||||
func TestAccessControlDashboardGuardian_CanSave(t *testing.T) {
|
||||
tests := []accessControlGuardianTestCase{
|
||||
{
|
||||
desc: "should be able to save with dashboard wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "dashboards:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to save with folder wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "folders:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to save with dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "dashboards:id:1",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to save with folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "folders:id:0",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to save with incorrect dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "dashboards:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to save with incorrect folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "folders:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
|
||||
|
||||
can, err := guardian.CanSave()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, can)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) {
|
||||
tests := []accessControlGuardianTestCase{
|
||||
{
|
||||
desc: "should be able to edit with dashboard wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "dashboards:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to edit with folder wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "folders:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to edit with dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "dashboards:id:1",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to edit with folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "folders:id:0",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to edit with incorrect dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "dashboards:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to edit with incorrect folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsWrite,
|
||||
Scope: "folders:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "should be able to edit with read action when viewer_can_edit is true",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsRead,
|
||||
Scope: "dashboards:id:1",
|
||||
},
|
||||
},
|
||||
viewersCanEdit: true,
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
|
||||
|
||||
if tt.viewersCanEdit {
|
||||
setting.ViewersCanEdit = true
|
||||
defer func() { setting.ViewersCanEdit = false }()
|
||||
}
|
||||
|
||||
can, err := guardian.CanEdit()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, can)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessControlDashboardGuardian_CanView(t *testing.T) {
|
||||
tests := []accessControlGuardianTestCase{
|
||||
{
|
||||
desc: "should be able to view with dashboard wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsRead,
|
||||
Scope: "dashboards:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to view with folder wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsRead,
|
||||
Scope: "folders:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to view with dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsRead,
|
||||
Scope: "dashboards:id:1",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to view with folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsRead,
|
||||
Scope: "folders:id:0",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to view with incorrect dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsRead,
|
||||
Scope: "dashboards:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to view with incorrect folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsRead,
|
||||
Scope: "folders:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
|
||||
|
||||
can, err := guardian.CanView()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, can)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) {
|
||||
tests := []accessControlGuardianTestCase{
|
||||
{
|
||||
desc: "should be able to admin with dashboard wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsRead,
|
||||
Scope: "dashboards:*",
|
||||
},
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsWrite,
|
||||
Scope: "dashboards:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to admin with folder wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsRead,
|
||||
Scope: "folders:*",
|
||||
},
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsWrite,
|
||||
Scope: "folders:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to admin with dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsRead,
|
||||
Scope: "dashboards:id:1",
|
||||
},
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsWrite,
|
||||
Scope: "dashboards:id:1",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to admin with folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsRead,
|
||||
Scope: "folders:id:0",
|
||||
},
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsWrite,
|
||||
Scope: "folders:id:0",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to admin with incorrect dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsRead,
|
||||
Scope: "dashboards:id:10",
|
||||
},
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsWrite,
|
||||
Scope: "dashboards:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to admin with incorrect folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsRead,
|
||||
Scope: "folders:id:10",
|
||||
},
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsPermissionsWrite,
|
||||
Scope: "folders:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
|
||||
|
||||
can, err := guardian.CanAdmin()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, can)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) {
|
||||
tests := []accessControlGuardianTestCase{
|
||||
{
|
||||
desc: "should be able to delete with dashboard wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsDelete,
|
||||
Scope: "dashboards:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to delete with folder wildcard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsDelete,
|
||||
Scope: "folders:*",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to delete with dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsDelete,
|
||||
Scope: "dashboards:id:1",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to delete with folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsDelete,
|
||||
Scope: "folders:id:0",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to delete with incorrect dashboard scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsDelete,
|
||||
Scope: "dashboards:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to delete with incorrect folder scope",
|
||||
dashboardID: 1,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDashboardsDelete,
|
||||
Scope: "folders:id:10",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
guardian := setupAccessControlGuardianTest(t, tt.dashboardID, tt.permissions)
|
||||
|
||||
can, err := guardian.CanDelete()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, can)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type accessControlGuardianCanCreateTestCase struct {
|
||||
desc string
|
||||
isFolder bool
|
||||
folderID int64
|
||||
permissions []*accesscontrol.Permission
|
||||
expected bool
|
||||
}
|
||||
|
||||
func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) {
|
||||
tests := []accessControlGuardianCanCreateTestCase{
|
||||
{
|
||||
desc: "should be able to create dashboard in folder 0",
|
||||
isFolder: false,
|
||||
folderID: 0,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:id:0"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should be able to create dashboard in any folder",
|
||||
isFolder: false,
|
||||
folderID: 100,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:*"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to create dashboard without permissions",
|
||||
isFolder: false,
|
||||
folderID: 100,
|
||||
permissions: []*accesscontrol.Permission{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "should be able to create folder with correct permissions",
|
||||
isFolder: true,
|
||||
folderID: 0,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionFoldersCreate},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to create folders without permissions",
|
||||
isFolder: true,
|
||||
folderID: 100,
|
||||
permissions: []*accesscontrol.Permission{},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
guardian := setupAccessControlGuardianTest(t, 0, tt.permissions)
|
||||
|
||||
can, err := guardian.CanCreate(tt.folderID, tt.isFolder)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, can)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupAccessControlGuardianTest(t *testing.T, dashID int64, permissions []*accesscontrol.Permission) *AccessControlDashboardGuardian {
|
||||
t.Helper()
|
||||
store := sqlstore.InitTestDB(t)
|
||||
// seed dashboard
|
||||
_, err := dashdb.ProvideDashboardStore(store).SaveDashboard(models.SaveDashboardCommand{
|
||||
Dashboard: &simplejson.Json{},
|
||||
UserId: 1,
|
||||
OrgId: 1,
|
||||
FolderId: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ac := accesscontrolmock.New().WithPermissions(permissions)
|
||||
services, err := ossaccesscontrol.ProvidePermissionsServices(routing.NewRouteRegister(), store, ac, database.ProvideService(store))
|
||||
require.NoError(t, err)
|
||||
|
||||
return NewAccessControlDashboardGuardian(context.Background(), dashID, &models.SignedInUser{OrgId: 1}, store, ac, services)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
22
pkg/services/guardian/provider.go
Normal file
22
pkg/services/guardian/provider.go
Normal file
@ -0,0 +1,22 @@
|
||||
package guardian
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
type Provider struct{}
|
||||
|
||||
func ProvideService(store *sqlstore.SQLStore, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, features featuremgmt.FeatureToggles) *Provider {
|
||||
if features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
// TODO: Fix this hack, see https://github.com/grafana/grafana-enterprise/issues/2935
|
||||
New = func(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian {
|
||||
return NewAccessControlDashboardGuardian(ctx, dashId, user, store, ac, permissionsServices)
|
||||
}
|
||||
}
|
||||
return &Provider{}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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)))
|
||||
}
|
@ -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")
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -1,9 +1,11 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
@ -73,3 +75,29 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) {
|
||||
params = append(params, okRoles...)
|
||||
return sql, params
|
||||
}
|
||||
|
||||
type AccessControlDashboardPermissionFilter struct {
|
||||
User *models.SignedInUser
|
||||
}
|
||||
|
||||
func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{}) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("(((")
|
||||
|
||||
dashFilter, _ := accesscontrol.Filter(context.Background(), "dashboard.id", "dashboards", "dashboards:read", f.User)
|
||||
builder.WriteString(dashFilter.Where)
|
||||
|
||||
builder.WriteString(" OR ")
|
||||
|
||||
dashFolderFilter, _ := accesscontrol.Filter(context.Background(), "dashboard.folder_id", "folders", "dashboards:read", f.User)
|
||||
builder.WriteString(dashFolderFilter.Where)
|
||||
|
||||
builder.WriteString(") AND NOT dashboard.is_folder) OR (")
|
||||
|
||||
folderFilter, _ := accesscontrol.Filter(context.Background(), "dashboard.id", "folders", "folders:read", f.User)
|
||||
builder.WriteString(folderFilter.Where)
|
||||
builder.WriteString(" AND dashboard.is_folder))")
|
||||
|
||||
return builder.String(), append(dashFilter.Args, append(dashFolderFilter.Args, folderFilter.Args...)...)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user