diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 5f9748e673e..5c32a3f6f89 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -40,6 +40,7 @@ import ( dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/login" @@ -392,6 +393,8 @@ func setupHTTPServerWithCfgDb( folderPermissionsService := accesscontrolmock.NewMockedPermissionsService() dashboardPermissionsService := accesscontrolmock.NewMockedPermissionsService() + folderSvc := foldertest.NewFakeService() + // Create minimal HTTP Server hs := &HTTPServer{ Cfg: cfg, @@ -408,6 +411,7 @@ func setupHTTPServerWithCfgDb( DashboardService: dashboardservice.ProvideDashboardService( cfg, dashboardsStore, dashboardsStore, nil, features, folderPermissionsService, dashboardPermissionsService, ac, + folderSvc, ), preferenceService: preftest.NewPreferenceServiceFake(), userService: userSvc, diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 1b07b5937bf..965dacc464a 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -12,12 +12,15 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/dashboards" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" @@ -37,12 +40,14 @@ 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) hs := &HTTPServer{ Cfg: settings, SQLStore: mockSQLStore, Features: features, DashboardService: dashboardservice.ProvideDashboardService( settings, dashboardStore, dashboards.NewFakeFolderStore(t), nil, features, folderPermissions, dashboardPermissions, ac, + folderSvc, ), AccessControl: accesscontrolmock.New().WithDisabled(), } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index f141d773a64..e9cb683e200 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -16,9 +16,11 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -35,6 +37,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboardversion/dashvertest" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements" @@ -980,10 +983,13 @@ 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) + if dashboardService == nil { dashboardService = service.ProvideDashboardService( cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, + folderSvc, ) } @@ -997,6 +1003,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr dashboardProvisioningService: service.ProvideDashboardService( cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, + folderSvc, ), DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 56ae1bfa2a6..57ce79e2cf4 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -46,6 +46,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { dashboardPermissionsService: dashboardPermissions, DashboardService: service.ProvideDashboardService( settings, dashboardStore, dashboards.NewFakeFolderStore(t), nil, features, folderPermissions, dashboardPermissions, ac, + folderService, ), AccessControl: accesscontrolmock.New().WithDisabled(), } diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index cd0bf9333d2..3c401ada3e1 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -5,6 +5,7 @@ import ( "strings" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/folder" ) const ( @@ -38,7 +39,7 @@ var ( ) // NewFolderNameScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope. -func NewFolderNameScopeResolver(db Store, folderDB FolderStore) (string, ac.ScopeAttributeResolver) { +func NewFolderNameScopeResolver(db Store, folderDB FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { prefix := ScopeFoldersProvider.GetResourceScopeName("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { if !strings.HasPrefix(scope, prefix) { @@ -52,12 +53,19 @@ func NewFolderNameScopeResolver(db Store, folderDB FolderStore) (string, ac.Scop if err != nil { return nil, err } - return []string{ScopeFoldersProvider.GetResourceScopeUID(folder.UID)}, nil + + result, err := getInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + if err != nil { + return nil, err + } + + result = append([]string{ScopeFoldersProvider.GetResourceScopeUID(folder.UID)}, result...) + return result, nil }) } // NewFolderIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:id:" into an uid based scope. -func NewFolderIDScopeResolver(db Store, folderDB FolderStore) (string, ac.ScopeAttributeResolver) { +func NewFolderIDScopeResolver(db Store, folderDB FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { prefix := ScopeFoldersProvider.GetResourceScope("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { if !strings.HasPrefix(scope, prefix) { @@ -78,13 +86,19 @@ func NewFolderIDScopeResolver(db Store, folderDB FolderStore) (string, ac.ScopeA return nil, err } - return []string{ScopeFoldersProvider.GetResourceScopeUID(folder.UID)}, nil + result, err := getInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + if err != nil { + return nil, err + } + + result = append([]string{ScopeFoldersProvider.GetResourceScopeUID(folder.UID)}, result...) + return result, nil }) } // NewDashboardIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:id:" // into uid based scopes for both dashboard and folder -func NewDashboardIDScopeResolver(db Store, folderDB FolderStore) (string, ac.ScopeAttributeResolver) { +func NewDashboardIDScopeResolver(db Store, folderDB FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { prefix := ScopeDashboardsProvider.GetResourceScope("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { if !strings.HasPrefix(scope, prefix) { @@ -101,13 +115,13 @@ func NewDashboardIDScopeResolver(db Store, folderDB FolderStore) (string, ac.Sco return nil, err } - return resolveDashboardScope(ctx, db, folderDB, orgID, dashboard) + return resolveDashboardScope(ctx, db, folderDB, orgID, dashboard, folderSvc) }) } // NewDashboardUIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:uid:" // into uid based scopes for both dashboard and folder -func NewDashboardUIDScopeResolver(db Store, folderDB FolderStore) (string, ac.ScopeAttributeResolver) { +func NewDashboardUIDScopeResolver(db Store, folderDB FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { prefix := ScopeDashboardsProvider.GetResourceScopeUID("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { if !strings.HasPrefix(scope, prefix) { @@ -124,11 +138,11 @@ func NewDashboardUIDScopeResolver(db Store, folderDB FolderStore) (string, ac.Sc return nil, err } - return resolveDashboardScope(ctx, db, folderDB, orgID, dashboard) + return resolveDashboardScope(ctx, db, folderDB, orgID, dashboard, folderSvc) }) } -func resolveDashboardScope(ctx context.Context, db Store, folderDB FolderStore, orgID int64, dashboard *Dashboard) ([]string, error) { +func resolveDashboardScope(ctx context.Context, db Store, folderDB FolderStore, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) { var folderUID string if dashboard.FolderID < 0 { return []string{ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)}, nil @@ -144,8 +158,36 @@ func resolveDashboardScope(ctx context.Context, db Store, folderDB FolderStore, folderUID = folder.UID } - return []string{ + result, err := getInheritedScopes(ctx, orgID, folderUID, folderSvc) + if err != nil { + return nil, err + } + + result = append([]string{ ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID), ScopeFoldersProvider.GetResourceScopeUID(folderUID), - }, nil + }, + result..., + ) + + return result, nil +} + +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 + } + + result := make([]string, 0, len(ancestors)) + for _, ff := range ancestors { + result = append(result, ScopeFoldersProvider.GetResourceScopeUID(ff.UID)) + } + + return result, nil } diff --git a/pkg/services/dashboards/accesscontrol_test.go b/pkg/services/dashboards/accesscontrol_test.go index ae7f319fc98..19e78f48c81 100644 --- a/pkg/services/dashboards/accesscontrol_test.go +++ b/pkg/services/dashboards/accesscontrol_test.go @@ -7,17 +7,19 @@ import ( "strconv" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/util" ) func TestNewFolderNameScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewFolderNameScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t)) + prefix, _ := NewFolderNameScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t), foldertest.NewFakeService()) require.Equal(t, "folders:name:", prefix) }) @@ -33,7 +35,7 @@ func TestNewFolderNameScopeResolver(t *testing.T) { scope := "folders:name:" + title - _, resolver := NewFolderNameScopeResolver(dashboardStore, folderStore) + _, resolver := NewFolderNameScopeResolver(dashboardStore, folderStore, foldertest.NewFakeService()) resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope) require.NoError(t, err) @@ -43,16 +45,53 @@ func TestNewFolderNameScopeResolver(t *testing.T) { folderStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title) }) + t.Run("resolver should include inherited scopes if any", func(t *testing.T) { + dashboardStore := &FakeDashboardStore{} + orgId := rand.Int63() + title := "Very complex :title with: and /" + util.GenerateShortUID() + + db := &folder.Folder{Title: title, ID: rand.Int63(), UID: util.GenerateShortUID()} + + folderStore := NewFakeFolderStore(t) + folderStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything).Return(db, nil).Once() + + scope := "folders:name:" + title + + folderSvc := foldertest.NewFakeService() + folderSvc.ExpectedFolders = []*folder.Folder{ + { + UID: "parent", + }, + { + UID: "grandparent", + }, + } + _, resolver := NewFolderNameScopeResolver(dashboardStore, folderStore, folderSvc) + + resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope) + require.NoError(t, err) + require.Len(t, resolvedScopes, 3) + + if diff := cmp.Diff([]string{ + fmt.Sprintf("folders:uid:%v", db.UID), + "folders:uid:parent", + "folders:uid:grandparent", + }, resolvedScopes); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + + folderStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title) + }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { dashboardStore := &FakeDashboardStore{} - _, resolver := NewFolderNameScopeResolver(dashboardStore, NewFakeFolderStore(t)) + _, resolver := NewFolderNameScopeResolver(dashboardStore, NewFakeFolderStore(t), foldertest.NewFakeService()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) { dashboardStore := &FakeDashboardStore{} - _, resolver := NewFolderNameScopeResolver(dashboardStore, NewFakeFolderStore(t)) + _, resolver := NewFolderNameScopeResolver(dashboardStore, NewFakeFolderStore(t), foldertest.NewFakeService()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:name:") require.ErrorIs(t, err, ac.ErrInvalidScope) @@ -61,7 +100,7 @@ func TestNewFolderNameScopeResolver(t *testing.T) { dashboardStore := &FakeDashboardStore{} folderStore := NewFakeFolderStore(t) - _, resolver := NewFolderNameScopeResolver(dashboardStore, folderStore) + _, resolver := NewFolderNameScopeResolver(dashboardStore, folderStore, foldertest.NewFakeService()) orgId := rand.Int63() folderStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrDashboardNotFound).Once() @@ -76,7 +115,7 @@ func TestNewFolderNameScopeResolver(t *testing.T) { func TestNewFolderIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewFolderIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t)) + prefix, _ := NewFolderIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t), foldertest.NewFakeService()) require.Equal(t, "folders:id:", prefix) }) @@ -84,7 +123,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { dashboardStore := &FakeDashboardStore{} folderStore := NewFakeFolderStore(t) - _, resolver := NewFolderIDScopeResolver(dashboardStore, folderStore) + _, resolver := NewFolderIDScopeResolver(dashboardStore, folderStore, foldertest.NewFakeService()) orgId := rand.Int63() uid := util.GenerateShortUID() @@ -101,9 +140,46 @@ func TestNewFolderIDScopeResolver(t *testing.T) { folderStore.AssertCalled(t, "GetFolderByID", mock.Anything, orgId, db.ID) }) + t.Run("resolver should should include inherited scopes if any", func(t *testing.T) { + dashboardStore := &FakeDashboardStore{} + folderStore := NewFakeFolderStore(t) + + folderSvc := foldertest.NewFakeService() + folderSvc.ExpectedFolders = []*folder.Folder{ + { + UID: "parent", + }, + { + UID: "grandparent", + }, + } + _, resolver := NewFolderIDScopeResolver(dashboardStore, folderStore, folderSvc) + + orgId := rand.Int63() + uid := util.GenerateShortUID() + + db := &folder.Folder{ID: rand.Int63(), UID: uid} + folderStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(db, nil).Once() + + scope := "folders:id:" + strconv.FormatInt(db.ID, 10) + + resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope) + require.NoError(t, err) + require.Len(t, resolvedScopes, 3) + + if diff := cmp.Diff([]string{ + fmt.Sprintf("folders:uid:%v", db.UID), + "folders:uid:parent", + "folders:uid:grandparent", + }, resolvedScopes); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + + folderStore.AssertCalled(t, "GetFolderByID", mock.Anything, orgId, db.ID) + }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { dashboardStore := &FakeDashboardStore{} - _, resolver := NewFolderIDScopeResolver(dashboardStore, NewFakeFolderStore(t)) + _, resolver := NewFolderIDScopeResolver(dashboardStore, NewFakeFolderStore(t), foldertest.NewFakeService()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:uid:123") require.ErrorIs(t, err, ac.ErrInvalidScope) @@ -114,7 +190,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { dashboardStore = &FakeDashboardStore{} orgId = rand.Int63() scope = "folders:id:0" - _, resolver = NewFolderIDScopeResolver(dashboardStore, NewFakeFolderStore(t)) + _, resolver = NewFolderIDScopeResolver(dashboardStore, NewFakeFolderStore(t), foldertest.NewFakeService()) ) resolved, err := resolver.Resolve(context.Background(), orgId, scope) @@ -126,7 +202,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) { dashboardStore := &FakeDashboardStore{} - _, resolver := NewFolderIDScopeResolver(dashboardStore, NewFakeFolderStore(t)) + _, resolver := NewFolderIDScopeResolver(dashboardStore, NewFakeFolderStore(t), foldertest.NewFakeService()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:") require.ErrorIs(t, err, ac.ErrInvalidScope) @@ -135,7 +211,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { dashboardStore := &FakeDashboardStore{} folderStore := NewFakeFolderStore(t) - _, resolver := NewFolderIDScopeResolver(dashboardStore, folderStore) + _, resolver := NewFolderIDScopeResolver(dashboardStore, folderStore, foldertest.NewFakeService()) orgId := rand.Int63() folderStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrDashboardNotFound).Once() @@ -149,7 +225,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { func TestNewDashboardIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewDashboardIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t)) + prefix, _ := NewDashboardIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t), foldertest.NewFakeService()) require.Equal(t, "dashboards:id:", prefix) }) @@ -157,7 +233,7 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { store := &FakeDashboardStore{} folderStore := NewFakeFolderStore(t) - _, resolver := NewDashboardIDScopeResolver(store, folderStore) + _, resolver := NewDashboardIDScopeResolver(store, folderStore, foldertest.NewFakeService()) orgID := rand.Int63() folder := &folder.Folder{ID: 2, UID: "2"} @@ -174,15 +250,52 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { require.Equal(t, fmt.Sprintf("folders:uid:%s", folder.UID), resolvedScopes[1]) }) + t.Run("resolver should inlude inherited scopes if any", func(t *testing.T) { + store := &FakeDashboardStore{} + folderStore := NewFakeFolderStore(t) + + folderSvc := foldertest.NewFakeService() + folderSvc.ExpectedFolders = []*folder.Folder{ + { + UID: "parent", + }, + { + UID: "grandparent", + }, + } + _, resolver := NewDashboardIDScopeResolver(store, folderStore, folderSvc) + + orgID := rand.Int63() + folder := &folder.Folder{ID: 2, UID: "2"} + dashboard := &Dashboard{ID: 1, FolderID: folder.ID, UID: "1"} + + store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil).Once() + folderStore.On("GetFolderByID", mock.Anything, orgID, folder.ID).Return(folder, nil).Once() + + scope := ac.Scope("dashboards", "id", strconv.FormatInt(dashboard.ID, 10)) + resolvedScopes, err := resolver.Resolve(context.Background(), orgID, scope) + require.NoError(t, err) + require.Len(t, resolvedScopes, 4) + + if diff := cmp.Diff([]string{ + fmt.Sprintf("dashboards:uid:%s", dashboard.UID), + fmt.Sprintf("folders:uid:%s", folder.UID), + "folders:uid:parent", + "folders:uid:grandparent", + }, resolvedScopes); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + }) + t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewDashboardIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t)) + _, resolver := NewDashboardIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t), foldertest.NewFakeService()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:uid:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) t.Run("resolver should convert folderID 0 to general uid scope for the folder scope", func(t *testing.T) { store := &FakeDashboardStore{} - _, resolver := NewDashboardIDScopeResolver(store, NewFakeFolderStore(t)) + _, resolver := NewDashboardIDScopeResolver(store, NewFakeFolderStore(t), foldertest.NewFakeService()) dashboard := &Dashboard{ID: 1, FolderID: 0, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil) @@ -197,14 +310,14 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { func TestNewDashboardUIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewDashboardUIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t)) + prefix, _ := NewDashboardUIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t), foldertest.NewFakeService()) require.Equal(t, "dashboards:uid:", prefix) }) t.Run("resolver should convert to uid dashboard and folder scope", func(t *testing.T) { store := &FakeDashboardStore{} folderStore := NewFakeFolderStore(t) - _, resolver := NewDashboardUIDScopeResolver(store, folderStore) + _, resolver := NewDashboardUIDScopeResolver(store, folderStore, foldertest.NewFakeService()) orgID := rand.Int63() folder := &folder.Folder{ID: 2, UID: "2"} @@ -221,15 +334,53 @@ func TestNewDashboardUIDScopeResolver(t *testing.T) { require.Equal(t, fmt.Sprintf("folders:uid:%s", folder.UID), resolvedScopes[1]) }) + t.Run("resolver should include inherited scopes if any", func(t *testing.T) { + store := &FakeDashboardStore{} + folderStore := NewFakeFolderStore(t) + + folderSvc := foldertest.NewFakeService() + folderSvc.ExpectedFolders = []*folder.Folder{ + { + UID: "parent", + }, + { + UID: "grandparent", + }, + } + + _, resolver := NewDashboardUIDScopeResolver(store, folderStore, folderSvc) + + orgID := rand.Int63() + folder := &folder.Folder{ID: 2, UID: "2"} + dashboard := &Dashboard{ID: 1, FolderID: folder.ID, UID: "1"} + + store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil).Once() + folderStore.On("GetFolderByID", mock.Anything, orgID, folder.ID).Return(folder, nil).Once() + + scope := ac.Scope("dashboards", "uid", dashboard.UID) + resolvedScopes, err := resolver.Resolve(context.Background(), orgID, scope) + require.NoError(t, err) + require.Len(t, resolvedScopes, 4) + + if diff := cmp.Diff([]string{ + fmt.Sprintf("dashboards:uid:%s", dashboard.UID), + fmt.Sprintf("folders:uid:%s", folder.UID), + "folders:uid:parent", + "folders:uid:grandparent", + }, resolvedScopes); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + }) + t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewDashboardUIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t)) + _, resolver := NewDashboardUIDScopeResolver(&FakeDashboardStore{}, NewFakeFolderStore(t), foldertest.NewFakeService()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:id:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) t.Run("resolver should convert folderID 0 to general uid scope for the folder scope", func(t *testing.T) { store := &FakeDashboardStore{} - _, resolver := NewDashboardUIDScopeResolver(store, NewFakeFolderStore(t)) + _, resolver := NewDashboardUIDScopeResolver(store, NewFakeFolderStore(t), foldertest.NewFakeService()) dashboard := &Dashboard{ID: 1, FolderID: 0, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index c5fc9833189..e234cb093b1 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -53,9 +53,10 @@ func ProvideDashboardService( cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore dashboards.FolderStore, dashAlertExtractor alerting.DashAlertExtractor, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl, + folderSvc folder.Service, ) *DashboardServiceImpl { - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashboardStore, folderStore)) - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(dashboardStore, folderStore)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashboardStore, folderStore, folderSvc)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(dashboardStore, folderStore, folderSvc)) return &DashboardServiceImpl{ cfg: cfg, diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 03264673f95..5a3c59233c0 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -831,6 +832,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.New(), + foldertest.NewFakeService(), ) guardian.InitLegacyGuardian(sqlStore, service, &teamtest.FakeService{}) @@ -890,6 +892,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.New(), + foldertest.NewFakeService(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) @@ -911,6 +914,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.New(), + foldertest.NewFakeService(), ) _, err = service.SaveDashboard(context.Background(), &dto, false) return err @@ -950,6 +954,7 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.New(), + foldertest.NewFakeService(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) @@ -990,6 +995,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.New(), + foldertest.NewFakeService(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 65bb786db1a..9254fcfb753 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -51,8 +51,6 @@ func ProvideService( features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, ) folder.Service { - ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(dashboardStore, folderStore)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(dashboardStore, folderStore)) store := ProvideStore(db, cfg, features) svr := &Service{ cfg: cfg, @@ -68,6 +66,9 @@ func ProvideService( if features.IsEnabled(featuremgmt.FlagNestedFolders) { svr.DBMigration(db) } + + ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(dashboardStore, folderStore, svr)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(dashboardStore, folderStore, svr)) return svr } @@ -185,6 +186,13 @@ func (s *Service) GetChildren(ctx context.Context, cmd *folder.GetChildrenQuery) return filtered, nil } +func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { + if !s.features.IsEnabled(featuremgmt.FlagNestedFolders) { + return nil, nil + } + return s.store.GetParents(ctx, q) +} + func (s *Service) getFolderByID(ctx context.Context, user *user.SignedInUser, id int64, orgID int64) (*folder.Folder, error) { if id == 0 { return &folder.Folder{ID: id, Title: "General"}, nil diff --git a/pkg/services/folder/foldertest/foldertest.go b/pkg/services/folder/foldertest/foldertest.go index e5606d87a09..c83be22bec0 100644 --- a/pkg/services/folder/foldertest/foldertest.go +++ b/pkg/services/folder/foldertest/foldertest.go @@ -21,6 +21,11 @@ var _ folder.Service = (*FakeService)(nil) func (s *FakeService) GetChildren(ctx context.Context, cmd *folder.GetChildrenQuery) ([]*folder.Folder, error) { return s.ExpectedFolders, s.ExpectedError } + +func (s *FakeService) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { + return s.ExpectedFolders, s.ExpectedError +} + func (s *FakeService) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) { return s.ExpectedFolder, s.ExpectedError } diff --git a/pkg/services/folder/service.go b/pkg/services/folder/service.go index 0a06cbb6252..f981ea8a946 100644 --- a/pkg/services/folder/service.go +++ b/pkg/services/folder/service.go @@ -7,6 +7,9 @@ import ( type Service interface { // GetChildren returns an array containing all child folders. GetChildren(ctx context.Context, cmd *GetChildrenQuery) ([]*Folder, error) + // GetParents returns an array containing add parent folders if nested folders are enabled + // otherwise it returns an empty array + GetParents(ctx context.Context, q GetParentsQuery) ([]*Folder, error) Create(ctx context.Context, cmd *CreateFolderCommand) (*Folder, error) // GetFolder takes a GetFolderCommand and returns a folder matching the diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 748164cbe04..b457977a42a 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" dashdb "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -601,7 +602,7 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce }) require.NoError(t, err) ac := accesscontrolmock.New().WithPermissions(permissions) - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(dashStore, dashStore)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(dashStore, dashStore, foldertest.NewFakeService())) license := licensingtest.NewFakeLicensing() license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() teamSvc := teamimpl.ProvideService(store, store.Cfg) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index c3d47e8d66f..0ad7ae45523 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -293,6 +294,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash service := dashboardservice.ProvideDashboardService( cfg, dashboardStore, dashboardStore, dashAlertExtractor, features, folderPermissions, dashboardPermissions, ac, + foldertest.NewFakeService(), ) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) @@ -437,6 +439,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo dashboardService := dashboardservice.ProvideDashboardService( sqlStore.Cfg, dashboardStore, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac, + foldertest.NewFakeService(), ) guardian.InitLegacyGuardian(sqlStore, dashboardService, &teamtest.FakeService{}) service := LibraryElementService{ diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 9a3efd9c2db..3671fa717bf 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/org" @@ -706,6 +707,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash service := dashboardservice.ProvideDashboardService( cfg, dashboardStore, dashboardStore, dashAlertService, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), acmock.NewMockedPermissionsService(), ac, + foldertest.NewFakeService(), ) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 28c7a55e4e0..2d07d32d163 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -81,6 +82,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, dashboardService := dashboardservice.ProvideDashboardService( cfg, dashboardStore, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac, + foldertest.NewFakeService(), ) tracer := tracing.InitializeTracerForTest()