From 2a5547e1b5db132fe42cab3e4af7d614aeb1f987 Mon Sep 17 00:00:00 2001 From: Ieva Date: Fri, 17 Nov 2023 09:57:25 +0000 Subject: [PATCH] Annotations: Update annotation scope resolver to resolve annotation scopes to dash and folder scopes (#78222) * update annotation scope resolver to resolve dashboard annotation scopes to dash and folder scopes * Update annotations.go remove unwanted changes * remove unwanted change * use switch statement --- pkg/api/annotations.go | 46 +++++++++++++++++-- pkg/api/annotations_test.go | 88 ++++++++++++++++++++++++++++--------- pkg/api/http_server.go | 2 +- 3 files changed, 111 insertions(+), 25 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 4247f15a48a..31065552cec 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -14,6 +14,8 @@ import ( "github.com/grafana/grafana/pkg/services/auth/identity" 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/user" "github.com/grafana/grafana/pkg/util" @@ -578,7 +580,9 @@ func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Res // AnnotationTypeScopeResolver provides an ScopeAttributeResolver able to // resolve annotation types. Scope "annotations:id:" will be translated to "annotations:type:, // where is the type of annotation with id . -func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository) (string, accesscontrol.ScopeAttributeResolver) { +// If annotationPermissionUpdate feature toggle is enabled, dashboard annotation scope will be resolved to the corresponding +// dashboard and folder scopes (eg, "dashboards:uid:", "folders:uid:" etc). +func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features *featuremgmt.FeatureManager, dashSvc dashboards.DashboardService, folderSvc folder.Service) (string, accesscontrol.ScopeAttributeResolver) { prefix := accesscontrol.ScopeAnnotationsProvider.GetResourceScope("") return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) { scopeParts := strings.Split(initialScope, ":") @@ -604,15 +608,51 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository) (string }, } + if features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { + tempUser = &user.SignedInUser{ + OrgID: orgID, + Permissions: map[int64]map[string][]string{ + orgID: { + accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeOrganization, dashboards.ScopeDashboardsAll}, + }, + }, + } + } + annotation, resp := findAnnotationByID(ctx, annotationsRepo, int64(annotationId), tempUser) if resp != nil { return nil, errors.New("could not resolve annotation type") } - if annotation.GetType() == annotations.Organization { + if !features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { + switch annotation.GetType() { + case annotations.Organization: + return []string{accesscontrol.ScopeAnnotationsTypeOrganization}, nil + case annotations.Dashboard: + return []string{accesscontrol.ScopeAnnotationsTypeDashboard}, nil + } + } + + if annotation.DashboardID == 0 { return []string{accesscontrol.ScopeAnnotationsTypeOrganization}, nil } else { - return []string{accesscontrol.ScopeAnnotationsTypeDashboard}, nil + dashboard, err := dashSvc.GetDashboard(ctx, &dashboards.GetDashboardQuery{ID: annotation.DashboardID, OrgID: orgID}) + if err != nil { + return nil, err + } + scopes := []string{dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)} + // Append dashboard parent scopes if dashboard is in a folder or the general scope if dashboard is not in a folder + if dashboard.FolderUID != "" { + scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID)) + inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc) + if err != nil { + return nil, err + } + scopes = append(scopes, inheritedScopes...) + } else { + scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)) + } + return scopes, nil } }) } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 16695404cda..e1057f7858a 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -14,6 +14,9 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/dashboards" + "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/setting" "github.com/grafana/grafana/pkg/web/webtest" @@ -231,7 +234,10 @@ func TestAPI_Annotations(t *testing.T) { _ = repo.Save(context.Background(), &annotations.Item{ID: 2, DashboardID: 1}) hs.annotationsRepo = repo hs.AccessControl = acimpl.ProvideAccessControl(hs.Cfg) - hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo)) + features := featuremgmt.WithFeatures() + dashSvc := &dashboards.FakeDashboardService{} + folderSvc := &foldertest.FakeService{} + hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, features, dashSvc, folderSvc)) }) var body io.Reader if tt.body != "" { @@ -247,60 +253,100 @@ func TestAPI_Annotations(t *testing.T) { } } func TestService_AnnotationTypeScopeResolver(t *testing.T) { + rootDashUID := "root-dashboard" + folderDashUID := "folder-dashboard" + folderUID := "folder" + dashSvc := &dashboards.FakeDashboardService{} + rootDash := &dashboards.Dashboard{ID: 1, OrgID: 1, UID: rootDashUID} + folderDash := &dashboards.Dashboard{ID: 2, OrgID: 1, UID: folderDashUID, FolderUID: folderUID} + dashSvc.On("GetDashboard", context.Background(), &dashboards.GetDashboardQuery{ID: rootDash.ID, OrgID: 1}).Return(rootDash, nil) + dashSvc.On("GetDashboard", context.Background(), &dashboards.GetDashboardQuery{ID: folderDash.ID, OrgID: 1}).Return(folderDash, nil) + + rootDashboardAnnotation := annotations.Item{ID: 1, DashboardID: rootDash.ID} + folderDashboardAnnotation := annotations.Item{ID: 3, DashboardID: folderDash.ID} + organizationAnnotation := annotations.Item{ID: 2} + + fakeAnnoRepo := annotationstest.NewFakeAnnotationsRepo() + _ = fakeAnnoRepo.Save(context.Background(), &rootDashboardAnnotation) + _ = fakeAnnoRepo.Save(context.Background(), &folderDashboardAnnotation) + _ = fakeAnnoRepo.Save(context.Background(), &organizationAnnotation) + type testCaseResolver struct { - desc string - given string - want string - wantErr error + desc string + given string + featureToggles []any + want []string + wantErr error } testCases := []testCaseResolver{ { desc: "correctly resolves dashboard annotations", given: "annotations:id:1", - want: accesscontrol.ScopeAnnotationsTypeDashboard, + want: []string{accesscontrol.ScopeAnnotationsTypeDashboard}, wantErr: nil, }, { desc: "correctly resolves organization annotations", given: "annotations:id:2", - want: accesscontrol.ScopeAnnotationsTypeOrganization, + want: []string{accesscontrol.ScopeAnnotationsTypeOrganization}, wantErr: nil, }, { desc: "invalid annotation ID", given: "annotations:id:123abc", - want: "", + want: []string{""}, wantErr: accesscontrol.ErrInvalidScope, }, { desc: "malformed scope", given: "annotations:1", - want: "", + want: []string{""}, wantErr: accesscontrol.ErrInvalidScope, }, + { + desc: "correctly resolves organization annotations with feature toggle", + given: "annotations:id:2", + featureToggles: []any{featuremgmt.FlagAnnotationPermissionUpdate}, + want: []string{accesscontrol.ScopeAnnotationsTypeOrganization}, + wantErr: nil, + }, + { + desc: "correctly resolves annotations from root dashboard with feature toggle", + given: "annotations:id:1", + featureToggles: []any{featuremgmt.FlagAnnotationPermissionUpdate}, + want: []string{ + dashboards.ScopeDashboardsProvider.GetResourceScopeUID(rootDashUID), + dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID), + }, + wantErr: nil, + }, + { + desc: "correctly resolves annotations from dashboard in a folder with feature toggle", + given: "annotations:id:3", + featureToggles: []any{featuremgmt.FlagAnnotationPermissionUpdate}, + want: []string{ + dashboards.ScopeDashboardsProvider.GetResourceScopeUID(folderDashUID), + dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID), + }, + wantErr: nil, + }, } - dashboardAnnotation := annotations.Item{ID: 1, DashboardID: 1} - organizationAnnotation := annotations.Item{ID: 2} - - fakeAnnoRepo := annotationstest.NewFakeAnnotationsRepo() - _ = fakeAnnoRepo.Save(context.Background(), &dashboardAnnotation) - _ = fakeAnnoRepo.Save(context.Background(), &organizationAnnotation) - - prefix, resolver := AnnotationTypeScopeResolver(fakeAnnoRepo) - require.Equal(t, "annotations:id:", prefix) - for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { + features := featuremgmt.WithFeatures(tc.featureToggles...) + prefix, resolver := AnnotationTypeScopeResolver(fakeAnnoRepo, features, dashSvc, &foldertest.FakeService{}) + require.Equal(t, "annotations:id:", prefix) + resolved, err := resolver.Resolve(context.Background(), 1, tc.given) if tc.wantErr != nil { require.Error(t, err) require.Equal(t, tc.wantErr, err) } else { require.NoError(t, err) - require.Len(t, resolved, 1) - require.Equal(t, tc.want, resolved[0]) + require.Len(t, resolved, len(tc.want)) + require.Equal(t, tc.want, resolved) } }) } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 30646b55f67..edeb7705f75 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -358,7 +358,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi hs.registerRoutes() // Register access control scope resolver for annotations - hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo)) + hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, features, dashboardService, folderService)) if err := hs.declareFixedRoles(); err != nil { return nil, err