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 @@
- + Cancel
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 { canAddAnnotations: () => Boolean(props.dashboard.meta.canEdit || props.dashboard.meta.canMakeEditable), onInstanceStateChange: this.onInstanceStateChange, onToggleLegendSort: this.onToggleLegendSort, + canEditAnnotations: this.canEditAnnotation, + canDeleteAnnotations: this.canDeleteAnnotation, }, data: this.getInitialPanelDataState(), }; } + canEditAnnotation = (dashboardId: number) => { + let canEdit = true; + + if (contextSrv.accessControlEnabled()) { + if (dashboardId !== 0) { + canEdit = !!this.props.dashboard.meta.annotationsPermissions?.dashboard.canEdit; + } else { + canEdit = !!this.props.dashboard.meta.annotationsPermissions?.organization.canEdit; + } + } + return canEdit && Boolean(this.props.dashboard.meta.canEdit || this.props.dashboard.meta.canMakeEditable); + }; + + canDeleteAnnotation = (dashboardId: number) => { + let canDelete = true; + + if (contextSrv.accessControlEnabled()) { + if (dashboardId !== 0) { + canDelete = !!this.props.dashboard.meta.annotationsPermissions?.dashboard.canDelete; + } else { + canDelete = !!this.props.dashboard.meta.annotationsPermissions?.organization.canDelete; + } + } + return canDelete && Boolean(this.props.dashboard.meta.canEdit || this.props.dashboard.meta.canMakeEditable); + }; + // Due to a mutable panel model we get the sync settings via function that proactively reads from the model getSync = () => (this.props.isEditing ? DashboardCursorSync.Off : this.props.dashboard.graphTooltip); diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 383fd66e6cb..c307f004196 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -1178,6 +1178,20 @@ export class DashboardModel implements TimeModel { return this.getVariablesFromState(this.uid); }; + canEditAnnotations(dashboardId: number) { + let canEdit = true; + + // if FGAC is enabled there are additional conditions to check + if (contextSrv.accessControlEnabled()) { + if (dashboardId === 0) { + canEdit = !!this.meta.annotationsPermissions?.organization.canEdit; + } else { + canEdit = !!this.meta.annotationsPermissions?.dashboard.canEdit; + } + } + return this.canAddAnnotations() && canEdit; + } + canAddAnnotations() { return this.meta.canEdit || this.meta.canMakeEditable; } diff --git a/public/app/plugins/panel/graph/annotation_tooltip.ts b/public/app/plugins/panel/graph/annotation_tooltip.ts index ce7d8b6af81..b27a87861e1 100644 --- a/public/app/plugins/panel/graph/annotation_tooltip.ts +++ b/public/app/plugins/panel/graph/annotation_tooltip.ts @@ -59,7 +59,7 @@ export function annotationTooltipDirective( `; // Show edit icon only for users with at least Editor role - if (event.id && dashboard?.canAddAnnotations()) { + if (event.id && dashboard?.canEditAnnotations(event.dashboardId)) { header += ` diff --git a/public/app/plugins/panel/graph/event_editor.ts b/public/app/plugins/panel/graph/event_editor.ts index a02ef28e89d..a2ea2f2972b 100644 --- a/public/app/plugins/panel/graph/event_editor.ts +++ b/public/app/plugins/panel/graph/event_editor.ts @@ -4,6 +4,7 @@ import { AnnotationEvent, dateTime } from '@grafana/data'; import { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl'; import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../../features/annotations/api'; import { getDashboardQueryRunner } from '../../../features/query/state/DashboardQueryRunner/DashboardQueryRunner'; +import { contextSrv } from '../../../core/services/context_srv'; export class EventEditorCtrl { // @ts-ignore initialized through Angular not constructor @@ -31,6 +32,16 @@ export class EventEditorCtrl { this.timeFormated = this.panelCtrl.dashboard.formatDate(this.event.time!); } + canDelete(): boolean { + if (contextSrv.accessControlEnabled()) { + if (this.event.source.type === 'dashboard') { + return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.dashboard.canDelete; + } + return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.organization.canDelete; + } + return true; + } + async save(): Promise { if (!this.form.$valid) { return; diff --git a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationMarker.tsx b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationMarker.tsx index 72631c16daa..c7c8cc19a01 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationMarker.tsx @@ -27,7 +27,7 @@ const POPPER_CONFIG = { }; export function AnnotationMarker({ annotation, timeZone, style }: Props) { - const { canAddAnnotations, ...panelCtx } = usePanelContext(); + const { canAddAnnotations, canEditAnnotations, canDeleteAnnotations, ...panelCtx } = usePanelContext(); const commonStyles = useStyles2(getCommonAnnotationStyles); const styles = useStyles2(getStyles); @@ -89,10 +89,11 @@ export function AnnotationMarker({ annotation, timeZone, style }: Props) { timeFormatter={timeFormatter} onEdit={onAnnotationEdit} onDelete={onAnnotationDelete} - editable={Boolean(canAddAnnotations && canAddAnnotations())} + canEdit={canEditAnnotations!(annotation.dashboardId)} + canDelete={canDeleteAnnotations!(annotation.dashboardId)} /> ); - }, [canAddAnnotations, onAnnotationDelete, onAnnotationEdit, timeFormatter, annotation]); + }, [canEditAnnotations, canDeleteAnnotations, onAnnotationDelete, onAnnotationEdit, timeFormatter, annotation]); const isRegionAnnotation = Boolean(annotation.isRegion); diff --git a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx index ef94c013b18..ce5ff0ba437 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx @@ -10,7 +10,8 @@ import config from 'app/core/config'; interface AnnotationTooltipProps { annotation: AnnotationsDataFrameViewDTO; timeFormatter: (v: number) => string; - editable: boolean; + canEdit: boolean; + canDelete: boolean; onEdit: () => void; onDelete: () => void; } @@ -18,7 +19,8 @@ interface AnnotationTooltipProps { export const AnnotationTooltip = ({ annotation, timeFormatter, - editable, + canEdit, + canDelete, onEdit, onDelete, }: AnnotationTooltipProps) => { @@ -51,11 +53,11 @@ export const AnnotationTooltip = ({ text = annotation.title + '
' + (typeof text === 'string' ? text : ''); } - if (editable) { + if (canEdit || canDelete) { editControls = (
- - + {canEdit && } + {canDelete && }
); } diff --git a/public/app/plugins/panel/timeseries/plugins/types.ts b/public/app/plugins/panel/timeseries/plugins/types.ts index 512e4852a8f..96644432ec0 100644 --- a/public/app/plugins/panel/timeseries/plugins/types.ts +++ b/public/app/plugins/panel/timeseries/plugins/types.ts @@ -1,5 +1,6 @@ interface AnnotationsDataFrameViewDTO { id: string; + dashboardId: number; time: number; timeEnd: number; text: string; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index c5e51eb680c..2fd12e51062 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -35,6 +35,17 @@ export interface DashboardMeta { fromScript?: boolean; fromFile?: boolean; hasUnsavedFolderChange?: boolean; + annotationsPermissions?: AnnotationsPermissions; +} + +export interface AnnotationActions { + canEdit: boolean; + canDelete: boolean; +} + +export interface AnnotationsPermissions { + dashboard: AnnotationActions; + organization: AnnotationActions; } export interface DashboardDataDTO {