Browse Source

dashfolders: permissions for saving annotations

ref #10275 Use folder permissions instead of hard coded
permissions on the annotations routes.
Daniel Lee 8 years ago
parent
commit
3ae1bf0c16

+ 65 - 0
pkg/api/annotations.go

@@ -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 - 0
pkg/api/annotations_test.go

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

+ 2 - 2
pkg/api/api.go

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

+ 10 - 8
pkg/services/annotations/annotations.go

@@ -10,14 +10,16 @@ type Repository interface {
 }
 
 type ItemQuery struct {
-	OrgId       int64    `json:"orgId"`
-	From        int64    `json:"from"`
-	To          int64    `json:"to"`
-	AlertId     int64    `json:"alertId"`
-	DashboardId int64    `json:"dashboardId"`
-	PanelId     int64    `json:"panelId"`
-	Tags        []string `json:"tags"`
-	Type        string   `json:"type"`
+	OrgId        int64    `json:"orgId"`
+	From         int64    `json:"from"`
+	To           int64    `json:"to"`
+	AlertId      int64    `json:"alertId"`
+	DashboardId  int64    `json:"dashboardId"`
+	PanelId      int64    `json:"panelId"`
+	AnnotationId int64    `json:"annotationId"`
+	RegionId     int64    `json:"regionId"`
+	Tags         []string `json:"tags"`
+	Type         string   `json:"type"`
 
 	Limit int64 `json:"limit"`
 }

+ 12 - 0
pkg/services/sqlstore/annotation.go

@@ -138,6 +138,17 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
 	sql.WriteString(`WHERE annotation.org_id = ?`)
 	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 {
 		sql.WriteString(` AND annotation.alert_id = ?`)
 		params = append(params, query.AlertId)
@@ -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))
 
 	items := make([]*annotations.ItemDTO, 0)
+
 	if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
 		return nil, err
 	}

+ 36 - 0
pkg/services/sqlstore/annotation_test.go

@@ -51,6 +51,20 @@ func TestAnnotations(t *testing.T) {
 			So(err, ShouldBeNil)
 			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() {
 				items, err := repo.Find(&annotations.ItemQuery{
 					OrgId:       1,
@@ -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() {
 				items, err := repo.Find(&annotations.ItemQuery{
 					OrgId:       1,

+ 1 - 1
public/app/features/annotations/annotation_tooltip.ts

@@ -54,7 +54,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
       `;
 
       // Show edit icon only for users with at least Editor role
-      if (event.id && contextSrv.isEditor) {
+      if (event.id && dashboard.meta.canEdit) {
         header += `
           <span class="pointer graph-annotation__edit-icon" ng-click="onEdit()">
             <i class="fa fa-pencil-square"></i>

+ 2 - 2
public/app/plugins/panel/graph/graph.ts

@@ -666,7 +666,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
           return;
         }
 
-        if ((ranges.ctrlKey || ranges.metaKey) && contextSrv.isEditor) {
+        if ((ranges.ctrlKey || ranges.metaKey) && dashboard.meta.canEdit) {
           // Add annotation
           setTimeout(() => {
             eventManager.updateTime(ranges.xaxis);
@@ -687,7 +687,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
           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)
           let isRangeSelection = pos.x !== pos.x1;
           if (!isRangeSelection) {