diff --git a/pkg/api/api.go b/pkg/api/api.go index 890142c44bd..5afec7e792e 100644 --- a/pkg/api/api.go +++ b/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)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index daab2ec9074..0eeeea94056 100644 --- a/pkg/api/dashboard.go +++ b/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,24 +36,31 @@ 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 - } - - isStarred, err := isDashboardStarredByUser(c, query.Result.Id) - if err != nil { - c.JsonApiErr(500, "Error while checking if dashboard was starred by user", err) - return + return ApiError(404, "Dashboard not found", err) } dash := query.Result + canView, canEdit, canSave, err := getPermissions(dash, c.OrgRole, c.IsGrafanaAdmin, c.OrgId, c.UserId) + if err != nil { + return ApiError(500, "Error while checking dashboard permissions", err) + } + + 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" if dash.UpdatedBy > 0 { @@ -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 { diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_acl_test.go index 22eb342c455..992924c5b4f 100644 --- a/pkg/api/dashboard_acl_test.go +++ b/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() { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go new file mode 100644 index 00000000000..7ace884a8ae --- /dev/null +++ b/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) + }) + }) + }) + }) +} diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 4e9bef1c5fa..81669510f4b 100644 --- a/pkg/services/guardian/guardian.go +++ b/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 } } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index bee33a07924..4bec550f791 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/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() { diff --git a/pkg/services/search/handlers.go b/pkg/services/search/handlers.go index 1f89eb650a8..e2ad56e7363 100644 --- a/pkg/services/search/handlers.go +++ b/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 }