mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access Control: adding FGAC validation to mass delete annotation endpoint (#46846)
* Access Control: adding FGAC validation to mass delete annotation endpoint
This commit is contained in:
parent
60d4cd80bf
commit
c5f295b5b3
@ -280,9 +280,9 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
|||||||
DisplayName: "Dashboard annotation writer",
|
DisplayName: "Dashboard annotation writer",
|
||||||
Description: "Update annotations associated with dashboards.",
|
Description: "Update annotations associated with dashboards.",
|
||||||
Group: "Annotations",
|
Group: "Annotations",
|
||||||
Version: 2,
|
Version: 3,
|
||||||
Permissions: []ac.Permission{
|
Permissions: []ac.Permission{
|
||||||
{Action: ac.ActionAnnotationsCreate},
|
{Action: ac.ActionAnnotationsCreate, Scope: ac.ScopeAnnotationsTypeDashboard},
|
||||||
{Action: ac.ActionAnnotationsDelete, Scope: ac.ScopeAnnotationsTypeDashboard},
|
{Action: ac.ActionAnnotationsDelete, Scope: ac.ScopeAnnotationsTypeDashboard},
|
||||||
{Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsTypeDashboard},
|
{Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsTypeDashboard},
|
||||||
},
|
},
|
||||||
@ -296,9 +296,9 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
|||||||
DisplayName: "Annotation writer",
|
DisplayName: "Annotation writer",
|
||||||
Description: "Update all annotations.",
|
Description: "Update all annotations.",
|
||||||
Group: "Annotations",
|
Group: "Annotations",
|
||||||
Version: 1,
|
Version: 2,
|
||||||
Permissions: []ac.Permission{
|
Permissions: []ac.Permission{
|
||||||
{Action: ac.ActionAnnotationsCreate},
|
{Action: ac.ActionAnnotationsCreate, Scope: ac.ScopeAnnotationsAll},
|
||||||
{Action: ac.ActionAnnotationsDelete, Scope: ac.ScopeAnnotationsAll},
|
{Action: ac.ActionAnnotationsDelete, Scope: ac.ScopeAnnotationsAll},
|
||||||
{Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsAll},
|
{Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsAll},
|
||||||
},
|
},
|
||||||
|
@ -49,11 +49,11 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response {
|
|||||||
return response.JSON(200, items)
|
return response.JSON(200, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateAnnotationError struct {
|
type AnnotationError struct {
|
||||||
message string
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *CreateAnnotationError) Error() string {
|
func (e *AnnotationError) Error() string {
|
||||||
return e.message
|
return e.message
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response {
|
|||||||
repo := annotations.GetRepository()
|
repo := annotations.GetRepository()
|
||||||
|
|
||||||
if cmd.Text == "" {
|
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)
|
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()
|
repo := annotations.GetRepository()
|
||||||
|
|
||||||
if cmd.What == "" {
|
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)
|
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 {
|
if tagStr, ok := t.(string); ok {
|
||||||
tagsArray = append(tagsArray, tagStr)
|
tagsArray = append(tagsArray, tagStr)
|
||||||
} else {
|
} 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)
|
return response.Error(400, "Failed to save Graphite annotation", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
err := &CreateAnnotationError{"unsupported tags format"}
|
err := &AnnotationError{"unsupported tags format"}
|
||||||
return response.Error(400, "Failed to save Graphite annotation", err)
|
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")
|
return response.Success("Annotation patched")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) DeleteAnnotations(c *models.ReqContext) response.Response {
|
func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Response {
|
||||||
cmd := dtos.DeleteAnnotationsCmd{}
|
cmd := dtos.MassDeleteAnnotationsCmd{}
|
||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
err := web.Bind(c.Req, &cmd)
|
||||||
|
if err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
}
|
}
|
||||||
repo := annotations.GetRepository()
|
|
||||||
|
|
||||||
err := repo.Delete(&annotations.DeleteParams{
|
if (cmd.DashboardId != 0 && cmd.PanelId == 0) || (cmd.PanelId != 0 && cmd.DashboardId == 0) {
|
||||||
OrgId: c.OrgId,
|
err := &AnnotationError{message: "DashboardId and PanelId are both required for mass delete"}
|
||||||
Id: cmd.AnnotationId,
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
DashboardId: cmd.DashboardId,
|
}
|
||||||
PanelId: cmd.PanelId,
|
|
||||||
})
|
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 {
|
if err != nil {
|
||||||
return response.Error(500, "Failed to delete annotations", err)
|
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)
|
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||||||
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
|
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
|
||||||
|
}
|
||||||
|
@ -71,7 +71,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
|
|||||||
mock := mockstore.NewSQLStoreMock()
|
mock := mockstore.NewSQLStoreMock()
|
||||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
|
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
|
||||||
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
fakeAnnoRepo = NewFakeAnnotationsRepo()
|
||||||
annotations.SetRepository(fakeAnnoRepo)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
sc.handlerFunc = hs.DeleteAnnotationByID
|
sc.handlerFunc = hs.DeleteAnnotationByID
|
||||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
@ -101,7 +101,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
|
|||||||
mock := mockstore.NewSQLStoreMock()
|
mock := mockstore.NewSQLStoreMock()
|
||||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
|
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
|
||||||
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
fakeAnnoRepo = NewFakeAnnotationsRepo()
|
||||||
annotations.SetRepository(fakeAnnoRepo)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
sc.handlerFunc = hs.DeleteAnnotationByID
|
sc.handlerFunc = hs.DeleteAnnotationByID
|
||||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
@ -134,7 +134,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
|
|||||||
Id: 1,
|
Id: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCmd := dtos.DeleteAnnotationsCmd{
|
deleteCmd := dtos.MassDeleteAnnotationsCmd{
|
||||||
DashboardId: 1,
|
DashboardId: 1,
|
||||||
PanelId: 1,
|
PanelId: 1,
|
||||||
}
|
}
|
||||||
@ -163,7 +163,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
|
|||||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
|
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
|
||||||
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||||
setUpACL()
|
setUpACL()
|
||||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
fakeAnnoRepo = NewFakeAnnotationsRepo()
|
||||||
annotations.SetRepository(fakeAnnoRepo)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
sc.handlerFunc = hs.DeleteAnnotationByID
|
sc.handlerFunc = hs.DeleteAnnotationByID
|
||||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
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",
|
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
|
||||||
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||||
setUpACL()
|
setUpACL()
|
||||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
fakeAnnoRepo = NewFakeAnnotationsRepo()
|
||||||
annotations.SetRepository(fakeAnnoRepo)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
sc.handlerFunc = hs.DeleteAnnotationByID
|
sc.handlerFunc = hs.DeleteAnnotationByID
|
||||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
@ -238,14 +238,33 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fakeAnnotationsRepo struct {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
func (repo *fakeAnnotationsRepo) Save(item *annotations.Item) error {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
func (repo *fakeAnnotationsRepo) Update(_ context.Context, item *annotations.Item) error {
|
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) {
|
func (repo *fakeAnnotationsRepo) Find(_ context.Context, query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
|
||||||
if annotation, has := repo.annotations[query.AnnotationId]; has {
|
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
|
return annotations, nil
|
||||||
}
|
}
|
||||||
func (repo *fakeAnnotationsRepo) FindTags(query *annotations.TagsQuery) (annotations.FindTagsResult, error) {
|
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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *fakeAnnotationsRepo) LoadItems() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var fakeAnnoRepo *fakeAnnotationsRepo
|
var fakeAnnoRepo *fakeAnnotationsRepo
|
||||||
|
|
||||||
func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
|
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)
|
return hs.PostAnnotation(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
fakeAnnoRepo = NewFakeAnnotationsRepo()
|
||||||
annotations.SetRepository(fakeAnnoRepo)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
|
|
||||||
sc.m.Post(routePattern, sc.defaultHandler)
|
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)
|
return hs.UpdateAnnotation(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
fakeAnnoRepo = NewFakeAnnotationsRepo()
|
||||||
annotations.SetRepository(fakeAnnoRepo)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
|
|
||||||
sc.m.Put(routePattern, sc.defaultHandler)
|
sc.m.Put(routePattern, sc.defaultHandler)
|
||||||
@ -350,7 +373,7 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern
|
|||||||
return hs.PatchAnnotation(c)
|
return hs.PatchAnnotation(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
fakeAnnoRepo = NewFakeAnnotationsRepo()
|
||||||
annotations.SetRepository(fakeAnnoRepo)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
|
|
||||||
sc.m.Patch(routePattern, sc.defaultHandler)
|
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,
|
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) {
|
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||||
defer bus.ClearBusHandlers()
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
@ -378,10 +401,10 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte
|
|||||||
sc.context.OrgId = testOrgID
|
sc.context.OrgId = testOrgID
|
||||||
sc.context.OrgRole = role
|
sc.context.OrgRole = role
|
||||||
|
|
||||||
return hs.DeleteAnnotations(c)
|
return hs.MassDeleteAnnotations(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
fakeAnnoRepo = NewFakeAnnotationsRepo()
|
||||||
annotations.SetRepository(fakeAnnoRepo)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
|
|
||||||
sc.m.Post(routePattern, sc.defaultHandler)
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
@ -396,12 +419,13 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
|
|||||||
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
|
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dashboardAnnotation := annotations.ItemDTO{Id: 1, DashboardId: 1}
|
dashboardAnnotation := &annotations.Item{Id: 1, DashboardId: 1}
|
||||||
organizationAnnotation := annotations.ItemDTO{Id: 2, DashboardId: 0}
|
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)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
|
|
||||||
postOrganizationCmd := dtos.PostAnnotationsCmd{
|
postOrganizationCmd := dtos.PostAnnotationsCmd{
|
||||||
@ -693,8 +717,9 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
setUpACL()
|
setUpFGACGuardian(t)
|
||||||
sc.acmock.RegisterAttributeScopeResolver(AnnotationTypeScopeResolver())
|
sc.acmock.
|
||||||
|
RegisterAttributeScopeResolver(AnnotationTypeScopeResolver())
|
||||||
setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgId)
|
setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgId)
|
||||||
|
|
||||||
r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t)
|
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}
|
dashboardAnnotation := annotations.Item{Id: 1, DashboardId: 1}
|
||||||
organizationAnnotation := annotations.ItemDTO{Id: 2}
|
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)
|
annotations.SetRepository(fakeAnnoRepo)
|
||||||
|
|
||||||
prefix, resolver := AnnotationTypeScopeResolver()
|
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() {
|
func setUpACL() {
|
||||||
viewerRole := models.ROLE_VIEWER
|
viewerRole := models.ROLE_VIEWER
|
||||||
editorRole := models.ROLE_EDITOR
|
editorRole := models.ROLE_EDITOR
|
||||||
@ -776,3 +941,12 @@ func setUpACL() {
|
|||||||
store.ExpectedTeamsByUser = []*models.TeamDTO{}
|
store.ExpectedTeamsByUser = []*models.TeamDTO{}
|
||||||
guardian.InitLegacyGuardian(store)
|
guardian.InitLegacyGuardian(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setUpFGACGuardian(t *testing.T) {
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
t.Cleanup(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
})
|
||||||
|
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true})
|
||||||
|
}
|
||||||
|
@ -437,7 +437,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
apiRoute.Get("/annotations", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead, ac.ScopeAnnotationsAll)), routing.Wrap(hs.GetAnnotations))
|
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) {
|
apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) {
|
||||||
annotationsRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsCreate)), routing.Wrap(hs.PostAnnotation))
|
annotationsRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsCreate)), routing.Wrap(hs.PostAnnotation))
|
||||||
|
@ -178,7 +178,7 @@ type GetAnnotationTagssParams struct {
|
|||||||
type MassDeleteAnnotationsParams struct {
|
type MassDeleteAnnotationsParams struct {
|
||||||
// in:body
|
// in:body
|
||||||
// required:true
|
// required:true
|
||||||
Body dtos.DeleteAnnotationsCmd `json:"body"`
|
Body dtos.MassDeleteAnnotationsCmd `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:parameters createAnnotation
|
// swagger:parameters createAnnotation
|
||||||
|
@ -28,8 +28,7 @@ type PatchAnnotationsCmd struct {
|
|||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteAnnotationsCmd struct {
|
type MassDeleteAnnotationsCmd struct {
|
||||||
AlertId int64 `json:"alertId"`
|
|
||||||
DashboardId int64 `json:"dashboardId"`
|
DashboardId int64 `json:"dashboardId"`
|
||||||
PanelId int64 `json:"panelId"`
|
PanelId int64 `json:"panelId"`
|
||||||
AnnotationId int64 `json:"annotationId"`
|
AnnotationId int64 `json:"annotationId"`
|
||||||
|
@ -75,7 +75,6 @@ type GetAnnotationTagsResponse struct {
|
|||||||
type DeleteParams struct {
|
type DeleteParams struct {
|
||||||
OrgId int64
|
OrgId int64
|
||||||
Id int64
|
Id int64
|
||||||
AlertId int64
|
|
||||||
DashboardId int64
|
DashboardId int64
|
||||||
PanelId int64
|
PanelId int64
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user