Browse Source

WIP: add permission check for GetDashboard

Daniel Lee 8 years ago
parent
commit
3913f16550

+ 1 - 1
pkg/api/api.go

@@ -234,7 +234,7 @@ func (hs *HttpServer) registerRoutes() {
 
 		// Dashboard
 		r.Group("/dashboards", func() {
-			r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
+			r.Combo("/db/:slug").Get(wrap(GetDashboard)).Delete(DeleteDashboard)
 
 			r.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
 			r.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))

+ 37 - 11
pkg/api/dashboard.go

@@ -17,6 +17,7 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/services/alerting"
+	"github.com/grafana/grafana/pkg/services/guardian"
 	"github.com/grafana/grafana/pkg/services/search"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
@@ -35,23 +36,30 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
 	return query.Result, nil
 }
 
-func GetDashboard(c *middleware.Context) {
+func GetDashboard(c *middleware.Context) Response {
 	slug := strings.ToLower(c.Params(":slug"))
 
 	query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
 	err := bus.Dispatch(&query)
 	if err != nil {
-		c.JsonApiErr(404, "Dashboard not found", nil)
-		return
+		return ApiError(404, "Dashboard not found", err)
 	}
 
-	isStarred, err := isDashboardStarredByUser(c, query.Result.Id)
+	dash := query.Result
+
+	canView, canEdit, canSave, err := getPermissions(dash, c.OrgRole, c.IsGrafanaAdmin, c.OrgId, c.UserId)
 	if err != nil {
-		c.JsonApiErr(500, "Error while checking if dashboard was starred by user", err)
-		return
+		return ApiError(500, "Error while checking dashboard permissions", err)
 	}
 
-	dash := query.Result
+	if !canView {
+		return ApiError(403, "Access denied to this dashboard", nil)
+	}
+
+	isStarred, err := isDashboardStarredByUser(c, dash.Id)
+	if err != nil {
+		return ApiError(500, "Error while checking if dashboard was starred by user", err)
+	}
 
 	// Finding creator and last updater of the dashboard
 	updater, creator := "Anonymous", "Anonymous"
@@ -72,8 +80,8 @@ func GetDashboard(c *middleware.Context) {
 			Slug:      slug,
 			Type:      m.DashTypeDB,
 			CanStar:   c.IsSignedIn,
-			CanSave:   c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
-			CanEdit:   canEditDashboard(c.OrgRole),
+			CanSave:   canSave,
+			CanEdit:   canEdit,
 			Created:   dash.Created,
 			Updated:   dash.Updated,
 			UpdatedBy: updater,
@@ -85,9 +93,27 @@ func GetDashboard(c *middleware.Context) {
 		},
 	}
 
-	// TODO(ben): copy this performance metrics logic for the new API endpoints added
 	c.TimeRequest(metrics.M_Api_Dashboard_Get)
-	c.JSON(200, dto)
+	return Json(200, dto)
+}
+
+func getPermissions(dash *m.Dashboard, orgRole m.RoleType, isGrafanaAdmin bool, orgId int64, userId int64) (bool, bool, bool, error) {
+	if !dash.HasAcl {
+		return true, canEditDashboard(orgRole), orgRole == m.ROLE_ADMIN || orgRole == m.ROLE_EDITOR, nil
+	}
+
+	dashId := dash.Id
+
+	if !dash.IsFolder {
+		dashId = dash.ParentId
+	}
+
+	canView, canEdit, canSave, err := guardian.CheckDashboardPermissions(dashId, orgRole, isGrafanaAdmin, orgId, userId)
+	if err != nil {
+		return false, false, false, err
+	}
+
+	return canView, canEdit, canSave, nil
 }
 
 func getUserLogin(userId int64) string {

+ 5 - 0
pkg/api/dashboard_acl_test.go

@@ -23,6 +23,11 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 			return nil
 		})
 
