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:
Ezequiel Victorero
2022-03-23 18:39:00 -03:00
committed by GitHub
parent 60d4cd80bf
commit c5f295b5b3
7 changed files with 284 additions and 52 deletions
+201 -27
View File
@@ -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})
}