Commit 3ae1bf0c by Daniel Lee

dashfolders: permissions for saving annotations

ref #10275 Use folder permissions instead of hard coded
permissions on the annotations routes.
parent eef37c40
...@@ -7,7 +7,9 @@ import ( ...@@ -7,7 +7,9 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware" "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/annotations"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
...@@ -51,6 +53,10 @@ func (e *CreateAnnotationError) Error() string { ...@@ -51,6 +53,10 @@ func (e *CreateAnnotationError) Error() string {
} }
func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response { 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() repo := annotations.GetRepository()
if cmd.Text == "" { if cmd.Text == "" {
...@@ -178,6 +184,10 @@ func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Resp ...@@ -178,6 +184,10 @@ func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Resp
repo := annotations.GetRepository() repo := annotations.GetRepository()
if resp := canSave(c, repo, annotationId); resp != nil {
return resp
}
item := annotations.Item{ item := annotations.Item{
OrgId: c.OrgId, OrgId: c.OrgId,
UserId: c.UserId, UserId: c.UserId,
...@@ -228,6 +238,10 @@ func DeleteAnnotationById(c *middleware.Context) Response { ...@@ -228,6 +238,10 @@ func DeleteAnnotationById(c *middleware.Context) Response {
repo := annotations.GetRepository() repo := annotations.GetRepository()
annotationId := c.ParamsInt64(":annotationId") annotationId := c.ParamsInt64(":annotationId")
if resp := canSave(c, repo, annotationId); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{ err := repo.Delete(&annotations.DeleteParams{
Id: annotationId, Id: annotationId,
}) })
...@@ -243,6 +257,10 @@ func DeleteAnnotationRegion(c *middleware.Context) Response { ...@@ -243,6 +257,10 @@ func DeleteAnnotationRegion(c *middleware.Context) Response {
repo := annotations.GetRepository() repo := annotations.GetRepository()
regionId := c.ParamsInt64(":regionId") regionId := c.ParamsInt64(":regionId")
if resp := canSave(c, repo, regionId); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{ err := repo.Delete(&annotations.DeleteParams{
RegionId: regionId, RegionId: regionId,
}) })
...@@ -253,3 +271,50 @@ func DeleteAnnotationRegion(c *middleware.Context) Response { ...@@ -253,3 +271,50 @@ func DeleteAnnotationRegion(c *middleware.Context) Response {
return ApiSuccess("Annotation region deleted") 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
}
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)
})
}
...@@ -317,8 +317,8 @@ func (hs *HttpServer) registerRoutes() { ...@@ -317,8 +317,8 @@ func (hs *HttpServer) registerRoutes() {
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById)) annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation)) annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion)) annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
annotationsRoute.Post("/graphite", bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation)) annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
}, reqEditorRole) })
// error test // error test
r.Get("/metrics/error", wrap(GenerateError)) r.Get("/metrics/error", wrap(GenerateError))
......
...@@ -10,14 +10,16 @@ type Repository interface { ...@@ -10,14 +10,16 @@ type Repository interface {
} }
type ItemQuery struct { type ItemQuery struct {
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
From int64 `json:"from"` From int64 `json:"from"`
To int64 `json:"to"` To int64 `json:"to"`
AlertId int64 `json:"alertId"` AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"` PanelId int64 `json:"panelId"`
Tags []string `json:"tags"` AnnotationId int64 `json:"annotationId"`
Type string `json:"type"` RegionId int64 `json:"regionId"`
Tags []string `json:"tags"`
Type string `json:"type"`
Limit int64 `json:"limit"` Limit int64 `json:"limit"`
} }
......
...@@ -138,6 +138,17 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I ...@@ -138,6 +138,17 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
sql.WriteString(`WHERE annotation.org_id = ?`) sql.WriteString(`WHERE annotation.org_id = ?`)
params = append(params, query.OrgId) params = append(params, query.OrgId)
if query.AnnotationId != 0 {
fmt.Print("annotation query")
sql.WriteString(` AND annotation.id = ?`)
params = append(params, query.AnnotationId)
}
if query.RegionId != 0 {
sql.WriteString(` AND annotation.region_id = ?`)
params = append(params, query.RegionId)
}
if query.AlertId != 0 { if query.AlertId != 0 {
sql.WriteString(` AND annotation.alert_id = ?`) sql.WriteString(` AND annotation.alert_id = ?`)
params = append(params, query.AlertId) params = append(params, query.AlertId)
...@@ -197,6 +208,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I ...@@ -197,6 +208,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit)) sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
items := make([]*annotations.ItemDTO, 0) items := make([]*annotations.ItemDTO, 0)
if err := x.Sql(sql.String(), params...).Find(&items); err != nil { if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
return nil, err return nil, err
} }
......
...@@ -51,6 +51,20 @@ func TestAnnotations(t *testing.T) { ...@@ -51,6 +51,20 @@ func TestAnnotations(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(annotation.Id, ShouldBeGreaterThan, 0) So(annotation.Id, ShouldBeGreaterThan, 0)
annotation2 := &annotations.Item{
OrgId: 1,
UserId: 1,
DashboardId: 2,
Text: "hello",
Type: "alert",
Epoch: 20,
Tags: []string{"outage", "error", "type:outage", "server:server-1"},
RegionId: 1,
}
err = repo.Save(annotation2)
So(err, ShouldBeNil)
So(annotation2.Id, ShouldBeGreaterThan, 0)
Convey("Can query for annotation", func() { Convey("Can query for annotation", func() {
items, err := repo.Find(&annotations.ItemQuery{ items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1, OrgId: 1,
...@@ -67,6 +81,28 @@ func TestAnnotations(t *testing.T) { ...@@ -67,6 +81,28 @@ func TestAnnotations(t *testing.T) {
}) })
}) })
Convey("Can query for annotation by id", func() {
items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1,
AnnotationId: annotation2.Id,
})
So(err, ShouldBeNil)
So(items, ShouldHaveLength, 1)
So(items[0].Id, ShouldEqual, annotation2.Id)
})
Convey("Can query for annotation by region id", func() {
items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1,
RegionId: annotation2.RegionId,
})
So(err, ShouldBeNil)
So(items, ShouldHaveLength, 1)
So(items[0].Id, ShouldEqual, annotation2.Id)
})
Convey("Should not find any when item is outside time range", func() { Convey("Should not find any when item is outside time range", func() {
items, err := repo.Find(&annotations.ItemQuery{ items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1, OrgId: 1,
......
...@@ -54,7 +54,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, ...@@ -54,7 +54,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
`; `;
// Show edit icon only for users with at least Editor role // Show edit icon only for users with at least Editor role
if (event.id && contextSrv.isEditor) { if (event.id && dashboard.meta.canEdit) {
header += ` header += `
<span class="pointer graph-annotation__edit-icon" ng-click="onEdit()"> <span class="pointer graph-annotation__edit-icon" ng-click="onEdit()">
<i class="fa fa-pencil-square"></i> <i class="fa fa-pencil-square"></i>
......
...@@ -666,7 +666,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { ...@@ -666,7 +666,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
return; return;
} }
if ((ranges.ctrlKey || ranges.metaKey) && contextSrv.isEditor) { if ((ranges.ctrlKey || ranges.metaKey) && dashboard.meta.canEdit) {
// Add annotation // Add annotation
setTimeout(() => { setTimeout(() => {
eventManager.updateTime(ranges.xaxis); eventManager.updateTime(ranges.xaxis);
...@@ -687,7 +687,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { ...@@ -687,7 +687,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
return; return;
} }
if ((pos.ctrlKey || pos.metaKey) && contextSrv.isEditor) { if ((pos.ctrlKey || pos.metaKey) && dashboard.meta.canEdit) {
// Skip if range selected (added in "plotselected" event handler) // Skip if range selected (added in "plotselected" event handler)
let isRangeSelection = pos.x !== pos.x1; let isRangeSelection = pos.x !== pos.x1;
if (!isRangeSelection) { if (!isRangeSelection) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment