diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 6088acfdf96..0d2955b58a2 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -40,7 +40,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { folderPermissions := accesscontrolmock.NewMockedPermissionsService() dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), settings, dashboardStore, dashboards.NewFakeFolderStore(t), mockSQLStore, featuremgmt.WithFeatures(), nil) + folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), settings, dashboardStore, dashboards.NewFakeFolderStore(t), mockSQLStore, featuremgmt.WithFeatures()) hs := &HTTPServer{ Cfg: settings, SQLStore: mockSQLStore, diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 2d2f0d42d22..e5bbf001564 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -983,7 +983,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() features := featuremgmt.WithFeatures() - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, db.InitTestDB(t), featuremgmt.WithFeatures(), nil) + folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, db.InitTestDB(t), featuremgmt.WithFeatures()) if dashboardService == nil { dashboardService = service.ProvideDashboardService( diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 5470d46bda8..a2f483450d2 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -1,6 +1,7 @@ package api import ( + "context" "errors" "net/http" "strconv" @@ -8,13 +9,16 @@ import ( "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" ) @@ -152,6 +156,10 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response return apierrors.ToFolderErrorResponse(err) } + if err := hs.setDefaultFolderPermissions(c.Req.Context(), cmd.OrgID, cmd.SignedInUser, folder); err != nil { + hs.log.Error("Could not set the default folder permissions", "folder", folder.Title, "user", cmd.SignedInUser, "error", err) + } + // Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call // Required for cases when caller wants to immediately interact with the newly created object if !hs.AccessControl.IsDisabled() { @@ -167,6 +175,30 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response return response.JSON(http.StatusOK, hs.newToFolderDto(c, g, folder)) } +func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int64, user *user.SignedInUser, folder *folder.Folder) error { + // Set default folder permissions + var permissionErr error + if !accesscontrol.IsDisabled(hs.Cfg) { + var permissions []accesscontrol.SetResourcePermissionCommand + if user.IsRealUser() && !user.IsAnonymous { + permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ + UserID: user.UserID, Permission: dashboards.PERMISSION_ADMIN.String(), + }) + } + + permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ + {BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, + {BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()}, + }...) + + _, permissionErr = hs.folderPermissionsService.SetPermissions(ctx, orgID, folder.UID, permissions...) + return permissionErr + } else if hs.Cfg.EditorsCanAdmin && user.IsRealUser() && !user.IsAnonymous { + return hs.folderService.MakeUserAdmin(ctx, orgID, user.UserID, folder.ID, true) + } + return nil +} + func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response { if hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) { cmd := folder.MoveFolderCommand{} diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index b441fa3ae22..f5a56a1f74b 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -248,12 +248,15 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) store := dbtest.NewFakeDB() guardian.InitLegacyGuardian(store, dashSvc, teamSvc) + folderPermissions := acmock.NewMockedPermissionsService() + folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) hs := HTTPServer{ - AccessControl: acmock.New(), - folderService: folderService, - Cfg: setting.NewCfg(), - Features: featuremgmt.WithFeatures(), - accesscontrolService: actest.FakeService{}, + AccessControl: acmock.New(), + folderService: folderService, + Cfg: setting.NewCfg(), + Features: featuremgmt.WithFeatures(), + accesscontrolService: actest.FakeService{}, + folderPermissionsService: folderPermissions, } sc := setupScenarioContext(t, url) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index aed3173e305..b156bfb654d 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -8,8 +8,11 @@ import ( "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/util/errutil" ) +var ErrInternal = errutil.NewBase(errutil.StatusInternal, "accesscontrol.internal") + // RoleRegistration stores a role and its assignments to built-in roles // (Viewer, Editor, Admin, Grafana Admin) type RoleRegistration struct { diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 81ed7e0ca01..9aa79fe51fb 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/retriever" @@ -114,7 +115,7 @@ var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.Act func ProvideDashboardPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { @@ -152,7 +153,13 @@ func ProvideDashboardPermissions( if err != nil { return nil, err } - return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID)}, nil + parentScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID) + + nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderService) + if err != nil { + return nil, err + } + return append([]string{parentScope}, nestedScopes...), nil } return []string{}, nil }, @@ -195,7 +202,7 @@ var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFol func ProvideFolderPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*FolderPermissionsService, error) { options := resourcepermissions.Options{ @@ -214,6 +221,9 @@ func ProvideFolderPermissions( return nil }, + InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { + return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderService) + }, Assignments: resourcepermissions.Assignments{ Users: true, Teams: true, diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index 3c401ada3e1..16ea72b2d59 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -54,7 +54,7 @@ func NewFolderNameScopeResolver(db Store, folderDB FolderStore, folderSvc folder return nil, err } - result, err := getInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) if err != nil { return nil, err } @@ -86,7 +86,7 @@ func NewFolderIDScopeResolver(db Store, folderDB FolderStore, folderSvc folder.S return nil, err } - result, err := getInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) if err != nil { return nil, err } @@ -158,7 +158,7 @@ func resolveDashboardScope(ctx context.Context, db Store, folderDB FolderStore, folderUID = folder.UID } - result, err := getInheritedScopes(ctx, orgID, folderUID, folderSvc) + result, err := GetInheritedScopes(ctx, orgID, folderUID, folderSvc) if err != nil { return nil, err } @@ -173,15 +173,14 @@ func resolveDashboardScope(ctx context.Context, db Store, folderDB FolderStore, return result, nil } -func getInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderSvc folder.Service) ([]string, error) { +func GetInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderSvc folder.Service) ([]string, error) { ancestors, err := folderSvc.GetParents(ctx, folder.GetParentsQuery{ UID: folderUID, OrgID: orgID, }) if err != nil { - // TODO return a specific error - return nil, err + return nil, ac.ErrInternal.Errorf("could not retrieve folder parents: %w", err) } result := make([]string, 0, len(ancestors)) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 0ac9918b47b..e9da3fa4cec 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -32,7 +32,6 @@ type Service struct { dashboardStore dashboards.Store dashboardFolderStore dashboards.FolderStore features featuremgmt.FeatureToggles - permissions accesscontrol.FolderPermissionsService accessControl accesscontrol.AccessControl // bus is currently used to publish events that cause scheduler to update rules. @@ -47,7 +46,6 @@ func ProvideService( folderStore dashboards.FolderStore, db db.DB, // DB for the (new) nested folder store features featuremgmt.FeatureToggles, - folderPermissionsService accesscontrol.FolderPermissionsService, ) folder.Service { store := ProvideStore(db, cfg, features) svr := &Service{ @@ -57,7 +55,6 @@ func ProvideService( dashboardFolderStore: folderStore, store: store, features: features, - permissions: folderPermissionsService, accessControl: ac, bus: bus, } @@ -311,29 +308,6 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( return nil, err } - var permissionErr error - if !accesscontrol.IsDisabled(s.cfg) { - var permissions []accesscontrol.SetResourcePermissionCommand - if user.IsRealUser() && !user.IsAnonymous { - permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ - UserID: userID, Permission: dashboards.PERMISSION_ADMIN.String(), - }) - } - - permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, - {BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()}, - }...) - - _, permissionErr = s.permissions.SetPermissions(ctx, cmd.OrgID, createdFolder.UID, permissions...) - } else if s.cfg.EditorsCanAdmin && user.IsRealUser() && !user.IsAnonymous { - permissionErr = s.MakeUserAdmin(ctx, cmd.OrgID, userID, createdFolder.ID, true) - } - - if permissionErr != nil { - logger.Error("Could not make user admin", "folder", createdFolder.Title, "user", userID, "error", permissionErr) - } - var nestedFolder *folder.Folder if s.features.IsEnabled(featuremgmt.FlagNestedFolders) { cmd := &folder.CreateFolderCommand{ diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 948178d3ace..3f989943f06 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -37,7 +37,7 @@ func TestIntegrationProvideFolderService(t *testing.T) { t.Run("should register scope resolvers", func(t *testing.T) { cfg := setting.NewCfg() ac := acmock.New() - ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, nil, &featuremgmt.FeatureManager{}, nil) + ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, nil, &featuremgmt.FeatureManager{}) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2) }) @@ -57,7 +57,6 @@ func TestIntegrationFolderService(t *testing.T) { cfg := setting.NewCfg() cfg.RBACEnabled = false features := featuremgmt.WithFeatures() - folderPermissions := acmock.NewMockedPermissionsService() service := &Service{ cfg: cfg, @@ -66,7 +65,6 @@ func TestIntegrationFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, - permissions: folderPermissions, bus: bus.ProvideBus(tracing.InitializeTracerForTest()), } diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index b457977a42a..f64b3ddc7cc 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -610,10 +610,10 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce require.NoError(t, err) folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( - setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac, teamSvc, userSvc) + setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, foldertest.NewFakeService(), ac, teamSvc, userSvc) require.NoError(t, err) dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( - setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac, teamSvc, userSvc) + setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, foldertest.NewFakeService(), ac, teamSvc, userSvc) require.NoError(t, err) if dashboardSvc == nil { fakeDashboardService := dashboards.NewFakeDashboardService(t) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 68b852bcea5..cce5e99f181 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -313,12 +313,11 @@ func createFolderWithACL(t *testing.T, sqlStore db.DB, title string, user user.S features := featuremgmt.WithFeatures() cfg.IsFeatureToggleEnabled = features.IsEnabled ac := acmock.New() - folderPermissions := acmock.NewMockedPermissionsService() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features, folderPermissions) + s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features) t.Logf("Creating folder with title and UID %q", title) ctx := appcontext.WithUser(context.Background(), &user) folder, err := s.Create(ctx, &folder.CreateFolderCommand{ @@ -447,7 +446,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo service := LibraryElementService{ Cfg: sqlStore.Cfg, SQLStore: sqlStore, - folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, dashboardStore, nil, features, folderPermissions), + folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, dashboardStore, nil, features), } // deliberate difference between signed in user and user in db to make it crystal clear diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 85929ee8282..c6110ce11f5 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -725,11 +725,10 @@ func createFolderWithACL(t *testing.T, sqlStore db.DB, title string, user *user. cfg.RBACEnabled = false cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled features := featuremgmt.WithFeatures() - folderPermissions := acmock.NewMockedPermissionsService() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features, folderPermissions) + s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features) t.Logf("Creating folder with title and UID %q", title) ctx := appcontext.WithUser(context.Background(), user) @@ -837,9 +836,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo features := featuremgmt.WithFeatures() ac := acmock.New() - folderPermissions := acmock.NewMockedPermissionsService() - - folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features, folderPermissions) + folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features) elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService) service := LibraryPanelService{ diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 2d07d32d163..18313f8d3d0 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -87,7 +87,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, tracer := tracing.InitializeTracerForTest() bus := bus.ProvideBus(tracer) - folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, dashboardStore, nil, features, folderPermissions) + folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, dashboardStore, nil, features) ng, err := ngalert.ProvideService( cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotatest.New(false, nil),