diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts index a83f5fb0cdc..fbbf77f9dba 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts +++ b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts @@ -30,6 +30,8 @@ export interface PanelContext { onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void; canAddAnnotations?: () => boolean; + canEditAnnotations?: (dashboardId: number) => boolean; + canDeleteAnnotations?: (dashboardId: number) => boolean; onAnnotationCreate?: (annotation: AnnotationEventUIModel) => void; onAnnotationUpdate?: (annotation: AnnotationEventUIModel) => void; onAnnotationDelete?: (id: string) => void; diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 17e1e4b49c9..293d40ddf82 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" @@ -115,25 +116,33 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { creator = hs.getUserLogin(c.Req.Context(), dash.CreatedBy) } + annotationPermissions := &dtos.AnnotationPermission{} + + if !hs.AccessControl.IsDisabled() { + hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard) + hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization) + } + meta := dtos.DashboardMeta{ - IsStarred: isStarred, - Slug: dash.Slug, - Type: models.DashTypeDB, - CanStar: c.IsSignedIn, - CanSave: canSave, - CanEdit: canEdit, - CanAdmin: canAdmin, - CanDelete: canDelete, - Created: dash.Created, - Updated: dash.Updated, - UpdatedBy: updater, - CreatedBy: creator, - Version: dash.Version, - HasAcl: dash.HasAcl, - IsFolder: dash.IsFolder, - FolderId: dash.FolderId, - Url: dash.GetUrl(), - FolderTitle: "General", + IsStarred: isStarred, + Slug: dash.Slug, + Type: models.DashTypeDB, + CanStar: c.IsSignedIn, + CanSave: canSave, + CanEdit: canEdit, + CanAdmin: canAdmin, + CanDelete: canDelete, + Created: dash.Created, + Updated: dash.Updated, + UpdatedBy: updater, + CreatedBy: creator, + Version: dash.Version, + HasAcl: dash.HasAcl, + IsFolder: dash.IsFolder, + FolderId: dash.FolderId, + Url: dash.GetUrl(), + FolderTitle: "General", + AnnotationsPermissions: annotationPermissions, } // lookup folder title @@ -190,6 +199,22 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { return response.JSON(200, dto) } +func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, actions *dtos.AnnotationActions, scope string) { + var err error + + evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, scope) + actions.CanDelete, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate) + if err != nil { + hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsDelete, "scope", scope) + } + + evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, scope) + actions.CanEdit, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate) + if err != nil { + hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsWrite, "scope", scope) + } +} + func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string { query := models.GetUserByIdQuery{Id: userID} err := hs.SQLStore.GetUserById(ctx, &query) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index d33ddc3349b..54b77fbb520 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -118,10 +118,11 @@ func TestDashboardAPIEndpoint(t *testing.T) { mockSQLStore.ExpectedDashboard = fakeDash hs := &HTTPServer{ - Cfg: setting.NewCfg(), - pluginStore: &fakePluginStore{}, - SQLStore: mockSQLStore, - Features: featuremgmt.WithFeatures(), + Cfg: setting.NewCfg(), + pluginStore: &fakePluginStore{}, + SQLStore: mockSQLStore, + AccessControl: accesscontrolmock.New(), + Features: featuremgmt.WithFeatures(), } hs.SQLStore = mockSQLStore @@ -224,6 +225,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &mockLibraryElementService{}, SQLStore: mockSQLStore, + AccessControl: accesscontrolmock.New(), dashboardService: service.ProvideDashboardService( cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(), ), @@ -890,6 +892,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { LibraryElementService: &mockLibraryElementService{}, dashboardProvisioningService: mockDashboardProvisioningService{}, SQLStore: mockSQLStore, + AccessControl: accesscontrolmock.New(), } hs.callGetDashboard(sc) @@ -927,6 +930,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr LibraryElementService: &libraryElementsService, SQLStore: sc.sqlStore, ProvisioningService: provisioningService, + AccessControl: accesscontrolmock.New(), dashboardProvisioningService: service.ProvideDashboardService( cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(), ), diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index c7f14f12ab7..707bd549804 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -7,31 +7,41 @@ import ( ) type DashboardMeta struct { - IsStarred bool `json:"isStarred,omitempty"` - IsHome bool `json:"isHome,omitempty"` - IsSnapshot bool `json:"isSnapshot,omitempty"` - Type string `json:"type,omitempty"` - CanSave bool `json:"canSave"` - CanEdit bool `json:"canEdit"` - CanAdmin bool `json:"canAdmin"` - CanStar bool `json:"canStar"` - CanDelete bool `json:"canDelete"` - Slug string `json:"slug"` - Url string `json:"url"` - Expires time.Time `json:"expires"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - UpdatedBy string `json:"updatedBy"` - CreatedBy string `json:"createdBy"` - Version int `json:"version"` - HasAcl bool `json:"hasAcl"` - IsFolder bool `json:"isFolder"` - FolderId int64 `json:"folderId"` - FolderUid string `json:"folderUid"` - FolderTitle string `json:"folderTitle"` - FolderUrl string `json:"folderUrl"` - Provisioned bool `json:"provisioned"` - ProvisionedExternalId string `json:"provisionedExternalId"` + IsStarred bool `json:"isStarred,omitempty"` + IsHome bool `json:"isHome,omitempty"` + IsSnapshot bool `json:"isSnapshot,omitempty"` + Type string `json:"type,omitempty"` + CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` + CanAdmin bool `json:"canAdmin"` + CanStar bool `json:"canStar"` + CanDelete bool `json:"canDelete"` + Slug string `json:"slug"` + Url string `json:"url"` + Expires time.Time `json:"expires"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + UpdatedBy string `json:"updatedBy"` + CreatedBy string `json:"createdBy"` + Version int `json:"version"` + HasAcl bool `json:"hasAcl"` + IsFolder bool `json:"isFolder"` + FolderId int64 `json:"folderId"` + FolderUid string `json:"folderUid"` + FolderTitle string `json:"folderTitle"` + FolderUrl string `json:"folderUrl"` + Provisioned bool `json:"provisioned"` + ProvisionedExternalId string `json:"provisionedExternalId"` + AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"` +} +type AnnotationPermission struct { + Dashboard AnnotationActions `json:"dashboard"` + Organization AnnotationActions `json:"organization"` +} + +type AnnotationActions struct { + CanEdit bool `json:"canEdit"` + CanDelete bool `json:"canDelete"` } type DashboardFullWithMeta struct { diff --git a/public/app/features/annotations/partials/event_editor.html b/public/app/features/annotations/partials/event_editor.html index b11b24ca767..53add6789cc 100644 --- a/public/app/features/annotations/partials/event_editor.html +++ b/public/app/features/annotations/partials/event_editor.html @@ -27,7 +27,7 @@
diff --git a/public/app/features/annotations/standardAnnotationSupport.ts b/public/app/features/annotations/standardAnnotationSupport.ts index 9348dd2535e..edc6ac668ca 100644 --- a/public/app/features/annotations/standardAnnotationSupport.ts +++ b/public/app/features/annotations/standardAnnotationSupport.ts @@ -124,6 +124,7 @@ const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [ { key: 'data' as any }, { key: 'panelId' }, { key: 'alertId' }, + { key: 'dashboardId' }, ]; export function getAnnotationsFromData( diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b4ee69845e6..3a082c19a07 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -38,6 +38,7 @@ import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../annota import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner'; import { liveTimer } from './liveTimer'; import { isSoloRoute } from '../../../routes/utils'; +import { contextSrv } from '../../../core/services/context_srv'; const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; @@ -90,11 +91,39 @@ export class PanelChrome extends PureComponent