Merge branch '7883_new_url_structure' into 7883_frontend_step2
This commit is contained in:
+53
-24
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
)
|
||||
|
||||
func ValidateOrgAlert(c *middleware.Context) {
|
||||
@@ -62,9 +63,22 @@ func GetAlerts(c *middleware.Context) Response {
|
||||
return ApiError(500, "List alerts failed", err)
|
||||
}
|
||||
|
||||
alertDTOs, resp := transformToDTOs(query.Result, c)
|
||||
if resp != nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
return Json(200, alertDTOs)
|
||||
}
|
||||
|
||||
func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.AlertRule, Response) {
|
||||
if len(alerts) == 0 {
|
||||
return []*dtos.AlertRule{}, nil
|
||||
}
|
||||
|
||||
dashboardIds := make([]int64, 0)
|
||||
alertDTOs := make([]*dtos.AlertRule, 0)
|
||||
for _, alert := range query.Result {
|
||||
for _, alert := range alerts {
|
||||
dashboardIds = append(dashboardIds, alert.DashboardId)
|
||||
alertDTOs = append(alertDTOs, &dtos.AlertRule{
|
||||
Id: alert.Id,
|
||||
@@ -83,10 +97,8 @@ func GetAlerts(c *middleware.Context) Response {
|
||||
DashboardIds: dashboardIds,
|
||||
}
|
||||
|
||||
if len(alertDTOs) > 0 {
|
||||
if err := bus.Dispatch(&dashboardsQuery); err != nil {
|
||||
return ApiError(500, "List alerts failed", err)
|
||||
}
|
||||
if err := bus.Dispatch(&dashboardsQuery); err != nil {
|
||||
return nil, ApiError(500, "List alerts failed", err)
|
||||
}
|
||||
|
||||
//TODO: should be possible to speed this up with lookup table
|
||||
@@ -98,7 +110,26 @@ func GetAlerts(c *middleware.Context) Response {
|
||||
}
|
||||
}
|
||||
|
||||
return Json(200, alertDTOs)
|
||||
permissionsQuery := models.GetDashboardPermissionsForUserQuery{
|
||||
DashboardIds: dashboardIds,
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.SignedInUser.UserId,
|
||||
OrgRole: c.SignedInUser.OrgRole,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&permissionsQuery); err != nil {
|
||||
return nil, ApiError(500, "List alerts failed", err)
|
||||
}
|
||||
|
||||
for _, alert := range alertDTOs {
|
||||
for _, perm := range permissionsQuery.Result {
|
||||
if alert.DashboardId == perm.DashboardId {
|
||||
alert.CanEdit = perm.Permission > 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alertDTOs, nil
|
||||
}
|
||||
|
||||
// POST /api/alerts/test
|
||||
@@ -155,24 +186,6 @@ func GetAlert(c *middleware.Context) Response {
|
||||
return Json(200, &query.Result)
|
||||
}
|
||||
|
||||
// DEL /api/alerts/:id
|
||||
func DelAlert(c *middleware.Context) Response {
|
||||
alertId := c.ParamsInt64(":alertId")
|
||||
|
||||
if alertId == 0 {
|
||||
return ApiError(401, "Failed to parse alertid", nil)
|
||||
}
|
||||
|
||||
cmd := models.DeleteAlertCommand{AlertId: alertId}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to delete alert", err)
|
||||
}
|
||||
|
||||
var resp = map[string]interface{}{"alertId": alertId}
|
||||
return Json(200, resp)
|
||||
}
|
||||
|
||||
func GetAlertNotifiers(c *middleware.Context) Response {
|
||||
return Json(200, alerting.GetNotifiers())
|
||||
}
|
||||
@@ -267,6 +280,22 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R
|
||||
//POST /api/alerts/:alertId/pause
|
||||
func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
|
||||
alertId := c.ParamsInt64("alertId")
|
||||
|
||||
query := models.GetAlertByIdQuery{Id: alertId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Get Alert failed", err)
|
||||
}
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(query.Result.DashboardId, c.OrgId, c.SignedInUser)
|
||||
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking permissions for Alert", err)
|
||||
}
|
||||
|
||||
return ApiError(403, "Access denied to this dashboard and alert", nil)
|
||||
}
|
||||
|
||||
cmd := models.PauseAlertCommand{
|
||||
OrgId: c.OrgId,
|
||||
AlertIds: []int64{alertId},
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAlertingApiEndpoint(t *testing.T) {
|
||||
Convey("Given an alert in a dashboard with an acl", t, func() {
|
||||
|
||||
singleAlert := &m.Alert{Id: 1, DashboardId: 1, Name: "singlealert"}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetAlertByIdQuery) error {
|
||||
query.Result = singleAlert
|
||||
return nil
|
||||
})
|
||||
|
||||
viewerRole := m.ROLE_VIEWER
|
||||
editorRole := m.ROLE_EDITOR
|
||||
|
||||
aclMockResp := []*m.DashboardAclInfoDTO{}
|
||||
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 editor and not in the ACL", func() {
|
||||
Convey("Should not be able to pause the alert", func() {
|
||||
cmd := dtos.PauseAlertCommand{
|
||||
AlertId: 1,
|
||||
Paused: true,
|
||||
}
|
||||
postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
|
||||
CallPauseAlert(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is editor and dashboard has default ACL", func() {
|
||||
aclMockResp = []*m.DashboardAclInfoDTO{
|
||||
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
|
||||
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
|
||||
Convey("Should be able to pause the alert", func() {
|
||||
cmd := dtos.PauseAlertCommand{
|
||||
AlertId: 1,
|
||||
Paused: true,
|
||||
}
|
||||
postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
|
||||
CallPauseAlert(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func CallPauseAlert(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(cmd *m.PauseAlertCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func postAlertScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PauseAlertCommand, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := setupScenarioContext(url)
|
||||
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
sc.context.OrgId = TestOrgID
|
||||
sc.context.OrgRole = role
|
||||
|
||||
return PauseAlert(c, cmd)
|
||||
})
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
|
||||
. "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{{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 := setupScenarioContext(url)
|
||||
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 := setupScenarioContext(url)
|
||||
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)
|
||||
})
|
||||
}
|
||||
+14
-12
@@ -150,13 +150,13 @@ func (hs *HttpServer) registerRoutes() {
|
||||
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||
teamsRoute.Get("/:teamId", wrap(GetTeamById))
|
||||
teamsRoute.Get("/search", wrap(SearchTeams))
|
||||
teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
||||
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
||||
teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
|
||||
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
|
||||
teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
||||
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
|
||||
}, reqOrgAdmin)
|
||||
teamsRoute.Post("/", quota("teams"), reqOrgAdmin, bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
||||
teamsRoute.Put("/:teamId", reqOrgAdmin, bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
||||
teamsRoute.Delete("/:teamId", reqOrgAdmin, wrap(DeleteTeamById))
|
||||
teamsRoute.Get("/:teamId/members", reqOrgAdmin, wrap(GetTeamMembers))
|
||||
teamsRoute.Post("/:teamId/members", reqOrgAdmin, quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
||||
teamsRoute.Delete("/:teamId/members/:userId", reqOrgAdmin, wrap(RemoveTeamMember))
|
||||
})
|
||||
|
||||
// org information available to all users.
|
||||
apiRoute.Group("/org", func(orgRoute RouteRegister) {
|
||||
@@ -252,19 +252,21 @@ func (hs *HttpServer) registerRoutes() {
|
||||
dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid))
|
||||
|
||||
dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
|
||||
dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard))
|
||||
dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
|
||||
|
||||
dashboardRoute.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
|
||||
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
|
||||
dashboardRoute.Get("/home", wrap(GetHomeDashboard))
|
||||
dashboardRoute.Get("/tags", GetDashboardTags)
|
||||
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
|
||||
|
||||
dashboardRoute.Get("/folders", wrap(GetFoldersForSignedInUser))
|
||||
|
||||
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
|
||||
dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
|
||||
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
|
||||
dashIdRoute.Post("/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
|
||||
dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
|
||||
aclRoute.Get("/", wrap(GetDashboardAclList))
|
||||
@@ -326,8 +328,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))
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
||||
loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn)
|
||||
}
|
||||
|
||||
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := setupScenarioContext(url)
|
||||
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
sc.context.OrgId = TestOrgID
|
||||
sc.context.OrgRole = role
|
||||
if sc.handlerFunc != nil {
|
||||
return sc.handlerFunc(sc.context)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
switch method {
|
||||
case "GET":
|
||||
sc.m.Get(routePattern, sc.defaultHandler)
|
||||
case "DELETE":
|
||||
sc.m.Delete(routePattern, sc.defaultHandler)
|
||||
}
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range queryParams {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
m *macaron.Macaron
|
||||
context *middleware.Context
|
||||
resp *httptest.ResponseRecorder
|
||||
handlerFunc handlerFunc
|
||||
defaultHandler macaron.Handler
|
||||
req *http.Request
|
||||
url string
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) exec() {
|
||||
sc.m.ServeHTTP(sc.resp, sc.req)
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
type handlerFunc func(c *middleware.Context) Response
|
||||
|
||||
func setupScenarioContext(url string) *scenarioContext {
|
||||
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{}))
|
||||
|
||||
return sc
|
||||
}
|
||||
+31
-18
@@ -38,9 +38,9 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
|
||||
func dashboardGuardianResponse(err error) Response {
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking dashboard permissions", err)
|
||||
} else {
|
||||
return ApiError(403, "Access denied to this dashboard", nil)
|
||||
}
|
||||
|
||||
return ApiError(403, "Access denied to this dashboard", nil)
|
||||
}
|
||||
|
||||
func GetDashboard(c *middleware.Context) Response {
|
||||
@@ -51,7 +51,6 @@ func GetDashboard(c *middleware.Context) Response {
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||
if canView, err := guardian.CanView(); err != nil || !canView {
|
||||
fmt.Printf("%v", err)
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
@@ -90,6 +89,7 @@ func GetDashboard(c *middleware.Context) Response {
|
||||
IsFolder: dash.IsFolder,
|
||||
FolderId: dash.FolderId,
|
||||
FolderTitle: "Root",
|
||||
Url: dash.GetUrl(),
|
||||
}
|
||||
|
||||
// lookup folder title
|
||||
@@ -99,12 +99,7 @@ func GetDashboard(c *middleware.Context) Response {
|
||||
return ApiError(500, "Dashboard folder could not be read", err)
|
||||
}
|
||||
meta.FolderTitle = query.Result.Title
|
||||
}
|
||||
|
||||
if dash.IsFolder {
|
||||
meta.Url = m.GetFolderUrl(dash.Uid, dash.Slug)
|
||||
} else {
|
||||
meta.Url = m.GetDashboardUrl(dash.Uid, dash.Slug)
|
||||
meta.FolderSlug = query.Result.Slug
|
||||
}
|
||||
|
||||
// make sure db version is in sync with json model version
|
||||
@@ -201,7 +196,14 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||
dashId := dash.Id
|
||||
|
||||
// if new dashboard, use parent folder permissions instead
|
||||
if dashId == 0 {
|
||||
dashId = cmd.FolderId
|
||||
}
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
@@ -244,7 +246,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == m.ErrDashboardWithSameNameExists {
|
||||
if err == m.ErrDashboardWithSameUIDExists {
|
||||
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||
}
|
||||
if err == m.ErrDashboardVersionMismatch {
|
||||
@@ -268,12 +270,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
}
|
||||
|
||||
var url string
|
||||
if dash.IsFolder {
|
||||
url = m.GetFolderUrl(dashboard.Uid, dashboard.Slug)
|
||||
} else {
|
||||
url = m.GetDashboardUrl(dashboard.Uid, dashboard.Slug)
|
||||
}
|
||||
dashboard.IsFolder = dash.IsFolder
|
||||
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||
return Json(200, util.DynMap{
|
||||
@@ -282,7 +279,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
"version": dashboard.Version,
|
||||
"id": dashboard.Id,
|
||||
"uid": dashboard.Uid,
|
||||
"url": url,
|
||||
"url": dashboard.GetUrl(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -489,3 +486,19 @@ func GetDashboardTags(c *middleware.Context) {
|
||||
|
||||
c.JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func GetFoldersForSignedInUser(c *middleware.Context) Response {
|
||||
title := c.Query("query")
|
||||
query := m.GetFoldersForSignedInUserQuery{
|
||||
OrgId: c.OrgId,
|
||||
SignedInUser: c.SignedInUser,
|
||||
Title: title,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&query)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get folders from database", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,14 @@ func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCom
|
||||
})
|
||||
}
|
||||
|
||||
if okToUpdate, err := guardian.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking dashboard permissions", err)
|
||||
}
|
||||
|
||||
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
|
||||
return ApiError(409, err.Error(), err)
|
||||
@@ -70,6 +78,14 @@ func DeleteDashboardAcl(c *middleware.Context) Response {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
if okToDelete, err := guardian.CheckPermissionBeforeRemove(m.PERMISSION_ADMIN, aclId); err != nil || !okToDelete {
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking dashboard permissions", err)
|
||||
}
|
||||
|
||||
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
|
||||
}
|
||||
|
||||
cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to delete permission for user", err)
|
||||
|
||||
@@ -3,8 +3,10 @@ package api
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@@ -37,6 +39,12 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
// This tests four scenarios:
|
||||
// 1. user is an org admin
|
||||
// 2. user is an org editor AND has been granted admin permission for the dashboard
|
||||
// 3. user is an org viewer AND has been granted edit permission for the dashboard
|
||||
// 4. user is an org editor AND has no permissions for the dashboard
|
||||
|
||||
Convey("When user is org admin", func() {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||
Convey("Should be able to access ACL", func() {
|
||||
@@ -54,9 +62,9 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is editor and has admin permission in the ACL", func() {
|
||||
Convey("When user is org editor and has admin permission in the ACL", func() {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||
|
||||
Convey("Should be able to access ACL", func() {
|
||||
sc.handlerFunc = GetDashboardAclList
|
||||
@@ -67,7 +75,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||
|
||||
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
|
||||
return nil
|
||||
@@ -81,6 +89,52 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||
|
||||
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("Should not be able to delete their own Admin permission", func() {
|
||||
sc.handlerFunc = DeleteDashboardAcl
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should not be able to downgrade their own Admin permission", func() {
|
||||
cmd := dtos.UpdateDashboardAclCommand{
|
||||
Items: []dtos.DashboardAclUpdateItem{
|
||||
{UserId: TestUserID, Permission: m.PERMISSION_EDIT},
|
||||
},
|
||||
}
|
||||
|
||||
postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||
|
||||
CallPostAcl(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should be able to update permissions", func() {
|
||||
cmd := dtos.UpdateDashboardAclCommand{
|
||||
Items: []dtos.DashboardAclUpdateItem{
|
||||
{UserId: TestUserID, Permission: m.PERMISSION_ADMIN},
|
||||
{UserId: 2, Permission: m.PERMISSION_EDIT},
|
||||
},
|
||||
}
|
||||
|
||||
postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||
|
||||
CallPostAcl(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is a member of a team in the ACL with admin permission", func() {
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"})
|
||||
@@ -99,11 +153,12 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is editor and has edit permission in the ACL", func() {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
Convey("When user is org viewer and has edit permission in the ACL", func() {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_VIEWER, func(sc *scenarioContext) {
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
|
||||
|
||||
Convey("Should not be able to access ACL", func() {
|
||||
// Getting the permissions is an Admin permission
|
||||
Convey("Should not be able to get list of permissions from ACL", func() {
|
||||
sc.handlerFunc = GetDashboardAclList
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
@@ -111,7 +166,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_VIEWER, func(sc *scenarioContext) {
|
||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
|
||||
|
||||
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
|
||||
@@ -127,7 +182,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is editor and not in the ACL", func() {
|
||||
Convey("When user is org editor and not in the ACL", func() {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
|
||||
Convey("Should not be able to access ACL", func() {
|
||||
@@ -172,3 +227,32 @@ func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardA
|
||||
|
||||
return dtos
|
||||
}
|
||||
|
||||
func CallPostAcl(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func postAclScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := setupScenarioContext(url)
|
||||
|
||||
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
sc.context.OrgId = TestOrgID
|
||||
sc.context.OrgRole = role
|
||||
|
||||
return UpdateDashboardAcl(c, cmd)
|
||||
})
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
+17
-18
@@ -2,12 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -32,6 +28,10 @@ func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem)
|
||||
|
||||
var fakeRepo *fakeDashboardRepo
|
||||
|
||||
// This tests two main scenarios. If a user has access to execute an action on a dashboard:
|
||||
// 1. and the dashboard is in a folder which does not have an acl
|
||||
// 2. and the dashboard is in a folder which does have an acl
|
||||
|
||||
func TestDashboardApiEndpoint(t *testing.T) {
|
||||
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
|
||||
fakeDash := m.NewDashboard("Child dash")
|
||||
@@ -79,6 +79,10 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
}),
|
||||
}
|
||||
|
||||
// This tests two scenarios:
|
||||
// 1. user is an org viewer
|
||||
// 2. user is an org editor
|
||||
|
||||
Convey("When user is an Org Viewer", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
@@ -279,6 +283,14 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
}),
|
||||
}
|
||||
|
||||
// This tests six scenarios:
|
||||
// 1. user is an org viewer AND has no permissions for this dashboard
|
||||
// 2. user is an org editor AND has no permissions for this dashboard
|
||||
// 3. user is an org viewer AND has been granted edit permission for the dashboard
|
||||
// 4. user is an org viewer AND all viewers have edit permission for this dashboard
|
||||
// 5. user is an org viewer AND has been granted an admin permission
|
||||
// 6. user is an org editor AND has been granted a view permission
|
||||
|
||||
Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
@@ -800,20 +812,7 @@ func postDashboardScenario(desc string, url string, routePattern string, role m.
|
||||
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 := setupScenarioContext(url)
|
||||
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
|
||||
@@ -2,17 +2,11 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@@ -54,88 +48,3 @@ func TestDataSourcesProxy(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
||||
loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn)
|
||||
}
|
||||
|
||||
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, 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
|
||||
if sc.handlerFunc != nil {
|
||||
return sc.handlerFunc(sc.context)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
switch method {
|
||||
case "GET":
|
||||
sc.m.Get(routePattern, sc.defaultHandler)
|
||||
case "DELETE":
|
||||
sc.m.Delete(routePattern, sc.defaultHandler)
|
||||
}
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range queryParams {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
m *macaron.Macaron
|
||||
context *middleware.Context
|
||||
resp *httptest.ResponseRecorder
|
||||
handlerFunc handlerFunc
|
||||
defaultHandler macaron.Handler
|
||||
req *http.Request
|
||||
url string
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) exec() {
|
||||
sc.m.ServeHTTP(sc.resp, sc.req)
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
type handlerFunc func(c *middleware.Context) Response
|
||||
|
||||
@@ -20,6 +20,7 @@ type AlertRule struct {
|
||||
EvalData *simplejson.Json `json:"evalData"`
|
||||
ExecutionError string `json:"executionError"`
|
||||
DashbboardUri string `json:"dashboardUri"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
}
|
||||
|
||||
type AlertNotification struct {
|
||||
|
||||
@@ -27,6 +27,7 @@ type DashboardMeta struct {
|
||||
IsFolder bool `json:"isFolder"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderSlug string `json:"folderSlug"`
|
||||
}
|
||||
|
||||
type DashboardFullWithMeta struct {
|
||||
|
||||
+3
-3
@@ -102,8 +102,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
}
|
||||
|
||||
dashboardChildNavs := []*dtos.NavLink{
|
||||
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
|
||||
{Divider: true, HideFromTabs: true},
|
||||
{Text: "Home", Id: "home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
|
||||
{Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true},
|
||||
{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"},
|
||||
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"},
|
||||
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"},
|
||||
@@ -261,7 +261,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
|
||||
if c.IsGrafanaAdmin {
|
||||
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||
Divider: true, HideFromTabs: true,
|
||||
Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
|
||||
})
|
||||
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||
Text: "Server Admin",
|
||||
|
||||
Reference in New Issue
Block a user