|
|
|
|
@ -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}) |
|
|
|
|
} |
|
|
|
|
|