WIP: get Dashboard Permissions

The guardian class checks if the user is allowed to get the
permissions for a dashboard.
This commit is contained in:
Daniel Lee 2017-05-08 15:35:34 +02:00
parent 074ef7ce4e
commit f1e1da39e3
12 changed files with 176 additions and 26 deletions

View File

@ -247,6 +247,10 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/home", wrap(GetHomeDashboard)) r.Get("/home", wrap(GetHomeDashboard))
r.Get("/tags", GetDashboardTags) r.Get("/tags", GetDashboardTags)
r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
r.Group("/:id/acl", func() {
r.Get("/", wrap(GetDashboardAcl))
}, reqSignedIn)
}) })
// Dashboard snapshots // Dashboard snapshots

View File

@ -79,6 +79,8 @@ func GetDashboard(c *middleware.Context) {
UpdatedBy: updater, UpdatedBy: updater,
CreatedBy: creator, CreatedBy: creator,
Version: dash.Version, Version: dash.Version,
HasAcl: dash.HasAcl,
IsFolder: dash.IsFolder,
}, },
} }

31
pkg/api/dashboard_acl.go Normal file
View File

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

View File

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

View File

@ -56,6 +56,10 @@ func TestDataSourcesProxy(t *testing.T) {
} }
func loggedInUserScenario(desc string, url string, fn scenarioFunc) { 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() { Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
@ -77,7 +81,7 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
sc.context = c sc.context = c
sc.context.UserId = TestUserID sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID sc.context.OrgId = TestOrgID
sc.context.OrgRole = models.ROLE_EDITOR sc.context.OrgRole = role
if sc.handlerFunc != nil { if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context) return sc.handlerFunc(sc.context)
} }

View File

@ -21,6 +21,8 @@ type DashboardMeta struct {
UpdatedBy string `json:"updatedBy"` UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"` CreatedBy string `json:"createdBy"`
Version int `json:"version"` Version int `json:"version"`
HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
} }
type DashboardFullWithMeta struct { type DashboardFullWithMeta struct {

View File

@ -17,16 +17,32 @@ const (
// Dashboard ACL model // Dashboard ACL model
type DashboardAcl struct { type DashboardAcl struct {
Id int64 Id int64 `json:"id"`
OrgId int64 OrgId int64 `json:"-"`
DashboardId int64 DashboardId int64 `json:"dashboardId"`
Created time.Time Created time.Time `json:"created"`
Updated time.Time Updated time.Time `json:"updated"`
UserId int64 UserId int64 `json:"userId"`
UserGroupId int64 UserGroupId int64 `json:"userGroupId"`
Permissions PermissionType 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 { type GetDashboardPermissionsQuery struct {
DashboardId int64 `json:"dashboardId" binding:"Required"` DashboardId int64 `json:"dashboardId" binding:"Required"`
Result []*DashboardAcl Result []*DashboardAclInfoDTO
} }

View File

@ -21,6 +21,24 @@ func RemoveRestrictedDashboards(dashList []int64, orgId int64, userId int64) ([]
return filteredList, err 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) { func getUser(userId int64) (*m.SignedInUser, error) {
query := m.GetSignedInUserQuery{UserId: userId} query := m.GetSignedInUserQuery{UserId: userId}
err := bus.Dispatch(&query) err := bus.Dispatch(&query)

View File

@ -16,13 +16,14 @@ func init() {
func AddOrUpdateDashboardPermission(cmd *m.AddOrUpdateDashboardPermissionCommand) error { func AddOrUpdateDashboardPermission(cmd *m.AddOrUpdateDashboardPermissionCommand) error {
return inTransaction(func(sess *xorm.Session) 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 return err
} else if len(res) == 1 { } else if len(res) == 1 {
entity := m.DashboardAcl{ entity := m.DashboardAcl{
Permissions: cmd.PermissionType, 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 return err
} }
@ -67,8 +68,8 @@ func AddOrUpdateDashboardPermission(cmd *m.AddOrUpdateDashboardPermissionCommand
func RemoveDashboardPermission(cmd *m.RemoveDashboardPermissionCommand) error { func RemoveDashboardPermission(cmd *m.RemoveDashboardPermissionCommand) error {
return inTransaction(func(sess *xorm.Session) error { return inTransaction(func(sess *xorm.Session) error {
var rawSql = "DELETE FROM dashboard_acl WHERE dashboard_id =? and (user_group_id=? or user_id=?)" 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) _, err := sess.Exec(rawSQL, cmd.DashboardId, cmd.UserGroupId, cmd.UserId)
if err != nil { if err != nil {
return err return err
} }
@ -78,7 +79,19 @@ func RemoveDashboardPermission(cmd *m.RemoveDashboardPermissionCommand) error {
} }
func GetDashboardPermissions(query *m.GetDashboardPermissionsQuery) error { func GetDashboardPermissions(query *m.GetDashboardPermissionsQuery) error {
sess := x.Where("dashboard_id=?", query.DashboardId) rawSQL := `SELECT
query.Result = make([]*m.DashboardAcl, 0) da.*,
return sess.Find(&query.Result) 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
} }

View File

@ -11,14 +11,15 @@ import (
func TestDashboardAclDataAccess(t *testing.T) { func TestDashboardAclDataAccess(t *testing.T) {
Convey("Testing DB", t, func() { Convey("Testing DB", t, func() {
InitTestDB(t) 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") savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp") childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp")
Convey("Should be able to add dashboard permission", func() { Convey("Should be able to add dashboard permission", func() {
err := AddOrUpdateDashboardPermission(&m.AddOrUpdateDashboardPermissionCommand{ err := AddOrUpdateDashboardPermission(&m.AddOrUpdateDashboardPermissionCommand{
OrgId: 1, OrgId: 1,
UserId: 1, UserId: currentUser.Id,
DashboardId: savedFolder.Id, DashboardId: savedFolder.Id,
PermissionType: m.PERMISSION_EDIT, PermissionType: m.PERMISSION_EDIT,
}) })
@ -29,7 +30,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permissions, ShouldEqual, m.PERMISSION_EDIT) 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() { Convey("Should update hasAcl field to true for dashboard folder and its children", func() {
q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}} q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}}

View File

@ -28,7 +28,7 @@ where (
rawSQL = fmt.Sprintf("%v and d.id in(%v)", rawSQL, dashboardIds) rawSQL = fmt.Sprintf("%v and d.id in(%v)", rawSQL, dashboardIds)
query.Result = make([]int64, 0) 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 { if err != nil {
return err return err

View File

@ -19,7 +19,7 @@ func TestGuardianDataAccess(t *testing.T) {
insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp") insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp")
insertTestDashboard("test dash 45", 1, folder.Id, false, "prod") 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("and no acls are set", func() {
Convey("should return all dashboards", 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.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(&currentUserCmd) err := CreateUser(&currentUserCmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
q1 := m.GetUserOrgListQuery{UserId: currentUserCmd.Result.Id} q1 := m.GetUserOrgListQuery{UserId: currentUserCmd.Result.Id}
GetUserOrgList(&q1) GetUserOrgList(&q1)
So(q1.Result[0].Role, ShouldEqual, "Viewer") So(q1.Result[0].Role, ShouldEqual, role)
return currentUserCmd.Result return currentUserCmd.Result
} }