diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 0498890e14a..5703028f2cb 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -221,6 +221,7 @@ export interface GrafanaConfig { rudderstackConfigUrl: string | undefined; rudderstackIntegrationsUrl: string | undefined; sqlConnectionLimits: SqlConnectionLimits; + sharedWithMeFolderUID?: string; // The namespace to use for kubernetes apiserver requests namespace: string; diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e4603e8bb79..90cb33a42df 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -838,19 +838,19 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), - cfg, dashboardStore, folderStore, db.InitTestDB(t), features) + cfg, dashboardStore, folderStore, db.InitTestDB(t), features, nil) if dashboardService == nil { dashboardService, err = service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, - ac, folderSvc, + ac, folderSvc, nil, ) require.NoError(t, err) } dashboardProvisioningService, err := service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, - ac, folderSvc, + ac, folderSvc, nil, ) require.NoError(t, err) diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 2ecf0c9f4c1..166dae62160 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -220,6 +220,7 @@ type FrontendSettingsDTO struct { SamlEnabled bool `json:"samlEnabled"` SamlName string `json:"samlName"` TokenExpirationDayLimit int `json:"tokenExpirationDayLimit"` + SharedWithMeFolderUID string `json:"sharedWithMeFolderUID"` GeomapDefaultBaseLayerConfig *map[string]any `json:"geomapDefaultBaseLayerConfig,omitempty"` GeomapDisableCustomBaseLayer bool `json:"geomapDisableCustomBaseLayer"` diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 1096b47989b..894754c5e3d 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -467,7 +467,7 @@ func setupServer(b testing.TB, sc benchScenario, features *featuremgmt.FeatureMa folderStore := folderimpl.ProvideDashboardFolderStore(sc.db) ac := acimpl.ProvideAccessControl(sc.cfg) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sc.cfg, dashStore, folderStore, sc.db, features) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sc.cfg, dashStore, folderStore, sc.db, features, nil) folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc) @@ -479,7 +479,7 @@ func setupServer(b testing.TB, sc benchScenario, features *featuremgmt.FeatureMa dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( sc.cfg, dashStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, - folderServiceWithFlagOn, + folderServiceWithFlagOn, nil, ) require.NoError(b, err) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index af901f458ef..215714b6f83 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -13,6 +13,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -156,6 +157,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro SecureSocksDSProxyEnabled: hs.Cfg.SecureSocksDSProxy.Enabled && hs.Cfg.SecureSocksDSProxy.ShowUI, DisableFrontendSandboxForPlugins: hs.Cfg.DisableFrontendSandboxForPlugins, PublicDashboardAccessToken: c.PublicDashboardAccessToken, + SharedWithMeFolderUID: folder.SharedWithMeFolderUID, Auth: dtos.FrontendSettingsAuthDTO{ OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync, diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 3d8b5b829b9..ac51ecc0f8b 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -22,7 +22,9 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/migrator" "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils" "github.com/grafana/grafana/pkg/services/auth/identity" + "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/user" "github.com/grafana/grafana/pkg/setting" ) @@ -33,6 +35,11 @@ const ( cacheTTL = 10 * time.Second ) +var SharedWithMeFolderPermission = accesscontrol.Permission{ + Action: dashboards.ActionFoldersRead, + Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.SharedWithMeFolderUID), +} + func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService, accessControl accesscontrol.AccessControl, features *featuremgmt.FeatureManager) (*Service, error) { service := ProvideOSSService(cfg, database.ProvideService(db), cache, features) @@ -115,6 +122,10 @@ func (s *Service) getUserPermissions(ctx context.Context, user identity.Requeste } } + if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { + permissions = append(permissions, SharedWithMeFolderPermission) + } + userID, err := identity.UserIdentifier(user.GetNamespacedID()) if err != nil { return nil, err diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index dd51a69b91d..9b6a824e423 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -6,6 +6,9 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" @@ -26,8 +29,6 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { @@ -220,7 +221,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { }) ac := acimpl.ProvideAccessControl(sql.Cfg) - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sql.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features) + folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sql.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features, nil) cfg := setting.NewCfg() cfg.AnnotationMaximumTagsLength = 60 diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index c94da1f3f3b..cbb17fa5945 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -24,6 +24,7 @@ type DashboardService interface { SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) (model.HitList, error) CountInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) (int64, error) + GetDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*Dashboard, error) } // PluginService is a service for operating on plugin dashboards. diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 987252cab7d..239468e14f1 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -286,6 +286,31 @@ func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *Fin return r0, r1 } +func (_m *FakeDashboardService) GetDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*Dashboard, error) { + ret := _m.Called(ctx, user) + + var r0 []*Dashboard + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) ([]*Dashboard, error)); ok { + return rf(ctx, user) + } + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) []*Dashboard); ok { + r0 = rf(ctx, user) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Dashboard) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, identity.Requester) error); ok { + r1 = rf(ctx, user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + type mockConstructorTestingTNewFakeDashboardService interface { mock.TestingT Cleanup(func()) diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index 788484996ed..86a8d52f65a 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -294,7 +294,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { guardian.New = origNewGuardian }) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, nil) parentUID := "" for i := 0; ; i++ { diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index d8f940cea8a..febcdb22d14 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -689,7 +689,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { ac := acimpl.ProvideAccessControl(sqlStore.Cfg) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, nil) user := &user.SignedInUser{ OrgID: 1, @@ -806,7 +806,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { ac := acimpl.ProvideAccessControl(sqlStore.Cfg) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, nil) user := &user.SignedInUser{ OrgID: 1, diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 27713908a23..49ee617d39a 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -4,6 +4,11 @@ import ( "context" "fmt" "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "golang.org/x/exp/slices" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" @@ -49,6 +54,7 @@ type DashboardServiceImpl struct { folderPermissions accesscontrol.FolderPermissionsService dashboardPermissions accesscontrol.DashboardPermissionsService ac accesscontrol.AccessControl + metrics *dashboardsMetrics } // This is the uber service that implements a three smaller services @@ -56,7 +62,7 @@ func ProvideDashboardServiceImpl( cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore, dashAlertExtractor alerting.DashAlertExtractor, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl, - folderSvc folder.Service, + folderSvc folder.Service, r prometheus.Registerer, ) (*DashboardServiceImpl, error) { dashSvc := &DashboardServiceImpl{ cfg: cfg, @@ -69,6 +75,7 @@ func ProvideDashboardServiceImpl( ac: ac, folderStore: folderStore, folderService: folderSvc, + metrics: newDashboardsMetrics(r), } ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, folderSvc)) @@ -537,7 +544,112 @@ func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashbo return dr.dashboardStore.GetDashboards(ctx, query) } +func (dr *DashboardServiceImpl) GetDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*dashboards.Dashboard, error) { + return dr.getDashboardsSharedWithUser(ctx, user) +} + +func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*dashboards.Dashboard, error) { + permissions := user.GetPermissions() + dashboardPermissions := permissions[dashboards.ActionDashboardsRead] + sharedDashboards := make([]*dashboards.Dashboard, 0) + dashboardUids := make([]string, 0) + for _, p := range dashboardPermissions { + if dashboardUid, found := strings.CutPrefix(p, dashboards.ScopeDashboardsPrefix); found { + if !slices.Contains(dashboardUids, dashboardUid) { + dashboardUids = append(dashboardUids, dashboardUid) + } + } + } + + if len(dashboardUids) == 0 { + return sharedDashboards, nil + } + + dashboardsQuery := &dashboards.GetDashboardsQuery{ + DashboardUIDs: dashboardUids, + OrgID: user.GetOrgID(), + } + sharedDashboards, err := dr.dashboardStore.GetDashboards(ctx, dashboardsQuery) + if err != nil { + return nil, err + } + return dr.filterUserSharedDashboards(ctx, user, sharedDashboards) +} + +// filterUserSharedDashboards filter dashboards directly assigned to user, but not located in folders with view permissions +func (dr *DashboardServiceImpl) filterUserSharedDashboards(ctx context.Context, user identity.Requester, userDashboards []*dashboards.Dashboard) ([]*dashboards.Dashboard, error) { + filteredDashboards := make([]*dashboards.Dashboard, 0) + + folderUIDs := make([]string, 0) + for _, dashboard := range userDashboards { + folderUIDs = append(folderUIDs, dashboard.FolderUID) + } + + dashFolders, err := dr.folderStore.GetFolders(ctx, user.GetOrgID(), folderUIDs) + if err != nil { + return nil, folder.ErrInternal.Errorf("failed to fetch parent folders from store: %w", err) + } + + for _, dashboard := range userDashboards { + // Filter out dashboards if user has access to parent folder + if dashboard.FolderUID == "" { + continue + } + + dashFolder, ok := dashFolders[dashboard.FolderUID] + if !ok { + dr.log.Error("failed to fetch folder by UID from store", "uid", dashboard.FolderUID) + continue + } + + g, err := guardian.NewByFolder(ctx, dashFolder, user.GetOrgID(), user) + if err != nil { + dr.log.Error("failed to check folder permissions", "folder uid", dashboard.FolderUID, "error", err) + continue + } + + canView, err := g.CanView() + if err != nil { + dr.log.Error("failed to fetch dashboard", "uid", dashboard.UID, "error", err) + continue + } + if !canView { + filteredDashboards = append(filteredDashboards, dashboard) + } + } + return filteredDashboards, nil +} + +func (dr *DashboardServiceImpl) getUserSharedDashboardUIDs(ctx context.Context, user identity.Requester) ([]string, error) { + userDashboards, err := dr.getDashboardsSharedWithUser(ctx, user) + if err != nil { + return nil, err + } + userDashboardUIDs := make([]string, 0) + for _, dashboard := range userDashboards { + userDashboardUIDs = append(userDashboardUIDs, dashboard.UID) + } + return userDashboardUIDs, nil +} + func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { + if dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && len(query.FolderUIDs) > 0 && slices.Contains(query.FolderUIDs, folder.SharedWithMeFolderUID) { + start := time.Now() + userDashboardUIDs, err := dr.getUserSharedDashboardUIDs(ctx, query.SignedInUser) + if err != nil { + dr.metrics.sharedWithMeFetchDashboardsRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds()) + return nil, err + } + if len(userDashboardUIDs) == 0 { + return []dashboards.DashboardSearchProjection{}, nil + } + query.DashboardUIDs = userDashboardUIDs + query.FolderUIDs = []string{} + + defer func(t time.Time) { + dr.metrics.sharedWithMeFetchDashboardsRequestsDuration.WithLabelValues("success").Observe(time.Since(start).Seconds()) + }(time.Now()) + } return dr.dashboardStore.FindDashboards(ctx, query) } diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index d9cf6d931f3..cc445995917 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -893,6 +893,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc dashboardPermissions, ac, foldertest.NewFakeService(), + nil, ) require.NoError(t, err) guardian.InitAccessControlGuardian(cfg, ac, dashboardService) @@ -961,6 +962,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt dashboardPermissions, actest.FakeAccessControl{}, foldertest.NewFakeService(), + nil, ) require.NoError(t, err) res, err := service.SaveDashboard(context.Background(), &dto, false) @@ -984,6 +986,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto accesscontrolmock.NewMockedPermissionsService(), actest.FakeAccessControl{}, foldertest.NewFakeService(), + nil, ) require.NoError(t, err) _, err = service.SaveDashboard(context.Background(), &dto, false) @@ -1027,6 +1030,7 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, folder dashboardPermissions, actest.FakeAccessControl{}, foldertest.NewFakeService(), + nil, ) require.NoError(t, err) res, err := service.SaveDashboard(context.Background(), &dto, false) @@ -1077,6 +1081,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da accesscontrolmock.NewMockedPermissionsService(), actest.FakeAccessControl{}, foldertest.NewFakeService(), + nil, ) require.NoError(t, err) res, err := service.SaveDashboard(context.Background(), &dto, false) diff --git a/pkg/services/dashboards/service/metrics.go b/pkg/services/dashboards/service/metrics.go new file mode 100644 index 00000000000..2fb45131a60 --- /dev/null +++ b/pkg/services/dashboards/service/metrics.go @@ -0,0 +1,30 @@ +package service + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const ( + metricsNamespace = "grafana" + metricsSubSystem = "dashboards" +) + +type dashboardsMetrics struct { + sharedWithMeFetchDashboardsRequestsDuration *prometheus.HistogramVec +} + +func newDashboardsMetrics(r prometheus.Registerer) *dashboardsMetrics { + return &dashboardsMetrics{ + sharedWithMeFetchDashboardsRequestsDuration: promauto.With(r).NewHistogramVec( + prometheus.HistogramOpts{ + Name: "sharedwithme_fetch_dashboards_duration_seconds", + Help: "Duration of fetching dashboards with permissions directly assigned to user", + Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + }, + []string{"status"}, + ), + } +} diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 95c6419b925..fc43492c0fc 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -6,7 +6,9 @@ import ( "fmt" "strings" "sync" + "time" + "github.com/prometheus/client_golang/prometheus" "golang.org/x/exp/slices" "github.com/grafana/grafana/pkg/bus" @@ -41,6 +43,7 @@ type Service struct { mutex sync.RWMutex registry map[string]folder.RegistryService + metrics *foldersMetrics } func ProvideService( @@ -51,6 +54,7 @@ func ProvideService( folderStore folder.FolderStore, db db.DB, // DB for the (new) nested folder store features featuremgmt.FeatureToggles, + r prometheus.Registerer, ) folder.Service { store := ProvideStore(db, cfg, features) srv := &Service{ @@ -64,6 +68,7 @@ func ProvideService( bus: bus, db: db, registry: make(map[string]folder.RegistryService), + metrics: newFoldersMetrics(r), } srv.DBMigration(db) @@ -115,6 +120,10 @@ func (s *Service) Get(ctx context.Context, cmd *folder.GetFolderQuery) (*folder. return nil, folder.ErrBadRequest.Errorf("missing signed in user") } + if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && cmd.UID != nil && *cmd.UID == folder.SharedWithMeFolderUID { + return folder.SharedWithMeFolder.WithURL(), nil + } + var dashFolder *folder.Folder var err error switch { @@ -185,6 +194,10 @@ func (s *Service) GetChildren(ctx context.Context, cmd *folder.GetChildrenQuery) return nil, folder.ErrBadRequest.Errorf("missing signed in user") } + if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && cmd.UID == folder.SharedWithMeFolderUID { + return s.GetSharedWithMe(ctx, cmd) + } + if cmd.UID != "" { g, err := guardian.NewByUID(ctx, cmd.UID, cmd.OrgID, cmd.SignedInUser) if err != nil { @@ -249,31 +262,40 @@ func (s *Service) GetChildren(ctx context.Context, cmd *folder.GetChildrenQuery) } } + if len(filtered) < len(children) { + // add "shared with me" folder + filtered = append(filtered, &folder.SharedWithMeFolder) + } + return filtered, nil } // GetSharedWithMe returns folders available to user, which cannot be accessed from the root folders func (s *Service) GetSharedWithMe(ctx context.Context, cmd *folder.GetChildrenQuery) ([]*folder.Folder, error) { + start := time.Now() availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, cmd.OrgID, cmd.SignedInUser) if err != nil { + s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds()) return nil, folder.ErrInternal.Errorf("failed to fetch subfolders to which the user has explicit access: %w", err) } rootFolders, err := s.GetChildren(ctx, &folder.GetChildrenQuery{UID: "", OrgID: cmd.OrgID, SignedInUser: cmd.SignedInUser}) if err != nil { + s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds()) return nil, folder.ErrInternal.Errorf("failed to fetch root folders to which the user has access: %w", err) } availableNonRootFolders = s.deduplicateAvailableFolders(ctx, availableNonRootFolders, rootFolders) + s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("success").Observe(time.Since(start).Seconds()) return availableNonRootFolders, nil } func (s *Service) getAvailableNonRootFolders(ctx context.Context, orgID int64, user identity.Requester) ([]*folder.Folder, error) { permissions := user.GetPermissions() - folderPermissions := permissions["folders:read"] - folderPermissions = append(folderPermissions, permissions["dashboards:read"]...) + folderPermissions := permissions[dashboards.ActionFoldersRead] + folderPermissions = append(folderPermissions, permissions[dashboards.ActionDashboardsRead]...) nonRootFolders := make([]*folder.Folder, 0) folderUids := make([]string, 0) for _, p := range folderPermissions { - if folderUid, found := strings.CutPrefix(p, "folders:uid:"); found { + if folderUid, found := strings.CutPrefix(p, dashboards.ScopeFoldersPrefix); found { if !slices.Contains(folderUids, folderUid) { folderUids = append(folderUids, folderUid) } @@ -335,6 +357,9 @@ func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]* if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { return nil, nil } + if q.UID == folder.SharedWithMeFolderUID { + return []*folder.Folder{&folder.SharedWithMeFolder}, nil + } return s.store.GetParents(ctx, q) } @@ -377,6 +402,10 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( dashFolder.FolderUID = cmd.ParentUID } + if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && cmd.UID == folder.SharedWithMeFolderUID { + return nil, folder.ErrBadRequest.Errorf("cannot create folder with UID %s", folder.SharedWithMeFolderUID) + } + trimmedUID := strings.TrimSpace(cmd.UID) if trimmedUID == accesscontrol.GeneralFolderUID { return nil, dashboards.ErrFolderInvalidUID diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 9f9d223b321..e0e68da3b6f 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -22,9 +22,11 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/alerting" + alertmodels "github.com/grafana/grafana/pkg/services/alerting/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" - "github.com/grafana/grafana/pkg/services/dashboards/service" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" @@ -54,7 +56,7 @@ func TestIntegrationProvideFolderService(t *testing.T) { cfg := setting.NewCfg() ac := acmock.New() db := sqlstore.InitTestDB(t) - ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, db, &featuremgmt.FeatureManager{}) + ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, db, &featuremgmt.FeatureManager{}, nil) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3) }) @@ -94,6 +96,7 @@ func TestIntegrationFolderService(t *testing.T) { bus: bus.ProvideBus(tracing.InitializeTracerForTest()), db: db, accessControl: acimpl.ProvideAccessControl(cfg), + metrics: newFoldersMetrics(nil), registry: make(map[string]folder.RegistryService), } @@ -391,6 +394,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { db: db, accessControl: ac, registry: make(map[string]folder.RegistryService), + metrics: newFoldersMetrics(nil), } signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ @@ -434,7 +438,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { CanEditValue: true, }) - dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac) @@ -502,6 +506,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { bus: b, db: db, registry: make(map[string]folder.RegistryService), + metrics: newFoldersMetrics(nil), } origNewGuardian := guardian.New @@ -512,8 +517,8 @@ func TestIntegrationNestedFolderService(t *testing.T) { CanEditValue: true, }) - dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff, - folderPermissions, dashboardPermissions, ac, serviceWithFlagOff) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff, + folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac) @@ -577,6 +582,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { bus: b, db: db, registry: make(map[string]folder.RegistryService), + metrics: newFoldersMetrics(nil), } testCases := []struct { @@ -655,7 +661,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { tc.service.dashboardStore = dashStore tc.service.store = nestedFolderStore - dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac) require.NoError(t, err) @@ -750,6 +756,7 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) { features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), log: log.New("test-folder-service"), accessControl: acimpl.ProvideAccessControl(cfg), + metrics: newFoldersMetrics(nil), } t.Run("create folder", func(t *testing.T) { nestedFolderStore.ExpectedFolder = &folder.Folder{ParentUID: util.GenerateShortUID()} @@ -1211,6 +1218,157 @@ func TestNestedFolderService(t *testing.T) { }) } +func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + db := sqlstore.InitTestDB(t) + quotaService := quotatest.New(false, nil) + folderStore := ProvideDashboardFolderStore(db) + + cfg := setting.NewCfg() + + featuresFlagOn := featuremgmt.WithFeatures("nestedFolders") + dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService) + require.NoError(t, err) + nestedFolderStore := ProvideStore(db, db.Cfg, featuresFlagOn) + + b := bus.ProvideBus(tracing.InitializeTracerForTest()) + ac := acimpl.ProvideAccessControl(cfg) + + serviceWithFlagOn := &Service{ + cfg: cfg, + log: log.New("test-folder-service"), + dashboardStore: dashStore, + dashboardFolderStore: folderStore, + store: nestedFolderStore, + features: featuresFlagOn, + bus: b, + db: db, + accessControl: ac, + registry: make(map[string]folder.RegistryService), + metrics: newFoldersMetrics(nil), + } + + dashboardPermissions := acmock.NewMockedPermissionsService() + dashboardService, err := dashboardservice.ProvideDashboardServiceImpl( + cfg, dashStore, folderStore, &dummyDashAlertExtractor{}, + featuresFlagOn, + acmock.NewMockedPermissionsService(), + dashboardPermissions, + actest.FakeAccessControl{}, + foldertest.NewFakeService(), + nil, + ) + require.NoError(t, err) + + signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ + orgID: { + dashboards.ActionFoldersRead: {}, + }, + }} + + signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ + orgID: { + dashboards.ActionFoldersCreate: {}, + dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, + }, + }} + + createCmd := folder.CreateFolderCommand{ + OrgID: orgID, + ParentUID: "", + SignedInUser: &signedInAdminUser, + } + + t.Run("Should get folders shared with given user", func(t *testing.T) { + depth := 3 + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + }) + + ancestorUIDsFolderWithPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withPermissions", createCmd) + ancestorUIDsFolderWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd) + + parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDsFolderWithoutPermissions[0]) + require.NoError(t, err) + subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDsFolderWithoutPermissions[1]) + require.NoError(t, err) + // nolint:staticcheck + dash1 := insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod") + // nolint:staticcheck + dash2 := insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, subfolder.UID, "prod") + + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + CanViewUIDs: []string{ + ancestorUIDsFolderWithPermissions[0], + ancestorUIDsFolderWithPermissions[1], + ancestorUIDsFolderWithoutPermissions[1], + dash1.UID, + dash2.UID, + }, + }) + signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{ + dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithPermissions[0]), + // Add permission to the subfolder of folder with permission (to check deduplication) + dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithPermissions[1]), + // Add permission to the subfolder of folder without permission + dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithoutPermissions[1]), + } + signedInUser.Permissions[orgID][dashboards.ActionDashboardsRead] = []string{ + dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash1.UID), + dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash2.UID), + } + + getSharedCmd := folder.GetChildrenQuery{ + UID: folder.SharedWithMeFolderUID, + OrgID: orgID, + SignedInUser: &signedInUser, + } + + sharedFolders, err := serviceWithFlagOn.GetChildren(context.Background(), &getSharedCmd) + sharedFoldersUIDs := make([]string, 0) + for _, f := range sharedFolders { + sharedFoldersUIDs = append(sharedFoldersUIDs, f.UID) + } + + require.NoError(t, err) + require.Len(t, sharedFolders, 1) + require.Contains(t, sharedFoldersUIDs, ancestorUIDsFolderWithoutPermissions[1]) + require.NotContains(t, sharedFoldersUIDs, ancestorUIDsFolderWithPermissions[1]) + + sharedDashboards, err := dashboardService.GetDashboardsSharedWithUser(context.Background(), &signedInUser) + sharedDashboardsUIDs := make([]string, 0) + for _, d := range sharedDashboards { + sharedDashboardsUIDs = append(sharedDashboardsUIDs, d.UID) + } + + require.NoError(t, err) + require.Len(t, sharedDashboards, 1) + require.Contains(t, sharedDashboardsUIDs, dash1.UID) + require.NotContains(t, sharedDashboardsUIDs, dash2.UID) + + t.Cleanup(func() { + guardian.New = origNewGuardian + for _, uid := range ancestorUIDsFolderWithPermissions { + err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID) + assert.NoError(t, err) + } + }) + t.Cleanup(func() { + guardian.New = origNewGuardian + for _, uid := range ancestorUIDsFolderWithoutPermissions { + err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID) + assert.NoError(t, err) + } + }) + }) +} + func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []string { t.Helper() @@ -1263,6 +1421,7 @@ func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder features: features, accessControl: ac, db: db, + metrics: newFoldersMetrics(nil), } } @@ -1287,3 +1446,14 @@ func createRule(t *testing.T, store *ngstore.DBstore, folderUID, title string) * return &rule } + +type dummyDashAlertExtractor struct { +} + +func (d *dummyDashAlertExtractor) GetAlerts(ctx context.Context, dashAlertInfo alerting.DashAlertInfo) ([]*alertmodels.Alert, error) { + return nil, nil +} + +func (d *dummyDashAlertExtractor) ValidateAlerts(ctx context.Context, dashAlertInfo alerting.DashAlertInfo) error { + return nil +} diff --git a/pkg/services/folder/folderimpl/metrics.go b/pkg/services/folder/folderimpl/metrics.go new file mode 100644 index 00000000000..7e569e84ed8 --- /dev/null +++ b/pkg/services/folder/folderimpl/metrics.go @@ -0,0 +1,30 @@ +package folderimpl + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const ( + metricsNamespace = "grafana" + metricsSubSystem = "folders" +) + +type foldersMetrics struct { + sharedWithMeFetchFoldersRequestsDuration *prometheus.HistogramVec +} + +func newFoldersMetrics(r prometheus.Registerer) *foldersMetrics { + return &foldersMetrics{ + sharedWithMeFetchFoldersRequestsDuration: promauto.With(r).NewHistogramVec( + prometheus.HistogramOpts{ + Name: "sharedwithme_fetch_folders_duration_seconds", + Help: "Duration of fetching folders with permissions directly assigned to user", + Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + }, + []string{"status"}, + ), + } +} diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 70e82c5f7c8..e43c5ac662c 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -19,9 +19,10 @@ var ErrTargetRegistrySrvConflict = errutil.Internal("folder.target-registry-srv- var ErrFolderNotEmpty = errutil.BadRequest("folder.not-empty", errutil.WithPublicMessage("Folder cannot be deleted: folder is not empty")) const ( - GeneralFolderUID = "general" - RootFolderUID = "" - MaxNestedFolderDepth = 4 + GeneralFolderUID = "general" + RootFolderUID = "" + MaxNestedFolderDepth = 4 + SharedWithMeFolderUID = "sharedwithme" ) var ErrFolderNotFound = errutil.NotFound("folder.notFound") @@ -49,6 +50,14 @@ type Folder struct { var GeneralFolder = Folder{ID: 0, Title: "General"} +var SharedWithMeFolder = Folder{ + Title: "Shared with me", + Description: "Dashboards and folders shared with me", + UID: SharedWithMeFolderUID, + ParentUID: "", + ID: -1, +} + func (f *Folder) IsGeneral() bool { // nolint:staticcheck return f.ID == GeneralFolder.ID && f.Title == GeneralFolder.Title diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index dd0638af69a..992c4703940 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -2,6 +2,7 @@ package guardian import ( "context" + "slices" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards" @@ -59,6 +60,7 @@ type FakeDashboardGuardian struct { CanEditValue bool CanViewValue bool CanAdminValue bool + CanViewUIDs []string } func (g *FakeDashboardGuardian) CanSave() (bool, error) { @@ -70,6 +72,9 @@ func (g *FakeDashboardGuardian) CanEdit() (bool, error) { } func (g *FakeDashboardGuardian) CanView() (bool, error) { + if g.CanViewUIDs != nil { + return slices.Contains(g.CanViewUIDs, g.DashUID), nil + } return g.CanViewValue, nil } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 1dfe489cee8..a2078ce200d 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -302,6 +302,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash cfg, dashboardStore, folderStore, dashAlertExtractor, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), + nil, ) require.NoError(t, err) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) @@ -321,7 +322,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sc.sqlStore, features) + s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sc.sqlStore, features, nil) t.Logf("Creating folder with title and UID %q", title) ctx := appcontext.WithUser(context.Background(), &sc.user) folder, err := s.Create(ctx, &folder.CreateFolderCommand{ @@ -385,6 +386,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena sqlStore.Cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), + nil, ) require.NoError(t, svcErr) guardian.InitAccessControlGuardian(sqlStore.Cfg, ac, dashboardService) @@ -445,6 +447,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo sqlStore.Cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), + nil, ) require.NoError(t, dashSvcErr) guardian.InitAccessControlGuardian(sqlStore.Cfg, ac, dashService) @@ -452,7 +455,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Cfg: sqlStore.Cfg, features: featuremgmt.WithFeatures(), SQLStore: sqlStore, - folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features), + folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, nil), } // 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 1c012a2c477..42ed6ec6273 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -734,6 +734,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash cfg, dashboardStore, folderStore, dashAlertService, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, foldertest.NewFakeService(), + nil, ) require.NoError(t, err) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) @@ -752,7 +753,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sc.sqlStore, features) + s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sc.sqlStore, features, nil) t.Logf("Creating folder with title and UID %q", title) ctx := appcontext.WithUser(context.Background(), sc.user) @@ -826,6 +827,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo setting.NewCfg(), dashStore, folderStore, dashAlertService, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, foldertest.NewFakeService(), + nil, ) require.NoError(t, err) guardian.InitAccessControlGuardian(setting.NewCfg(), ac, dashService) @@ -833,7 +835,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) features := featuremgmt.WithFeatures() - folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sqlStore, features) + folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sqlStore, features, nil) elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures(), ac) service := LibraryPanelService{ diff --git a/pkg/services/ngalert/migration/store/testing.go b/pkg/services/ngalert/migration/store/testing.go index 67d0a747c47..40fa3a7ac2b 100644 --- a/pkg/services/ngalert/migration/store/testing.go +++ b/pkg/services/ngalert/migration/store/testing.go @@ -67,7 +67,7 @@ func NewTestMigrationStore(t testing.TB, sqlStore *sqlstore.SQLStore, cfg *setti dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, sqlStore, features) + folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, sqlStore, features, nil) err = folderService.RegisterService(alertingStore) require.NoError(t, err) @@ -83,6 +83,7 @@ func NewTestMigrationStore(t testing.TB, sqlStore *sqlstore.SQLStore, cfg *setti cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, folderService, + nil, ) require.NoError(t, err) guardian.InitAccessControlGuardian(setting.NewCfg(), ac, dashboardService) diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index 4f4caea9941..b0957e08501 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -30,7 +30,7 @@ func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStor ac := acmock.New() features := featuremgmt.WithFeatures() - return folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, db, features) + return folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, db, features, nil) } func SetupDashboardService(tb testing.TB, sqlStore *sqlstore.SQLStore, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) { @@ -61,6 +61,7 @@ func SetupDashboardService(tb testing.TB, sqlStore *sqlstore.SQLStore, fs *folde cfg, dashboardStore, fs, nil, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), + nil, ) require.NoError(tb, err) diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index a7385bc119e..e6636b59e23 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -843,7 +843,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol dashStore, err := database.ProvideDashboardStore(db, db.Cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil)) require.NoError(t, err) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features, nil) // create parent folder parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go index 42b78918e7e..dd53cbf51b1 100644 --- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go +++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go @@ -83,7 +83,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe dashboardWriteStore, err := database.ProvideDashboardStore(store, store.Cfg, features, tagimpl.ProvideService(store), quotaService) require.NoError(b, err) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), store.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), store.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features, nil) origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})