+		bus.AddHandler("test", func(query *models.GetUserGroupsByUserQuery) error {
+			query.Result = []*models.UserGroup{}
+			return nil
+		})
+
 		Convey("When user is org admin", func() {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/1/acl", "/api/dashboards/:id/acl", models.ROLE_ADMIN, func(sc *scenarioContext) {
 				Convey("Should be able to access ACL", func() {

+ 179 - 0
pkg/api/dashboard_test.go

@@ -0,0 +1,179 @@
+package api
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/grafana/grafana/pkg/api/dtos"
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDashboardApiEndpoint(t *testing.T) {
+	Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
+		fakeDash := models.NewDashboard("Child dash")
+		fakeDash.ParentId = 1
+		fakeDash.HasAcl = false
+
+		bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
+			query.Result = fakeDash
+			return nil
+		})
+
+		Convey("When user is an Org Viewer", func() {
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", models.ROLE_VIEWER, func(sc *scenarioContext) {
+				sc.handlerFunc = GetDashboard
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				dash := dtos.DashboardFullWithMeta{}
+				err := json.NewDecoder(sc.resp.Body).Decode(&dash)
+				So(err, ShouldBeNil)
+
+				Convey("Should not be able to edit or save dashboard", func() {
+					So(dash.Meta.CanEdit, ShouldBeFalse)
+					So(dash.Meta.CanSave, ShouldBeFalse)
+				})
+			})
+		})
+
+		Convey("When user is an Org Read Only Editor", func() {
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", models.ROLE_READ_ONLY_EDITOR, func(sc *scenarioContext) {
+				sc.handlerFunc = GetDashboard
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				dash := dtos.DashboardFullWithMeta{}
+				err := json.NewDecoder(sc.resp.Body).Decode(&dash)
+				So(err, ShouldBeNil)
+
+				Convey("Should be able to edit but not save the dashboard", func() {
+					So(dash.Meta.CanEdit, ShouldBeTrue)
+					So(dash.Meta.CanSave, ShouldBeFalse)
+				})
+			})
+		})
+
+		Convey("When user is an Org Editor", func() {
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", models.ROLE_EDITOR, func(sc *scenarioContext) {
+				sc.handlerFunc = GetDashboard
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				dash := dtos.DashboardFullWithMeta{}
+				err := json.NewDecoder(sc.resp.Body).Decode(&dash)
+				So(err, ShouldBeNil)
+
+				Convey("Should be able to edit or save dashboard", func() {
+					So(dash.Meta.CanEdit, ShouldBeTrue)
+					So(dash.Meta.CanSave, ShouldBeTrue)
+				})
+			})
+		})
+	})
+
+	Convey("Given a dashboard with a parent folder which has an acl", t, func() {
+		fakeDash := models.NewDashboard("Child dash")
+		fakeDash.ParentId = 1
+		fakeDash.HasAcl = true
+
+		bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
+			query.Result = fakeDash
+			return nil
+		})
+
+		bus.AddHandler("test", func(query *models.GetUserGroupsByUserQuery) error {
+			query.Result = []*models.UserGroup{}
+			return nil
+		})
+
+		Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
+			bus.AddHandler("test", func(query *models.GetDashboardPermissionsQuery) error {
+				query.Result = []*models.DashboardAclInfoDTO{}
+				return nil
+			})
+
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", models.ROLE_VIEWER, func(sc *scenarioContext) {
+				sc.handlerFunc = GetDashboard
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+				Convey("Should be denied access", func() {
+					So(sc.resp.Code, ShouldEqual, 403)
+				})
+			})
+		})
+
+		Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
+			bus.AddHandler("test", func(query *models.GetDashboardPermissionsQuery) error {
+				query.Result = []*models.DashboardAclInfoDTO{}
+				return nil
+			})
+
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", models.ROLE_EDITOR, func(sc *scenarioContext) {
+				sc.handlerFunc = GetDashboard
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+				Convey("Should be denied access", func() {
+					So(sc.resp.Code, ShouldEqual, 403)
+				})
+			})
+		})
+
+		Convey("When user is an Org Viewer but has an edit permission", func() {
+			mockResult := []*models.DashboardAclInfoDTO{
+				{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, PermissionType: models.PERMISSION_EDIT},
+			}
+
+			bus.AddHandler("test", func(query *models.GetDashboardPermissionsQuery) error {
+				query.Result = mockResult
+				return nil
+			})
+
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", models.ROLE_VIEWER, func(sc *scenarioContext) {
+				sc.handlerFunc = GetDashboard
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				dash := dtos.DashboardFullWithMeta{}
+				err := json.NewDecoder(sc.resp.Body).Decode(&dash)
+				So(err, ShouldBeNil)
+
+				Convey("Should be able to get dashboard with edit rights", func() {
+					So(dash.Meta.CanEdit, ShouldBeTrue)
+					So(dash.Meta.CanSave, ShouldBeTrue)
+				})
+			})
+		})
+
+		Convey("When user is an Org Editor but has a view permission", func() {
+			mockResult := []*models.DashboardAclInfoDTO{
+				{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, PermissionType: models.PERMISSION_VIEW},
+			}
+
+			bus.AddHandler("test", func(query *models.GetDashboardPermissionsQuery) error {
+				query.Result = mockResult
+				return nil
+			})
+
+			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", models.ROLE_VIEWER, func(sc *scenarioContext) {
+				sc.handlerFunc = GetDashboard
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				dash := dtos.DashboardFullWithMeta{}
+				err := json.NewDecoder(sc.resp.Body).Decode(&dash)
+				So(err, ShouldBeNil)
+
+				Convey("Should not be able to edit or save dashboard", func() {
+					So(dash.Meta.CanEdit, ShouldBeFalse)
+					So(dash.Meta.CanSave, ShouldBeFalse)
+				})
+			})
+		})
+	})
+}

