diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 3195d1cd76b..398229012c3 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -280,9 +280,9 @@ func (hs *HTTPServer) declareFixedRoles() error { DisplayName: "Dashboard annotation writer", Description: "Update annotations associated with dashboards.", Group: "Annotations", - Version: 2, + Version: 3, Permissions: []ac.Permission{ - {Action: ac.ActionAnnotationsCreate}, + {Action: ac.ActionAnnotationsCreate, Scope: ac.ScopeAnnotationsTypeDashboard}, {Action: ac.ActionAnnotationsDelete, Scope: ac.ScopeAnnotationsTypeDashboard}, {Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsTypeDashboard}, }, @@ -296,9 +296,9 @@ func (hs *HTTPServer) declareFixedRoles() error { DisplayName: "Annotation writer", Description: "Update all annotations.", Group: "Annotations", - Version: 1, + Version: 2, Permissions: []ac.Permission{ - {Action: ac.ActionAnnotationsCreate}, + {Action: ac.ActionAnnotationsCreate, Scope: ac.ScopeAnnotationsAll}, {Action: ac.ActionAnnotationsDelete, Scope: ac.ScopeAnnotationsAll}, {Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsAll}, }, diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 4f0d354a1b0..8647fbf325c 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -49,11 +49,11 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { return response.JSON(200, items) } -type CreateAnnotationError struct { +type AnnotationError struct { message string } -func (e *CreateAnnotationError) Error() string { +func (e *AnnotationError) Error() string { return e.message } @@ -85,7 +85,7 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { repo := annotations.GetRepository() if cmd.Text == "" { - err := &CreateAnnotationError{"text field should not be empty"} + err := &AnnotationError{"text field should not be empty"} return response.Error(400, "Failed to save annotation", err) } @@ -132,7 +132,7 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Resp repo := annotations.GetRepository() if cmd.What == "" { - err := &CreateAnnotationError{"what field should not be empty"} + err := &AnnotationError{"what field should not be empty"} return response.Error(400, "Failed to save Graphite annotation", err) } @@ -152,12 +152,12 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Resp if tagStr, ok := t.(string); ok { tagsArray = append(tagsArray, tagStr) } else { - err := &CreateAnnotationError{"tag should be a string"} + err := &AnnotationError{"tag should be a string"} return response.Error(400, "Failed to save Graphite annotation", err) } } default: - err := &CreateAnnotationError{"unsupported tags format"} + err := &AnnotationError{"unsupported tags format"} return response.Error(400, "Failed to save Graphite annotation", err) } @@ -289,19 +289,59 @@ func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response { return response.Success("Annotation patched") } -func (hs *HTTPServer) DeleteAnnotations(c *models.ReqContext) response.Response { - cmd := dtos.DeleteAnnotationsCmd{} - if err := web.Bind(c.Req, &cmd); err != nil { +func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Response { + cmd := dtos.MassDeleteAnnotationsCmd{} + err := web.Bind(c.Req, &cmd) + if err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - repo := annotations.GetRepository() - err := repo.Delete(&annotations.DeleteParams{ - OrgId: c.OrgId, - Id: cmd.AnnotationId, - DashboardId: cmd.DashboardId, - PanelId: cmd.PanelId, - }) + if (cmd.DashboardId != 0 && cmd.PanelId == 0) || (cmd.PanelId != 0 && cmd.DashboardId == 0) { + err := &AnnotationError{message: "DashboardId and PanelId are both required for mass delete"} + return response.Error(http.StatusBadRequest, "bad request data", err) + } + + repo := annotations.GetRepository() + var deleteParams *annotations.DeleteParams + + // validations only for FGAC. A user can mass delete all annotations in a (dashboard + panel) or a specific annotation + // if has access to that dashboard. + if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) { + var dashboardId int64 + + if cmd.AnnotationId != 0 { + annotation, respErr := findAnnotationByID(c.Req.Context(), repo, cmd.AnnotationId, c.OrgId) + if respErr != nil { + return respErr + } + dashboardId = annotation.DashboardId + deleteParams = &annotations.DeleteParams{ + OrgId: c.OrgId, + Id: cmd.AnnotationId, + } + } else { + dashboardId = cmd.DashboardId + deleteParams = &annotations.DeleteParams{ + OrgId: c.OrgId, + DashboardId: cmd.DashboardId, + PanelId: cmd.PanelId, + } + } + + canSave, err := hs.canMassDeleteAnnotations(c, dashboardId) + if err != nil || !canSave { + return dashboardGuardianResponse(err) + } + } else { // legacy permissions + deleteParams = &annotations.DeleteParams{ + OrgId: c.OrgId, + Id: cmd.AnnotationId, + DashboardId: cmd.DashboardId, + PanelId: cmd.PanelId, + } + } + + err = repo.Delete(deleteParams) if err != nil { return response.Error(500, "Failed to delete annotations", err) @@ -424,3 +464,23 @@ func (hs *HTTPServer) canCreateOrganizationAnnotation(c *models.ReqContext) (boo evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) } + +func (hs *HTTPServer) canMassDeleteAnnotations(c *models.ReqContext, dashboardID int64) (bool, error) { + if dashboardID == 0 { + evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization) + return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) + } else { + evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeDashboard) + canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) + if err != nil || !canSave { + return false, err + } + + canSave, err = canSaveDashboardAnnotation(c, dashboardID) + if err != nil || !canSave { + return false, err + } + } + + return true, nil +} diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 6abe17e379c..36b911296fa 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -71,7 +71,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { mock := mockstore.NewSQLStoreMock() loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { - fakeAnnoRepo = &fakeAnnotationsRepo{} + fakeAnnoRepo = NewFakeAnnotationsRepo() annotations.SetRepository(fakeAnnoRepo) sc.handlerFunc = hs.DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -101,7 +101,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { mock := mockstore.NewSQLStoreMock() loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { - fakeAnnoRepo = &fakeAnnotationsRepo{} + fakeAnnoRepo = NewFakeAnnotationsRepo() annotations.SetRepository(fakeAnnoRepo) sc.handlerFunc = hs.DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -134,7 +134,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { Id: 1, } - deleteCmd := dtos.DeleteAnnotationsCmd{ + deleteCmd := dtos.MassDeleteAnnotationsCmd{ DashboardId: 1, PanelId: 1, } @@ -163,7 +163,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { setUpACL() - fakeAnnoRepo = &fakeAnnotationsRepo{} + fakeAnnoRepo = NewFakeAnnotationsRepo() annotations.SetRepository(fakeAnnoRepo) sc.handlerFunc = hs.DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -196,7 +196,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { setUpACL() - fakeAnnoRepo = &fakeAnnotationsRepo{} + fakeAnnoRepo = NewFakeAnnotationsRepo() annotations.SetRepository(fakeAnnoRepo) sc.handlerFunc = hs.DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -238,14 +238,33 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { } type fakeAnnotationsRepo struct { - annotations map[int64]annotations.ItemDTO + annotations map[int64]annotations.Item +} + +func NewFakeAnnotationsRepo() *fakeAnnotationsRepo { + return &fakeAnnotationsRepo{ + annotations: map[int64]annotations.Item{}, + } } func (repo *fakeAnnotationsRepo) Delete(params *annotations.DeleteParams) error { + if params.Id != 0 { + delete(repo.annotations, params.Id) + } else { + for _, v := range repo.annotations { + if params.DashboardId == v.DashboardId && params.PanelId == v.PanelId { + delete(repo.annotations, v.Id) + } + } + } + return nil } func (repo *fakeAnnotationsRepo) Save(item *annotations.Item) error { - item.Id = 1 + if item.Id == 0 { + item.Id = int64(len(repo.annotations) + 1) + } + repo.annotations[item.Id] = *item return nil } func (repo *fakeAnnotationsRepo) Update(_ context.Context, item *annotations.Item) error { @@ -253,9 +272,9 @@ func (repo *fakeAnnotationsRepo) Update(_ context.Context, item *annotations.Ite } func (repo *fakeAnnotationsRepo) Find(_ context.Context, query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) { if annotation, has := repo.annotations[query.AnnotationId]; has { - return []*annotations.ItemDTO{&annotation}, nil + return []*annotations.ItemDTO{{Id: annotation.Id, DashboardId: annotation.DashboardId}}, nil } - annotations := []*annotations.ItemDTO{{Id: 1}} + annotations := []*annotations.ItemDTO{{Id: 1, DashboardId: 0}} return annotations, nil } func (repo *fakeAnnotationsRepo) FindTags(query *annotations.TagsQuery) (annotations.FindTagsResult, error) { @@ -265,6 +284,10 @@ func (repo *fakeAnnotationsRepo) FindTags(query *annotations.TagsQuery) (annotat return result, nil } +func (repo *fakeAnnotationsRepo) LoadItems() { + +} + var fakeAnnoRepo *fakeAnnotationsRepo func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, @@ -289,7 +312,7 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern return hs.PostAnnotation(c) }) - fakeAnnoRepo = &fakeAnnotationsRepo{} + fakeAnnoRepo = NewFakeAnnotationsRepo() annotations.SetRepository(fakeAnnoRepo) sc.m.Post(routePattern, sc.defaultHandler) @@ -320,7 +343,7 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s return hs.UpdateAnnotation(c) }) - fakeAnnoRepo = &fakeAnnotationsRepo{} + fakeAnnoRepo = NewFakeAnnotationsRepo() annotations.SetRepository(fakeAnnoRepo) sc.m.Put(routePattern, sc.defaultHandler) @@ -350,7 +373,7 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern return hs.PatchAnnotation(c) }) - fakeAnnoRepo = &fakeAnnotationsRepo{} + fakeAnnoRepo = NewFakeAnnotationsRepo() annotations.SetRepository(fakeAnnoRepo) sc.m.Patch(routePattern, sc.defaultHandler) @@ -360,7 +383,7 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern } func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, - cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) { + cmd dtos.MassDeleteAnnotationsCmd, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() @@ -378,10 +401,10 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte sc.context.OrgId = testOrgID sc.context.OrgRole = role - return hs.DeleteAnnotations(c) + return hs.MassDeleteAnnotations(c) }) - fakeAnnoRepo = &fakeAnnotationsRepo{} + fakeAnnoRepo = NewFakeAnnotationsRepo() annotations.SetRepository(fakeAnnoRepo) sc.m.Post(routePattern, sc.defaultHandler) @@ -396,12 +419,13 @@ func TestAPI_Annotations_AccessControl(t *testing.T) { _, err := sc.db.CreateOrgWithMember("TestOrg", testUserID) require.NoError(t, err) - dashboardAnnotation := annotations.ItemDTO{Id: 1, DashboardId: 1} - organizationAnnotation := annotations.ItemDTO{Id: 2, DashboardId: 0} + dashboardAnnotation := &annotations.Item{Id: 1, DashboardId: 1} + organizationAnnotation := &annotations.Item{Id: 2, DashboardId: 0} + + fakeAnnoRepo = NewFakeAnnotationsRepo() + _ = fakeAnnoRepo.Save(dashboardAnnotation) + _ = fakeAnnoRepo.Save(organizationAnnotation) - fakeAnnoRepo = &fakeAnnotationsRepo{ - annotations: map[int64]annotations.ItemDTO{1: dashboardAnnotation, 2: organizationAnnotation}, - } annotations.SetRepository(fakeAnnoRepo) postOrganizationCmd := dtos.PostAnnotationsCmd{ @@ -693,8 +717,9 @@ func TestAPI_Annotations_AccessControl(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - setUpACL() - sc.acmock.RegisterAttributeScopeResolver(AnnotationTypeScopeResolver()) + setUpFGACGuardian(t) + sc.acmock. + RegisterAttributeScopeResolver(AnnotationTypeScopeResolver()) setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgId) r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t) @@ -738,12 +763,13 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) { }, } - dashboardAnnotation := annotations.ItemDTO{Id: 1, DashboardId: 1} - organizationAnnotation := annotations.ItemDTO{Id: 2} + dashboardAnnotation := annotations.Item{Id: 1, DashboardId: 1} + organizationAnnotation := annotations.Item{Id: 2} + + fakeAnnoRepo = NewFakeAnnotationsRepo() + _ = fakeAnnoRepo.Save(&dashboardAnnotation) + _ = fakeAnnoRepo.Save(&organizationAnnotation) - fakeAnnoRepo = &fakeAnnotationsRepo{ - annotations: map[int64]annotations.ItemDTO{1: dashboardAnnotation, 2: organizationAnnotation}, - } annotations.SetRepository(fakeAnnoRepo) prefix, resolver := AnnotationTypeScopeResolver() @@ -763,6 +789,145 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) { } } +func TestAPI_MassDeleteAnnotations_AccessControl(t *testing.T) { + sc := setupHTTPServer(t, true, true) + setInitCtxSignedInEditor(sc.initCtx) + _, err := sc.db.CreateOrgWithMember("TestOrg", testUserID) + require.NoError(t, err) + + type args struct { + permissions []*accesscontrol.Permission + url string + body io.Reader + method string + } + + tests := []struct { + name string + args args + want int + }{ + { + name: "Mass delete dashboard annotations without dashboardId is not allowed", + args: args{ + permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, + url: "/api/annotations/mass-delete", + method: http.MethodPost, + body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ + DashboardId: 0, + PanelId: 1, + }), + }, + want: http.StatusBadRequest, + }, + { + name: "Mass delete dashboard annotations without panelId is not allowed", + args: args{ + permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, + url: "/api/annotations/mass-delete", + method: http.MethodPost, + body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ + DashboardId: 10, + PanelId: 0, + }), + }, + want: http.StatusBadRequest, + }, + { + name: "AccessControl mass delete dashboard annotations with correct dashboardId and panelId as input is allowed", + args: args{ + permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + url: "/api/annotations/mass-delete", + method: http.MethodPost, + body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ + DashboardId: 1, + PanelId: 1, + }), + }, + want: http.StatusOK, + }, + { + name: "Mass delete organization annotations without input to delete all organization annotations is allowed", + args: args{ + permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, + url: "/api/annotations/mass-delete", + method: http.MethodPost, + body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ + DashboardId: 0, + PanelId: 0, + }), + }, + want: http.StatusOK, + }, + { + name: "Mass delete organization annotations without permissions is forbidden", + args: args{ + permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + url: "/api/annotations/mass-delete", + method: http.MethodPost, + body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ + DashboardId: 0, + PanelId: 0, + }), + }, + want: http.StatusForbidden, + }, + { + name: "AccessControl mass delete dashboard annotations with correct annotationId as input is allowed", + args: args{ + permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + url: "/api/annotations/mass-delete", + method: http.MethodPost, + body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ + AnnotationId: 1, + }), + }, + want: http.StatusOK, + }, + { + name: "AccessControl mass delete annotation without access to dashboard annotations is forbidden", + args: args{ + permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, + url: "/api/annotations/mass-delete", + method: http.MethodPost, + body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ + AnnotationId: 1, + }), + }, + want: http.StatusForbidden, + }, + { + name: "AccessControl mass delete annotation without access to organization annotations is forbidden", + args: args{ + permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, + url: "/api/annotations/mass-delete", + method: http.MethodPost, + body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ + AnnotationId: 2, + }), + }, + want: http.StatusForbidden, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setUpFGACGuardian(t) + setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgId) + dashboardAnnotation := &annotations.Item{Id: 1, DashboardId: 1} + organizationAnnotation := &annotations.Item{Id: 2, DashboardId: 0} + + fakeAnnoRepo = NewFakeAnnotationsRepo() + _ = fakeAnnoRepo.Save(dashboardAnnotation) + _ = fakeAnnoRepo.Save(organizationAnnotation) + + annotations.SetRepository(fakeAnnoRepo) + + r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t) + assert.Equalf(t, tt.want, r.Code, "Annotations API(%v)", tt.args.url) + }) + } +} + func setUpACL() { viewerRole := models.ROLE_VIEWER editorRole := models.ROLE_EDITOR @@ -776,3 +941,12 @@ func setUpACL() { store.ExpectedTeamsByUser = []*models.TeamDTO{} guardian.InitLegacyGuardian(store) } + +func setUpFGACGuardian(t *testing.T) { + origNewGuardian := guardian.New + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true}) +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 3eb19d216c6..1b3fc216d1a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -437,7 +437,7 @@ func (hs *HTTPServer) registerRoutes() { }) apiRoute.Get("/annotations", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead, ac.ScopeAnnotationsAll)), routing.Wrap(hs.GetAnnotations)) - apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, routing.Wrap(hs.DeleteAnnotations)) + apiRoute.Post("/annotations/mass-delete", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAnnotationsDelete)), routing.Wrap(hs.MassDeleteAnnotations)) apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) { annotationsRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsCreate)), routing.Wrap(hs.PostAnnotation)) diff --git a/pkg/api/docs/definitions/annotations.go b/pkg/api/docs/definitions/annotations.go index a4f22345a3c..280842fedc3 100644 --- a/pkg/api/docs/definitions/annotations.go +++ b/pkg/api/docs/definitions/annotations.go @@ -178,7 +178,7 @@ type GetAnnotationTagssParams struct { type MassDeleteAnnotationsParams struct { // in:body // required:true - Body dtos.DeleteAnnotationsCmd `json:"body"` + Body dtos.MassDeleteAnnotationsCmd `json:"body"` } // swagger:parameters createAnnotation diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index 83ad866ef1d..15cbc48838d 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -28,8 +28,7 @@ type PatchAnnotationsCmd struct { Tags []string `json:"tags"` } -type DeleteAnnotationsCmd struct { - AlertId int64 `json:"alertId"` +type MassDeleteAnnotationsCmd struct { DashboardId int64 `json:"dashboardId"` PanelId int64 `json:"panelId"` AnnotationId int64 `json:"annotationId"` diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index 9798e4a42b6..f571b1f8e62 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -75,7 +75,6 @@ type GetAnnotationTagsResponse struct { type DeleteParams struct { OrgId int64 Id int64 - AlertId int64 DashboardId int64 PanelId int64 }