From f1e1da39e3991eeb38b6f882ce039ec9d51d5473 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 8 May 2017 15:35:34 +0200 Subject: [PATCH] WIP: get Dashboard Permissions The guardian class checks if the user is allowed to get the permissions for a dashboard. --- pkg/api/api.go | 4 ++ pkg/api/dashboard.go | 2 + pkg/api/dashboard_acl.go | 31 +++++++++++ pkg/api/dashboard_acl_test.go | 57 +++++++++++++++++++++ pkg/api/datasources_test.go | 6 ++- pkg/api/dtos/dashboard.go | 2 + pkg/models/dashboard_acl.go | 34 ++++++++---- pkg/services/guardian/guardian.go | 18 +++++++ pkg/services/sqlstore/dashboard_acl.go | 27 +++++++--- pkg/services/sqlstore/dashboard_acl_test.go | 9 ++-- pkg/services/sqlstore/guardian.go | 2 +- pkg/services/sqlstore/guardian_test.go | 10 ++-- 12 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 pkg/api/dashboard_acl.go create mode 100644 pkg/api/dashboard_acl_test.go diff --git a/pkg/api/api.go b/pkg/api/api.go index ca33588f072..4f4163bde77 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -247,6 +247,10 @@ func (hs *HttpServer) registerRoutes() { r.Get("/home", wrap(GetHomeDashboard)) r.Get("/tags", GetDashboardTags) r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) + + r.Group("/:id/acl", func() { + r.Get("/", wrap(GetDashboardAcl)) + }, reqSignedIn) }) // Dashboard snapshots diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index df0cbbd745c..b4511c916a8 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -79,6 +79,8 @@ func GetDashboard(c *middleware.Context) { UpdatedBy: updater, CreatedBy: creator, Version: dash.Version, + HasAcl: dash.HasAcl, + IsFolder: dash.IsFolder, }, } diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go new file mode 100644 index 00000000000..72e15c13239 --- /dev/null +++ b/pkg/api/dashboard_acl.go @@ -0,0 +1,31 @@ +package api + +import ( + "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/guardian" + "github.com/grafana/grafana/pkg/util" +) + +func GetDashboardAcl(c *middleware.Context) Response { + dashboardId := c.ParamsInt64(":id") + + hasPermission, err := guardian.CanViewAcl(dashboardId, c.OrgRole, c.IsGrafanaAdmin, c.OrgId, c.UserId) + + if err != nil { + return ApiError(500, "Failed to get Dashboard ACL", err) + } + + if !hasPermission { + return Json(403, util.DynMap{"status": "Forbidden", "message": "Does not have access to this Dashboard ACL"}) + } + + query := m.GetDashboardPermissionsQuery{DashboardId: dashboardId} + + if err := bus.Dispatch(&query); err != nil { + return ApiError(500, "Failed to get Dashboard ACL", err) + } + + return Json(200, &query.Result) +} diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_acl_test.go new file mode 100644 index 00000000000..26636ad87f6 --- /dev/null +++ b/pkg/api/dashboard_acl_test.go @@ -0,0 +1,57 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestDashboardAclApiEndpoint(t *testing.T) { + Convey("Given a dashboard acl", t, func() { + mockResult := []*models.DashboardAclInfoDTO{ + {Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permissions: models.PERMISSION_EDIT}, + {Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permissions: models.PERMISSION_VIEW}, + } + bus.AddHandler("test", func(query *models.GetDashboardPermissionsQuery) error { + query.Result = mockResult + return nil + }) + + Convey("When user is org admin", func() { + loggedInUserScenarioWithRole("When calling GET on", "/api/dashboard/1/acl", models.ROLE_ADMIN, func(sc *scenarioContext) { + Convey("Should be able to access ACL", func() { + sc.handlerFunc = GetDashboardAcl + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2) + So(respJSON.GetIndex(0).Get("permissions").MustInt(), ShouldEqual, models.PERMISSION_EDIT) + }) + }) + }) + + Convey("When user is editor and not in the ACL", func() { + loggedInUserScenarioWithRole("When calling GET on", "/api/dashboard/1/acl", models.ROLE_EDITOR, func(sc *scenarioContext) { + + bus.AddHandler("test2", func(query *models.GetAllowedDashboardsQuery) error { + query.Result = []int64{1} + return nil + }) + + Convey("Should not be able to access ACL", func() { + sc.handlerFunc = GetDashboardAcl + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + }) + }) +} diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 5ae752bea91..6d65486e5e7 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -56,6 +56,10 @@ func TestDataSourcesProxy(t *testing.T) { } func loggedInUserScenario(desc string, url string, fn scenarioFunc) { + loggedInUserScenarioWithRole(desc, url, models.ROLE_EDITOR, fn) +} + +func loggedInUserScenarioWithRole(desc string, url string, role models.RoleType, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() @@ -77,7 +81,7 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID - sc.context.OrgRole = models.ROLE_EDITOR + sc.context.OrgRole = role if sc.handlerFunc != nil { return sc.handlerFunc(sc.context) } diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 9ef9a96edc4..e0ff44ff016 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -21,6 +21,8 @@ type DashboardMeta struct { UpdatedBy string `json:"updatedBy"` CreatedBy string `json:"createdBy"` Version int `json:"version"` + HasAcl bool `json:"hasAcl"` + IsFolder bool `json:"isFolder"` } type DashboardFullWithMeta struct { diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 82577504b30..edd36ec146f 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -17,16 +17,32 @@ const ( // Dashboard ACL model type DashboardAcl struct { - Id int64 - OrgId int64 - DashboardId int64 + Id int64 `json:"id"` + OrgId int64 `json:"-"` + DashboardId int64 `json:"dashboardId"` - Created time.Time - Updated time.Time + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` - UserId int64 - UserGroupId int64 - Permissions PermissionType + UserId int64 `json:"userId"` + UserGroupId int64 `json:"userGroupId"` + Permissions PermissionType `json:"permissions"` +} + +type DashboardAclInfoDTO struct { + Id int64 `json:"id"` + OrgId int64 `json:"-"` + DashboardId int64 `json:"dashboardId"` + + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + + UserId int64 `json:"userId"` + UserLogin string `json:"userLogin"` + UserEmail string `json:"userEmail"` + UserGroupId int64 `json:"userGroupId"` + UserGroup string `json:"userGroup"` + Permissions PermissionType `json:"permissions"` } // @@ -54,5 +70,5 @@ type RemoveDashboardPermissionCommand struct { type GetDashboardPermissionsQuery struct { DashboardId int64 `json:"dashboardId" binding:"Required"` - Result []*DashboardAcl + Result []*DashboardAclInfoDTO } diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 76a7fefa880..491c7acb08c 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -21,6 +21,24 @@ func RemoveRestrictedDashboards(dashList []int64, orgId int64, userId int64) ([] return filteredList, err } +// CanViewAcl determines if a user has permission to view a dashboard's ACL +func CanViewAcl(dashboardId int64, role m.RoleType, isGrafanaAdmin bool, orgId int64, userId int64) (bool, error) { + if role == m.ROLE_ADMIN || isGrafanaAdmin { + return true, nil + } + + filteredList, err := getAllowedDashboards([]int64{dashboardId}, orgId, userId) + if err != nil { + return false, err + } + + if len(filteredList) > 1 && filteredList[0] == dashboardId { + return true, nil + } + + return false, nil +} + func getUser(userId int64) (*m.SignedInUser, error) { query := m.GetSignedInUserQuery{UserId: userId} err := bus.Dispatch(&query) diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 04dae5a85a5..b18ed3e3b7c 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -16,13 +16,14 @@ func init() { func AddOrUpdateDashboardPermission(cmd *m.AddOrUpdateDashboardPermissionCommand) error { return inTransaction(func(sess *xorm.Session) error { - if res, err := sess.Query("SELECT 1 from dashboard_acl WHERE dashboard_id =? and (user_group_id=? or user_id=?)", cmd.DashboardId, cmd.UserGroupId, cmd.UserId); err != nil { + if res, err := sess.Query("SELECT 1 from "+dialect.Quote("dashboard_acl")+" WHERE dashboard_id =? and (user_group_id=? or user_id=?)", cmd.DashboardId, cmd.UserGroupId, cmd.UserId); err != nil { return err } else if len(res) == 1 { entity := m.DashboardAcl{ Permissions: cmd.PermissionType, + Updated: time.Now(), } - if _, err := sess.Cols("permissions").Where("dashboard_id =? and (user_group_id=? or user_id=?)", cmd.DashboardId, cmd.UserGroupId, cmd.UserId).Update(&entity); err != nil { + if _, err := sess.Cols("updated", "permissions").Where("dashboard_id =? and (user_group_id=? or user_id=?)", cmd.DashboardId, cmd.UserGroupId, cmd.UserId).Update(&entity); err != nil { return err } @@ -67,8 +68,8 @@ func AddOrUpdateDashboardPermission(cmd *m.AddOrUpdateDashboardPermissionCommand func RemoveDashboardPermission(cmd *m.RemoveDashboardPermissionCommand) error { return inTransaction(func(sess *xorm.Session) error { - var rawSql = "DELETE FROM dashboard_acl WHERE dashboard_id =? and (user_group_id=? or user_id=?)" - _, err := sess.Exec(rawSql, cmd.DashboardId, cmd.UserGroupId, cmd.UserId) + var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE dashboard_id =? and (user_group_id=? or user_id=?)" + _, err := sess.Exec(rawSQL, cmd.DashboardId, cmd.UserGroupId, cmd.UserId) if err != nil { return err } @@ -78,7 +79,19 @@ func RemoveDashboardPermission(cmd *m.RemoveDashboardPermissionCommand) error { } func GetDashboardPermissions(query *m.GetDashboardPermissionsQuery) error { - sess := x.Where("dashboard_id=?", query.DashboardId) - query.Result = make([]*m.DashboardAcl, 0) - return sess.Find(&query.Result) + rawSQL := `SELECT + da.*, + u.login AS user_login, + u.email AS user_email, + ug.name AS user_group + FROM` + dialect.Quote("dashboard_acl") + ` as da + LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id + LEFT OUTER JOIN user_group ug on ug.id = da.user_group_id + WHERE dashboard_id=?` + + query.Result = make([]*m.DashboardAclInfoDTO, 0) + + err := x.SQL(rawSQL, query.DashboardId).Find(&query.Result) + + return err } diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index 771b242a154..8445c3acd21 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -11,14 +11,15 @@ import ( func TestDashboardAclDataAccess(t *testing.T) { Convey("Testing DB", t, func() { InitTestDB(t) - Convey("Given a dashboard folder", func() { + Convey("Given a dashboard folder and a user", func() { + currentUser := createUser("viewer", "Viewer", false) savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp") childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp") Convey("Should be able to add dashboard permission", func() { err := AddOrUpdateDashboardPermission(&m.AddOrUpdateDashboardPermissionCommand{ OrgId: 1, - UserId: 1, + UserId: currentUser.Id, DashboardId: savedFolder.Id, PermissionType: m.PERMISSION_EDIT, }) @@ -29,7 +30,9 @@ func TestDashboardAclDataAccess(t *testing.T) { So(err, ShouldBeNil) So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) So(q1.Result[0].Permissions, ShouldEqual, m.PERMISSION_EDIT) - So(q1.Result[0].UserId, ShouldEqual, 1) + So(q1.Result[0].UserId, ShouldEqual, currentUser.Id) + So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login) + So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email) Convey("Should update hasAcl field to true for dashboard folder and its children", func() { q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}} diff --git a/pkg/services/sqlstore/guardian.go b/pkg/services/sqlstore/guardian.go index 1ba346def2c..f3e5d45e707 100644 --- a/pkg/services/sqlstore/guardian.go +++ b/pkg/services/sqlstore/guardian.go @@ -28,7 +28,7 @@ where ( rawSQL = fmt.Sprintf("%v and d.id in(%v)", rawSQL, dashboardIds) query.Result = make([]int64, 0) - err := x.In("DashboardId", query.DashList).SQL(rawSQL, query.UserId, query.UserId, query.UserId, query.UserId, query.OrgId).Find(&query.Result) + err := x.SQL(rawSQL, query.UserId, query.UserId, query.UserId, query.UserId, query.OrgId).Find(&query.Result) if err != nil { return err diff --git a/pkg/services/sqlstore/guardian_test.go b/pkg/services/sqlstore/guardian_test.go index ed77131c787..dfdc51d675f 100644 --- a/pkg/services/sqlstore/guardian_test.go +++ b/pkg/services/sqlstore/guardian_test.go @@ -19,7 +19,7 @@ func TestGuardianDataAccess(t *testing.T) { insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp") insertTestDashboard("test dash 45", 1, folder.Id, false, "prod") - currentUser := createUser("viewer") + currentUser := createUser("viewer", "Viewer", false) Convey("and no acls are set", func() { Convey("should return all dashboards", func() { @@ -61,17 +61,17 @@ func TestGuardianDataAccess(t *testing.T) { }) } -func createUser(name string) m.User { +func createUser(name string, role string, isAdmin bool) m.User { setting.AutoAssignOrg = true - setting.AutoAssignOrgRole = "Viewer" + setting.AutoAssignOrgRole = role - currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: false} + currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin} err := CreateUser(¤tUserCmd) So(err, ShouldBeNil) q1 := m.GetUserOrgListQuery{UserId: currentUserCmd.Result.Id} GetUserOrgList(&q1) - So(q1.Result[0].Role, ShouldEqual, "Viewer") + So(q1.Result[0].Role, ShouldEqual, role) return currentUserCmd.Result }