+ 47 - 3
pkg/services/guardian/guardian.go

@@ -6,7 +6,7 @@ import (
 )
 
 // RemoveRestrictedDashboards filters out dashboards from the list that the user does have access to
-func RemoveRestrictedDashboards(dashList []int64, orgId int64, userId int64) ([]int64, error) {
+func FilterRestrictedDashboards(dashList []int64, orgId int64, userId int64) ([]int64, error) {
 	user, err := getUser(userId)
 	if err != nil {
 		return nil, err
@@ -54,15 +54,59 @@ func CanDeleteFromAcl(dashboardId int64, role m.RoleType, isGrafanaAdmin bool, o
 		return true, nil
 	}
 
+	minimumPermission := m.PERMISSION_EDIT
+	return checkPermission(minimumPermission, permissions, userId)
+}
+
+// CheckDashboardPermissions determines if a user has permission to view, edit or save a dashboard
+func CheckDashboardPermissions(dashboardId int64, role m.RoleType, isGrafanaAdmin bool, orgId int64, userId int64) (bool, bool, bool, error) {
+	if role == m.ROLE_ADMIN || isGrafanaAdmin {
+		return true, true, true, nil
+	}
+
+	permissions, err := getDashboardPermissions(dashboardId)
+	if err != nil {
+		return false, false, false, err
+	}
+
+	if len(permissions) == 0 {
+		return false, false, false, nil
+	}
+
+	minimumPermission := m.PERMISSION_VIEW
+	canView, err := checkPermission(minimumPermission, permissions, userId)
+	if err != nil {
+		return false, false, false, err
+	}
+
+	minimumPermission = m.PERMISSION_READ_ONLY_EDIT
+	canEdit, err := checkPermission(minimumPermission, permissions, userId)
+	if err != nil {
+		return false, false, false, err
+	}
+
+	minimumPermission = m.PERMISSION_EDIT
+	canSave, err := checkPermission(minimumPermission, permissions, userId)
+	if err != nil {
+		return false, false, false, err
+	}
+
+	return canView, canEdit, canSave, nil
+}
+
+func checkPermission(minimumPermission m.PermissionType, permissions []*m.DashboardAclInfoDTO, userId int64) (bool, error) {
 	userGroups, err := getUserGroupsByUser(userId)
+	if err != nil {
+		return false, err
+	}
 
 	for _, p := range permissions {
-		if p.UserId == userId && p.PermissionType == m.PERMISSION_EDIT {
+		if p.UserId == userId && p.PermissionType >= minimumPermission {
 			return true, nil
 		}
 
 		for _, ug := range userGroups {
-			if ug.Id == p.UserGroupId && p.PermissionType == m.PERMISSION_EDIT {
+			if ug.Id == p.UserGroupId && p.PermissionType >= minimumPermission {
 				return true, nil
 			}
 		}

+ 3 - 3
pkg/services/guardian/guardian_test.go

@@ -22,7 +22,7 @@ func TestGuardian(t *testing.T) {
 				return nil
 			})
 
-			filteredHitlist, err := RemoveRestrictedDashboards(hitList, orgId, userId)
+			filteredHitlist, err := FilterRestrictedDashboards(hitList, orgId, userId)
 			So(err, ShouldBeNil)
 
 			Convey("should return all dashboards", func() {
@@ -38,7 +38,7 @@ func TestGuardian(t *testing.T) {
 				return nil
 			})
 
-			filteredHitlist, err := RemoveRestrictedDashboards(hitList, orgId, userId)
+			filteredHitlist, err := FilterRestrictedDashboards(hitList, orgId, userId)
 			So(err, ShouldBeNil)
 
 			Convey("should return all dashboards", func() {
@@ -58,7 +58,7 @@ func TestGuardian(t *testing.T) {
 				return nil
 			})
 
-			filteredHitlist, err := RemoveRestrictedDashboards(hitList, orgId, userId)
+			filteredHitlist, err := FilterRestrictedDashboards(hitList, orgId, userId)
 			So(err, ShouldBeNil)
 
 			Convey("should return dashboard that editor has access to", func() {

+ 1 - 1
pkg/services/search/handlers.go

@@ -108,7 +108,7 @@ func removeRestrictedDashboardsFromList(hits HitList, query *Query) (HitList, er
 		dashboardIds = append(dashboardIds, hit.Id)
 	}
 
-	filteredHits, err := guardian.RemoveRestrictedDashboards(dashboardIds, query.OrgId, query.UserId)
+	filteredHits, err := guardian.FilterRestrictedDashboards(dashboardIds, query.OrgId, query.UserId)
 	if err != nil {
 		return nil, err
 	}