dashfolders: permissions for saving annotations

ref #10275 Use folder permissions instead of hard coded
permissions on the annotations routes.
This commit is contained in:
Daniel Lee
2017-12-21 00:52:21 +01:00
parent eef37c4071
commit 3ae1bf0c16
8 changed files with 399 additions and 13 deletions

View File

@@ -7,7 +7,9 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
)
@@ -51,6 +53,10 @@ func (e *CreateAnnotationError) Error() string {
}
func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
if canSave, err := canSaveByDashboardId(c, cmd.DashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
repo := annotations.GetRepository()
if cmd.Text == "" {
@@ -178,6 +184,10 @@ func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Resp
repo := annotations.GetRepository()
if resp := canSave(c, repo, annotationId); resp != nil {
return resp
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
@@ -228,6 +238,10 @@ func DeleteAnnotationById(c *middleware.Context) Response {
repo := annotations.GetRepository()
annotationId := c.ParamsInt64(":annotationId")
if resp := canSave(c, repo, annotationId); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{
Id: annotationId,
})
@@ -243,6 +257,10 @@ func DeleteAnnotationRegion(c *middleware.Context) Response {
repo := annotations.GetRepository()
regionId := c.ParamsInt64(":regionId")
if resp := canSave(c, repo, regionId); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{
RegionId: regionId,
})
@@ -253,3 +271,50 @@ func DeleteAnnotationRegion(c *middleware.Context) Response {
return ApiSuccess("Annotation region deleted")
}
func canSaveByDashboardId(c *middleware.Context, dashboardId int64) (bool, error) {
if dashboardId == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) {
return false, nil
}
if dashboardId > 0 {
guardian := guardian.NewDashboardGuardian(dashboardId, c.OrgId, c.SignedInUser)
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
return false, err
}
}
return true, nil
}
func canSave(c *middleware.Context, repo annotations.Repository, annotationId int64) Response {
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationId, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return ApiError(500, "Could not find annotation to update", err)
}
dashboardId := items[0].DashboardId
if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
return nil
}
func canSaveByRegionId(c *middleware.Context, repo annotations.Repository, regionId int64) Response {
items, err := repo.Find(&annotations.ItemQuery{RegionId: regionId, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return ApiError(500, "Could not find annotation to update", err)
}
dashboardId := items[0].DashboardId
if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
return nil
}

271
pkg/api/annotations_test.go Normal file
View File

@@ -0,0 +1,271 @@
package api
import (
"path/filepath"
"testing"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
macaron "gopkg.in/macaron.v1"
. "github.com/smartystreets/goconvey/convey"
)
func TestAnnotationsApiEndpoint(t *testing.T) {
Convey("Given an annotation without a dashboard id", t, func() {
cmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
}
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
Convey("Should not be allowed to save an annotation", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
Convey("Should be able to save an annotation", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
Convey("Given an annotation with a dashboard id and the dashboard does not have an acl", t, func() {
cmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
DashboardId: 1,
PanelId: 1,
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
Id: 1,
}
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
return nil
})
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
Convey("Should not be allowed to save an annotation", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
Convey("Should be able to save an annotation", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
}
type fakeAnnotationsRepo struct {
}
func (repo *fakeAnnotationsRepo) Delete(params *annotations.DeleteParams) error {
return nil
}
func (repo *fakeAnnotationsRepo) Save(item *annotations.Item) error {
item.Id = 1
return nil
}
func (repo *fakeAnnotationsRepo) Update(item *annotations.Item) error {
return nil
}
func (repo *fakeAnnotationsRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
annotations := []*annotations.ItemDTO{&annotations.ItemDTO{Id: 1}}
return annotations, nil
}
var fakeAnnoRepo *fakeAnnotationsRepo
func postAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PostAnnotationsCmd, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return PostAnnotation(c, cmd)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func putAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return UpdateAnnotation(c, cmd)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.m.Put(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@@ -317,8 +317,8 @@ func (hs *HttpServer) registerRoutes() {
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
annotationsRoute.Post("/graphite", bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
}, reqEditorRole)
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
})
// error test
r.Get("/metrics/error", wrap(GenerateError))