Commit aab6c98e by Torkel Ödegaard

Merge branch 'dashboard-acl' into dashboard_folders

parents 1e064f6c a37c4411
...@@ -23,7 +23,8 @@ func GetDashboardAclList(c *middleware.Context) Response { ...@@ -23,7 +23,8 @@ func GetDashboardAclList(c *middleware.Context) Response {
return ApiError(500, "Failed to get Dashboard ACL", err) return ApiError(500, "Failed to get Dashboard ACL", err)
} }
return Json(200, &query.Result) list := query.Result
return Json(200, list)
} }
func PostDashboardAcl(c *middleware.Context, cmd m.SetDashboardAclCommand) Response { func PostDashboardAcl(c *middleware.Context, cmd m.SetDashboardAclCommand) Response {
......
...@@ -13,16 +13,16 @@ import ( ...@@ -13,16 +13,16 @@ import (
func TestDashboardAclApiEndpoint(t *testing.T) { func TestDashboardAclApiEndpoint(t *testing.T) {
Convey("Given a dashboard acl", t, func() { Convey("Given a dashboard acl", t, func() {
mockResult := []*models.DashboardAcl{ mockResult := []*models.DashboardAcl{
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permissions: models.PERMISSION_EDIT}, {Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: models.PERMISSION_EDIT},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permissions: models.PERMISSION_VIEW}, {Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: models.PERMISSION_VIEW},
{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permissions: models.PERMISSION_EDIT}, {Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permission: models.PERMISSION_EDIT},
{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permissions: models.PERMISSION_READ_ONLY_EDIT}, {Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permission: models.PERMISSION_READ_ONLY_EDIT},
} }
dtoRes := []*models.DashboardAclInfoDTO{ dtoRes := []*models.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permissions: models.PERMISSION_EDIT}, {Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: models.PERMISSION_EDIT},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permissions: models.PERMISSION_VIEW}, {Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: models.PERMISSION_VIEW},
{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permissions: models.PERMISSION_EDIT}, {Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permission: models.PERMISSION_EDIT},
{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permissions: models.PERMISSION_READ_ONLY_EDIT}, {Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permission: models.PERMISSION_READ_ONLY_EDIT},
} }
bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error {
...@@ -59,7 +59,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { ...@@ -59,7 +59,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
Convey("When user is editor and in the ACL", func() { Convey("When user is editor and in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", models.ROLE_EDITOR, func(sc *scenarioContext) { loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_EDIT}) mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: models.PERMISSION_EDIT})
Convey("Should be able to access ACL", func() { Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList sc.handlerFunc = GetDashboardAclList
...@@ -70,7 +70,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { ...@@ -70,7 +70,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
}) })
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", models.ROLE_EDITOR, func(sc *scenarioContext) { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_EDIT}) mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: models.PERMISSION_EDIT})
bus.AddHandler("test3", func(cmd *models.RemoveDashboardAclCommand) error { bus.AddHandler("test3", func(cmd *models.RemoveDashboardAclCommand) error {
return nil return nil
...@@ -114,7 +114,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { ...@@ -114,7 +114,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
}) })
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", models.ROLE_EDITOR, func(sc *scenarioContext) { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_VIEW}) mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: models.PERMISSION_VIEW})
bus.AddHandler("test3", func(cmd *models.RemoveDashboardAclCommand) error { bus.AddHandler("test3", func(cmd *models.RemoveDashboardAclCommand) error {
return nil return nil
}) })
......
...@@ -174,7 +174,7 @@ func TestDashboardApiEndpoint(t *testing.T) { ...@@ -174,7 +174,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
aclMockResp := []*models.DashboardAcl{ aclMockResp := []*models.DashboardAcl{
{ {
DashboardId: 1, DashboardId: 1,
Permissions: models.PERMISSION_EDIT, Permission: models.PERMISSION_EDIT,
UserId: 200, UserId: 200,
}, },
} }
...@@ -273,7 +273,7 @@ func TestDashboardApiEndpoint(t *testing.T) { ...@@ -273,7 +273,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
role := models.ROLE_VIEWER role := models.ROLE_VIEWER
mockResult := []*models.DashboardAcl{ mockResult := []*models.DashboardAcl{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permissions: models.PERMISSION_EDIT}, {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_EDIT},
} }
bus.AddHandler("test", func(query *models.GetInheritedDashboardAclQuery) error { bus.AddHandler("test", func(query *models.GetInheritedDashboardAclQuery) error {
...@@ -315,7 +315,7 @@ func TestDashboardApiEndpoint(t *testing.T) { ...@@ -315,7 +315,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
role := models.ROLE_EDITOR role := models.ROLE_EDITOR
mockResult := []*models.DashboardAcl{ mockResult := []*models.DashboardAcl{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permissions: models.PERMISSION_VIEW}, {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW},
} }
bus.AddHandler("test", func(query *models.GetInheritedDashboardAclQuery) error { bus.AddHandler("test", func(query *models.GetInheritedDashboardAclQuery) error {
......
...@@ -9,15 +9,15 @@ type PermissionType int ...@@ -9,15 +9,15 @@ type PermissionType int
const ( const (
PERMISSION_VIEW PermissionType = 1 << iota PERMISSION_VIEW PermissionType = 1 << iota
PERMISSION_READ_ONLY_EDIT
PERMISSION_EDIT PERMISSION_EDIT
PERMISSION_ADMIN
) )
func (p PermissionType) String() string { func (p PermissionType) String() string {
names := map[int]string{ names := map[int]string{
int(PERMISSION_VIEW): "View", int(PERMISSION_VIEW): "View",
int(PERMISSION_READ_ONLY_EDIT): "Read-only Edit", int(PERMISSION_EDIT): "Edit",
int(PERMISSION_EDIT): "Edit", int(PERMISSION_ADMIN): "Admin",
} }
return names[int(p)] return names[int(p)]
} }
...@@ -36,7 +36,7 @@ type DashboardAcl struct { ...@@ -36,7 +36,7 @@ type DashboardAcl struct {
UserId int64 UserId int64
UserGroupId int64 UserGroupId int64
Permissions PermissionType Permission PermissionType
Created time.Time Created time.Time
Updated time.Time Updated time.Time
...@@ -55,7 +55,8 @@ type DashboardAclInfoDTO struct { ...@@ -55,7 +55,8 @@ type DashboardAclInfoDTO struct {
UserEmail string `json:"userEmail"` UserEmail string `json:"userEmail"`
UserGroupId int64 `json:"userGroupId"` UserGroupId int64 `json:"userGroupId"`
UserGroup string `json:"userGroup"` UserGroup string `json:"userGroup"`
Permissions PermissionType `json:"permissions"` Role RoleType `json:"role"`
Permission PermissionType `json:"permission"`
PermissionName string `json:"permissionName"` PermissionName string `json:"permissionName"`
} }
...@@ -68,7 +69,7 @@ type SetDashboardAclCommand struct { ...@@ -68,7 +69,7 @@ type SetDashboardAclCommand struct {
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
UserId int64 `json:"userId"` UserId int64 `json:"userId"`
UserGroupId int64 `json:"userGroupId"` UserGroupId int64 `json:"userGroupId"`
Permissions PermissionType `json:"permissions" binding:"Required"` Permission PermissionType `json:"permission" binding:"Required"`
Result DashboardAcl `json:"-"` Result DashboardAcl `json:"-"`
} }
......
...@@ -29,7 +29,7 @@ func (g *DashboardGuardian) CanSave() (bool, error) { ...@@ -29,7 +29,7 @@ func (g *DashboardGuardian) CanSave() (bool, error) {
} }
func (g *DashboardGuardian) CanEdit() (bool, error) { func (g *DashboardGuardian) CanEdit() (bool, error) {
return g.HasPermission(m.PERMISSION_READ_ONLY_EDIT, m.ROLE_READ_ONLY_EDITOR) return g.HasPermission(m.PERMISSION_EDIT, m.ROLE_READ_ONLY_EDITOR)
} }
func (g *DashboardGuardian) CanView() (bool, error) { func (g *DashboardGuardian) CanView() (bool, error) {
...@@ -57,12 +57,12 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType, fallbackR ...@@ -57,12 +57,12 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType, fallbackR
} }
for _, p := range acl { for _, p := range acl {
if p.UserId == g.user.UserId && p.Permissions >= permission { if p.UserId == g.user.UserId && p.Permission >= permission {
return true, nil return true, nil
} }
for _, ug := range userGroups { for _, ug := range userGroups {
if ug.Id == p.UserGroupId && p.Permissions >= permission { if ug.Id == p.UserGroupId && p.Permission >= permission {
return true, nil return true, nil
} }
} }
......
...@@ -27,11 +27,13 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { ...@@ -27,11 +27,13 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
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 { 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.Permissions, Permission: cmd.Permission,
Updated: time.Now(), Updated: time.Now(),
} }
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 {
if _, err := sess.Cols("updated", "permission").Where("dashboard_id =? and (user_group_id=? or user_id=?)", cmd.DashboardId, cmd.UserGroupId, cmd.UserId).Update(&entity); err != nil {
return err return err
} }
...@@ -45,10 +47,10 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { ...@@ -45,10 +47,10 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
Created: time.Now(), Created: time.Now(),
Updated: time.Now(), Updated: time.Now(),
DashboardId: cmd.DashboardId, DashboardId: cmd.DashboardId,
Permissions: cmd.Permissions, Permission: cmd.Permission,
} }
cols := []string{"org_id", "created", "updated", "dashboard_id", "permissions"} cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"}
if cmd.UserId != 0 { if cmd.UserId != 0 {
cols = append(cols, "user_id") cols = append(cols, "user_id")
...@@ -58,12 +60,12 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { ...@@ -58,12 +60,12 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
cols = append(cols, "user_group_id") cols = append(cols, "user_group_id")
} }
entityId, err := sess.Cols(cols...).Insert(&entity) _, err := sess.Cols(cols...).Insert(&entity)
if err != nil { if err != nil {
return err return err
} }
cmd.Result = entity cmd.Result = entity
cmd.Result.Id = entityId
// Update dashboard HasAcl flag // Update dashboard HasAcl flag
dashboard := m.Dashboard{ dashboard := m.Dashboard{
...@@ -97,7 +99,7 @@ func GetInheritedDashboardAcl(query *m.GetInheritedDashboardAclQuery) error { ...@@ -97,7 +99,7 @@ func GetInheritedDashboardAcl(query *m.GetInheritedDashboardAclQuery) error {
da.dashboard_id, da.dashboard_id,
da.user_id, da.user_id,
da.user_group_id, da.user_group_id,
da.permissions, da.permission,
da.created, da.created,
da.updated da.updated
FROM dashboard_acl as da FROM dashboard_acl as da
...@@ -112,29 +114,51 @@ func GetInheritedDashboardAcl(query *m.GetInheritedDashboardAclQuery) error { ...@@ -112,29 +114,51 @@ func GetInheritedDashboardAcl(query *m.GetInheritedDashboardAclQuery) error {
} }
func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
rawSQL := `SELECT rawSQL := `
da.id, SELECT
da.org_id, da.id,
da.dashboard_id, da.org_id,
da.user_id, da.dashboard_id,
da.user_group_id, da.user_id,
da.permissions, da.user_group_id,
da.created, da.permission,
da.updated, da.role,
u.login AS user_login, da.created,
u.email AS user_email, da.updated,
ug.name AS user_group u.login AS user_login,
u.email AS user_email,
ug.name AS user_group
FROM` + dialect.Quote("dashboard_acl") + ` as da FROM` + dialect.Quote("dashboard_acl") + ` as da
LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id 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 LEFT OUTER JOIN user_group ug on ug.id = da.user_group_id
WHERE dashboard_id=?` WHERE dashboard_id = ?
-- Also include default permission if has_acl = 0
UNION
SELECT
da.id,
da.org_id,
da.dashboard_id,
da.user_id,
da.user_group_id,
da.permission,
da.role,
da.created,
da.updated,
'' as user_login,
'' as user_email,
'' as user_group
FROM dashboard_acl as da, dashboard as dash
WHERE dash.id = ? AND dash.has_acl = 0 AND da.dashboard_id = -1
`
query.Result = make([]*m.DashboardAclInfoDTO, 0) query.Result = make([]*m.DashboardAclInfoDTO, 0)
err := x.SQL(rawSQL, query.DashboardId).Find(&query.Result) err := x.SQL(rawSQL, query.DashboardId, query.DashboardId).Find(&query.Result)
for _, p := range query.Result { for _, p := range query.Result {
p.PermissionName = p.Permissions.String() p.PermissionName = p.Permission.String()
} }
return err return err
......
...@@ -20,7 +20,7 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -20,7 +20,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
err := SetDashboardAcl(&m.SetDashboardAclCommand{ err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1, OrgId: 1,
DashboardId: savedFolder.Id, DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_EDIT, Permission: m.PERMISSION_EDIT,
}) })
So(err, ShouldEqual, m.ErrDashboardAclInfoMissing) So(err, ShouldEqual, m.ErrDashboardAclInfoMissing)
}) })
...@@ -30,7 +30,7 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -30,7 +30,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
OrgId: 1, OrgId: 1,
UserId: currentUser.Id, UserId: currentUser.Id,
DashboardId: savedFolder.Id, DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_EDIT, Permission: m.PERMISSION_EDIT,
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
...@@ -49,7 +49,7 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -49,7 +49,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
OrgId: 1, OrgId: 1,
UserId: currentUser.Id, UserId: currentUser.Id,
DashboardId: childDash.Id, DashboardId: childDash.Id,
Permissions: m.PERMISSION_EDIT, Permission: m.PERMISSION_EDIT,
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
...@@ -67,23 +67,29 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -67,23 +67,29 @@ func TestDashboardAclDataAccess(t *testing.T) {
}) })
Convey("Should be able to add dashboard permission", func() { Convey("Should be able to add dashboard permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{ setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1, OrgId: 1,
UserId: currentUser.Id, UserId: currentUser.Id,
DashboardId: savedFolder.Id, DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_EDIT, Permission: m.PERMISSION_EDIT,
}) }
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(setDashAclCmd.Result.Id, ShouldEqual, 3)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id} q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
err = GetDashboardAclInfoList(q1) err = GetDashboardAclInfoList(q1)
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].Permission, ShouldEqual, m.PERMISSION_EDIT)
So(q1.Result[0].PermissionName, ShouldEqual, "Edit") So(q1.Result[0].PermissionName, ShouldEqual, "Edit")
So(q1.Result[0].UserId, ShouldEqual, currentUser.Id) So(q1.Result[0].UserId, ShouldEqual, currentUser.Id)
So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login) So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login)
So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email) So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email)
So(q1.Result[0].Id, ShouldEqual, setDashAclCmd.Result.Id)
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}}
...@@ -98,8 +104,9 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -98,8 +104,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
OrgId: 1, OrgId: 1,
UserId: 1, UserId: 1,
DashboardId: savedFolder.Id, DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_READ_ONLY_EDIT, Permission: m.PERMISSION_ADMIN,
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id} q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
...@@ -107,7 +114,7 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -107,7 +114,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1) So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id) So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permissions, ShouldEqual, m.PERMISSION_READ_ONLY_EDIT) So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].UserId, ShouldEqual, 1) So(q3.Result[0].UserId, ShouldEqual, 1)
}) })
...@@ -115,8 +122,9 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -115,8 +122,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
Convey("Should be able to delete an existing permission", func() { Convey("Should be able to delete an existing permission", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{ err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1, OrgId: 1,
AclId: 1, AclId: setDashAclCmd.Result.Id,
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id} q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
...@@ -132,20 +140,35 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -132,20 +140,35 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("Should be able to add a user permission for a user group", func() { Convey("Should be able to add a user permission for a user group", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{ setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1, OrgId: 1,
UserGroupId: group1.Result.Id, UserGroupId: group1.Result.Id,
DashboardId: savedFolder.Id, DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_EDIT, Permission: m.PERMISSION_EDIT,
}) }
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id} q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
err = GetDashboardAclInfoList(q1) err = GetDashboardAclInfoList(q1)
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].Permission, ShouldEqual, m.PERMISSION_EDIT)
So(q1.Result[0].UserGroupId, ShouldEqual, group1.Result.Id) So(q1.Result[0].UserGroupId, ShouldEqual, group1.Result.Id)
Convey("Should be able to delete an existing permission for a user group", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1,
AclId: setDashAclCmd.Result.Id,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
}) })
Convey("Should be able to update an existing permission for a user group", func() { Convey("Should be able to update an existing permission for a user group", func() {
...@@ -153,7 +176,7 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -153,7 +176,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
OrgId: 1, OrgId: 1,
UserGroupId: group1.Result.Id, UserGroupId: group1.Result.Id,
DashboardId: savedFolder.Id, DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_READ_ONLY_EDIT, Permission: m.PERMISSION_ADMIN,
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
...@@ -162,23 +185,10 @@ func TestDashboardAclDataAccess(t *testing.T) { ...@@ -162,23 +185,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1) So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id) So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permissions, ShouldEqual, m.PERMISSION_READ_ONLY_EDIT) So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].UserGroupId, ShouldEqual, group1.Result.Id) So(q3.Result[0].UserGroupId, ShouldEqual, group1.Result.Id)
}) })
Convey("Should be able to delete an existing permission for a user group", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1,
AclId: 1,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
}) })
}) })
}) })
......
...@@ -384,7 +384,7 @@ func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.Permis ...@@ -384,7 +384,7 @@ func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.Permis
OrgId: 1, OrgId: 1,
UserId: userId, UserId: userId,
DashboardId: dashId, DashboardId: dashId,
Permissions: permissions, Permission: permissions,
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
} }
...@@ -11,21 +11,42 @@ func addDashboardAclMigrations(mg *Migrator) { ...@@ -11,21 +11,42 @@ func addDashboardAclMigrations(mg *Migrator) {
{Name: "dashboard_id", Type: DB_BigInt}, {Name: "dashboard_id", Type: DB_BigInt},
{Name: "user_id", Type: DB_BigInt, Nullable: true}, {Name: "user_id", Type: DB_BigInt, Nullable: true},
{Name: "user_group_id", Type: DB_BigInt, Nullable: true}, {Name: "user_group_id", Type: DB_BigInt, Nullable: true},
{Name: "permissions", Type: DB_SmallInt, Default: "4"}, {Name: "permission", Type: DB_SmallInt, Default: "4"},
{Name: "role", Type: DB_Varchar, Length: 20, Nullable: true},
{Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false},
}, },
Indices: []*Index{ Indices: []*Index{
{Cols: []string{"org_id"}}, {Cols: []string{"dashboard_id"}},
{Cols: []string{"dashboard_id", "user_id"}, Type: UniqueIndex}, {Cols: []string{"dashboard_id", "user_id"}, Type: UniqueIndex},
{Cols: []string{"dashboard_id", "user_group_id"}, Type: UniqueIndex}, {Cols: []string{"dashboard_id", "user_group_id"}, Type: UniqueIndex},
}, },
} }
mg.AddMigration("create dashboard acl table", NewAddTableMigration(dashboardAclV1)) mg.AddMigration("create dashboard acl table", NewAddTableMigration(dashboardAclV1))
//------- indexes ------------------ //------- indexes ------------------
mg.AddMigration("add unique index dashboard_acl_org_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[0])) mg.AddMigration("add unique index dashboard_acl_dashboard_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[0]))
mg.AddMigration("add unique index dashboard_acl_dashboard_id_user_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[1])) mg.AddMigration("add unique index dashboard_acl_dashboard_id_user_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[1]))
mg.AddMigration("add unique index dashboard_acl_dashboard_id_group_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[2])) mg.AddMigration("add unique index dashboard_acl_dashboard_id_group_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[2]))
const rawSQL = `
INSERT INTO dashboard_acl
(
org_id,
dashboard_id,
permission,
role,
created,
updated
)
VALUES
(-1,-1, 1,'Viewer','2017-06-20','2017-06-20'),
(-1,-1, 2,'Editor','2017-06-20','2017-06-20')
`
mg.AddMigration("save default acl rules in dashboard_acl table", new(RawSqlMigration).
Sqlite(rawSQL).
Postgres(rawSQL).
Mysql(rawSQL))
} }
...@@ -174,10 +174,10 @@ func TestAccountDataAccess(t *testing.T) { ...@@ -174,10 +174,10 @@ func TestAccountDataAccess(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 3) So(len(query.Result), ShouldEqual, 3)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: ac1.OrgId, UserId: ac3.Id, Permissions: m.PERMISSION_EDIT}) err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT})
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 2, OrgId: ac3.OrgId, UserId: ac3.Id, Permissions: m.PERMISSION_EDIT}) err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 2, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT})
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("When org user is deleted", func() { Convey("When org user is deleted", func() {
......
...@@ -94,7 +94,7 @@ func TestUserGroupCommandsAndQueries(t *testing.T) { ...@@ -94,7 +94,7 @@ func TestUserGroupCommandsAndQueries(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: groupId, UserId: userIds[2]}) err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: groupId, UserId: userIds[2]})
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permissions: m.PERMISSION_EDIT, UserGroupId: groupId}) err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permission: m.PERMISSION_EDIT, UserGroupId: groupId})
err = DeleteUserGroup(&m.DeleteUserGroupCommand{Id: groupId}) err = DeleteUserGroup(&m.DeleteUserGroupCommand{Id: groupId})
So(err, ShouldBeNil) So(err, ShouldBeNil)
......
...@@ -99,7 +99,7 @@ func TestUserDataAccess(t *testing.T) { ...@@ -99,7 +99,7 @@ func TestUserDataAccess(t *testing.T) {
err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId}) err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId})
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permissions: m.PERMISSION_EDIT}) err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT})
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"}) err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})
......
...@@ -8,9 +8,9 @@ ...@@ -8,9 +8,9 @@
<i class="fa fa-chevron-left"></i> <i class="fa fa-chevron-left"></i>
</a> </a>
<!-- <a class="navbar&#45;page&#45;btn navbar&#45;page&#45;btn&#45;&#45;search" ng&#45;click="ctrl.showSearch()"> --> <a class="navbar-page-btn navbar-page-btn--search" ng-click="ctrl.showSearch()">
<!-- <i class="fa fa&#45;search"></i> --> <i class="fa fa-search"></i>
<!-- </a> --> </a>
<div ng-if="::!ctrl.hasMenu"> <div ng-if="::!ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn"> <a href="{{::ctrl.section.url}}" class="navbar-page-btn">
......
...@@ -4,66 +4,38 @@ import _ from 'lodash'; ...@@ -4,66 +4,38 @@ import _ from 'lodash';
const template = ` const template = `
<div class="dropdown"> <div class="dropdown">
<metric-segment segment="ctrl.userGroupSegment" <gf-form-dropdown model="ctrl.group"
get-options="ctrl.debouncedSearchUserGroups($query)" get-options="ctrl.debouncedSearchGroups($query)"
on-change="ctrl.onChange()"></metric-segment> css-class="gf-size-auto"
</div> on-change="ctrl.onChange($option)"
</gf-form-dropdown>
</div>
`; `;
export class UserGroupPickerCtrl { export class UserGroupPickerCtrl {
userGroupSegment: any; group: any;
userGroupId: number; userGroupPicked: any;
debouncedSearchUserGroups: any; debouncedSearchGroups: any;
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, private $scope, $sce, private uiSegmentSrv) { constructor(private backendSrv, private $scope, $sce, private uiSegmentSrv) {
this.debouncedSearchUserGroups = _.debounce(this.searchUserGroups, 500, {'leading': true, 'trailing': false}); this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {'leading': true, 'trailing': false});
this.resetUserGroupSegment(); this.reset();
} }
resetUserGroupSegment() { reset() {
this.userGroupId = null; this.group = {text: 'Choose', value: null};
const userGroupSegment = this.uiSegmentSrv.newSegment({
value: 'Choose',
selectMode: true,
fake: true,
cssClass: 'gf-size-auto'
});
if (!this.userGroupSegment) {
this.userGroupSegment = userGroupSegment;
} else {
this.userGroupSegment.value = userGroupSegment.value;
this.userGroupSegment.html = userGroupSegment.html;
this.userGroupSegment.value = userGroupSegment.value;
}
}
userGroupIdChanged(newVal) {
if (!newVal) {
this.resetUserGroupSegment();
}
} }
searchUserGroups(query: string) { searchGroups(query: string) {
return Promise.resolve(this.backendSrv.get('/api/user-groups/search?perpage=10&page=1&query=' + query).then(result => { return Promise.resolve(this.backendSrv.get('/api/user-groups/search?perpage=10&page=1&query=' + query).then(result => {
return _.map(result.userGroups, ug => { return this.uiSegmentSrv.newSegment(ug.name); }); return _.map(result.userGroups, ug => {
return {text: ug.name, value: ug};
});
})); }));
} }
onChange() { onChange(option) {
this.backendSrv.get('/api/user-groups/search?perpage=10&page=1&query=' + this.userGroupSegment.value) this.userGroupPicked({$group: option.value});
.then(result => {
if (!result) {
return;
}
result.userGroups.forEach(ug => {
if (ug.name === this.userGroupSegment.value) {
this.userGroupId = ug.id;
}
});
});
} }
} }
...@@ -75,11 +47,11 @@ export function userGroupPicker() { ...@@ -75,11 +47,11 @@ export function userGroupPicker() {
bindToController: true, bindToController: true,
controllerAs: 'ctrl', controllerAs: 'ctrl',
scope: { scope: {
userGroupId: '=', userGroupPicked: '&',
}, },
link: function(scope, elem, attrs, ctrl) { link: function(scope, elem, attrs, ctrl) {
scope.$watch("ctrl.userGroupId", (newVal, oldVal) => { scope.$on("user-group-picker-reset", () => {
ctrl.userGroupIdChanged(newVal); ctrl.reset();
}); });
} }
}; };
......
...@@ -7,37 +7,35 @@ const template = ` ...@@ -7,37 +7,35 @@ const template = `
<gf-form-dropdown model="ctrl.user" <gf-form-dropdown model="ctrl.user"
get-options="ctrl.debouncedSearchUsers($query)" get-options="ctrl.debouncedSearchUsers($query)"
css-class="gf-size-auto" css-class="gf-size-auto"
on-change="ctrl.onChange()" on-change="ctrl.onChange($option)"
</gf-form-dropdown> </gf-form-dropdown>
</div> </div>
`; `;
export class UserPickerCtrl { export class UserPickerCtrl {
user: any; user: any;
userId: number;
debouncedSearchUsers: any; debouncedSearchUsers: any;
userPicked: any;
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, private $scope, $sce) { constructor(private backendSrv, private $scope, $sce) {
this.user = {text: 'Choose', value: null}; this.reset();
this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {'leading': true, 'trailing': false}); this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {'leading': true, 'trailing': false});
} }
searchUsers(query: string) { searchUsers(query: string) {
return Promise.resolve(this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => { return Promise.resolve(this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => {
return _.map(result.users, user => { return _.map(result.users, user => {
return {text: user.login + ' - ' + user.email, value: user.id}; return {text: user.login + ' - ' + user.email, value: user};
}); });
})); }));
} }
onChange() { onChange(option) {
this.userId = this.user.value; this.userPicked({$user: option.value});
} }
userIdChanged() { reset() {
if (this.userId === null) { this.user = {text: 'Choose', value: null};
this.user = {text: 'Choose', value: null};
}
} }
} }
...@@ -56,11 +54,11 @@ export function userPicker() { ...@@ -56,11 +54,11 @@ export function userPicker() {
bindToController: true, bindToController: true,
controllerAs: 'ctrl', controllerAs: 'ctrl',
scope: { scope: {
userId: '=', userPicked: '&',
}, },
link: function(scope, elem, attrs, ctrl) { link: function(scope, elem, attrs, ctrl) {
scope.$watch("ctrl.userId", (newVal, oldVal) => { scope.$on("user-picker-reset", () => {
ctrl.userIdChanged(newVal); ctrl.reset();
}); });
} }
}; };
......
...@@ -2,8 +2,9 @@ define([ ...@@ -2,8 +2,9 @@ define([
'jquery', 'jquery',
'angular', 'angular',
'../core_module', '../core_module',
'lodash',
], ],
function ($, angular, coreModule) { function ($, angular, coreModule, _) {
'use strict'; 'use strict';
var editViewMap = { var editViewMap = {
...@@ -12,7 +13,8 @@ function ($, angular, coreModule) { ...@@ -12,7 +13,8 @@ function ($, angular, coreModule) {
'templating': { src: 'public/app/features/templating/partials/editor.html'}, 'templating': { src: 'public/app/features/templating/partials/editor.html'},
'history': { html: '<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>'}, 'history': { html: '<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>'},
'timepicker': { src: 'public/app/features/dashboard/timepicker/dropdown.html' }, 'timepicker': { src: 'public/app/features/dashboard/timepicker/dropdown.html' },
'import': { html: '<dash-import></dash-import>' } 'import': { html: '<dash-import dismiss="dismiss()"></dash-import>', isModal: true },
'permissions': { html: '<dash-acl-modal dismiss="dismiss()"></dash-acl-modal>', isModal: true }
}; };
coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) { coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) {
...@@ -20,6 +22,7 @@ function ($, angular, coreModule) { ...@@ -20,6 +22,7 @@ function ($, angular, coreModule) {
restrict: 'A', restrict: 'A',
link: function(scope, elem) { link: function(scope, elem) {
var editorScope; var editorScope;
var modalScope;
var lastEditView; var lastEditView;
function hideEditorPane(hideToShowOtherView) { function hideEditorPane(hideToShowOtherView) {
...@@ -31,8 +34,7 @@ function ($, angular, coreModule) { ...@@ -31,8 +34,7 @@ function ($, angular, coreModule) {
function showEditorPane(evt, options) { function showEditorPane(evt, options) {
if (options.editview) { if (options.editview) {
options.src = editViewMap[options.editview].src; _.defaults(options, editViewMap[options.editview]);
options.html = editViewMap[options.editview].html;
} }
if (lastEditView && lastEditView === options.editview) { if (lastEditView && lastEditView === options.editview) {
...@@ -46,6 +48,11 @@ function ($, angular, coreModule) { ...@@ -46,6 +48,11 @@ function ($, angular, coreModule) {
editorScope = options.scope ? options.scope.$new() : scope.$new(); editorScope = options.scope ? options.scope.$new() : scope.$new();
editorScope.dismiss = function(hideToShowOtherView) { editorScope.dismiss = function(hideToShowOtherView) {
if (modalScope) {
modalScope.dismiss();
modalScope = null;
}
editorScope.$destroy(); editorScope.$destroy();
lastEditView = null; lastEditView = null;
editorScope = null; editorScope = null;
...@@ -61,19 +68,24 @@ function ($, angular, coreModule) { ...@@ -61,19 +68,24 @@ function ($, angular, coreModule) {
var urlParams = $location.search(); var urlParams = $location.search();
if (options.editview === urlParams.editview) { if (options.editview === urlParams.editview) {
delete urlParams.editview; delete urlParams.editview;
$location.search(urlParams); // hack for consistently updating url
setTimeout(function() {
$rootScope.$apply(function() {
$location.search(urlParams);
});
});
} }
} }
}; };
if (options.editview === 'import') { if (options.isModal) {
var modalScope = $rootScope.$new(); modalScope = $rootScope.$new();
modalScope.$on("$destroy", function() { modalScope.$on("$destroy", function() {
editorScope.dismiss(); editorScope.dismiss();
}); });
$rootScope.appEvent('show-modal', { $rootScope.appEvent('show-modal', {
templateHtml: '<dash-import></dash-import>', templateHtml: options.html,
scope: modalScope, scope: modalScope,
backdrop: 'static' backdrop: 'static'
}); });
......
...@@ -168,6 +168,12 @@ export class NavModelSrv { ...@@ -168,6 +168,12 @@ export class NavModelSrv {
clickHandler: () => dashNavCtrl.openEditView('annotations') clickHandler: () => dashNavCtrl.openEditView('annotations')
}); });
menu.push({
title: 'Permissions...',
icon: 'fa fa-fw fa-lock',
clickHandler: () => dashNavCtrl.openEditView('permissions')
});
if (!dashboard.meta.isHome) { if (!dashboard.meta.isHome) {
menu.push({ menu.push({
title: 'Version history', title: 'Version history',
...@@ -199,7 +205,7 @@ export class NavModelSrv { ...@@ -199,7 +205,7 @@ export class NavModelSrv {
if (this.contextSrv.isEditor && !dashboard.meta.isFolder) { if (this.contextSrv.isEditor && !dashboard.meta.isFolder) {
menu.push({ menu.push({
title: 'Save As ...', title: 'Save As...',
icon: 'fa fa-fw fa-save', icon: 'fa fa-fw fa-save',
clickHandler: () => dashNavCtrl.saveDashboardAs() clickHandler: () => dashNavCtrl.saveDashboardAs()
}); });
......
<div class="editor-row"> <div class="modal-body">
<h5 class="section-heading">Add New Permission</h5> <div class="modal-header">
<form name="addPermission" class="gf-form-group"> <h2 class="modal-header-title">
<div class="gf-form-inline"> <i class="fa fa-lock"></i>
<div class="gf-form"> <span class="p-l-1">Permissions</span>
<span class="gf-form-label">Type</span> </h2>
<select class="gf-form-input gf-size-auto" ng-model="ctrl.type" ng-options="r for r in ['User Group', 'User']"></select>
</div> <a class="modal-header-close" ng-click="ctrl.dismiss();">
<div class="gf-form" ng-show="ctrl.type === 'User'"> <i class="fa fa-remove"></i>
<span class="gf-form-label">User</span> </a>
<user-picker user-id="ctrl.userId"></user-picker> </div>
</div>
<div class="gf-form" ng-show="ctrl.type === 'User Group'">
<span class="gf-form-label">User Group</span>
<user-group-picker user-group-id="ctrl.userGroupId"></user-group-picker>
</div>
<div class="gf-form">
<span class="gf-form-label">Permission</span>
<select class="gf-form-input gf-size-auto" ng-model="ctrl.permission" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions"></select>
</div>
<div class="gf-form">
<button class="btn gf-form-btn btn-success" ng-click="ctrl.addPermission()">Add</button>
</div>
</div>
</form>
<div class="permissionlist"> <div class="modal-content">
<div class="permissionlist__section"> <table class="filter-table gf-form-group">
<div class="permissionlist__section-header"> <tr ng-repeat="acl in ctrl.aclItems">
<h6>Permissions</h6> <td style="width: 100%;">
<i class="{{acl.icon}}"></i>
<span ng-bind-html="acl.nameHtml"></span>
</td>
<td class="query-keyword">Can</td>
<td>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="acl.permission" ng-options="p.value as p.text for p in ctrl.permissionOptions" ng-change="ctrl.permissionChanged(acl)"></select>
</div>
</td>
<td>
<a class="btn btn-inverse btn-small" ng-click="ctrl.removeItem($index)">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
<tr ng-show="ctrl.aclItems.length === 0">
<td colspan="4">
<em>No permissions. Will only be accessible by admins.</em>
</td>
</tr>
</table>
<form name="addPermission" class="gf-form-group">
<h6 class="muted">Add Permission For</h6>
<div class="gf-form-inline">
<div class="gf-form">
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.newType" ng-options="p.value as p.text for p in ctrl.aclTypes" ng-change="ctrl.typeChanged()"></select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.newType === 'User'">
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
</div>
<div class="gf-form" ng-show="ctrl.newType === 'Group'">
<user-group-picker user-group-picked="ctrl.groupPicked($group)"></user-group-picker>
</div>
</div> </div>
<table class="filter-table form-inline"> </form>
<thead>
<tr> <div class="gf-form-button-row text-center">
<th style="width: 50px;"></th> <button type="button" class="btn btn-danger" ng-disabled="!ctrl.canUpdate" ng-click="ctrl.update()">
<th>Name</th> Update Permissions
<th style="width: 220px;">Permission</th> </button>
<th style="width: 120px"></th> <a class="btn-text" ng-click="ctrl.dismiss();">Close</a>
</tr> </div>
</thead>
<tbody>
<tr ng-repeat="permission in ctrl.userPermissions" class="permissionlist__item">
<td><i class="fa fa-fw fa-user"></i></td>
<td>{{permission.userLogin}}</td>
<td><select class="gf-form-input gf-size-auto" ng-model="permission.permissions" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions" ng-change="ctrl.updatePermission(permission)"></select></td>
<td class="text-right">
<a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
<tr ng-repeat="permission in ctrl.userGroupPermissions" class="permissionlist__item">
<td><i class="fa fa-fw fa-users"></i></td>
<td>{{permission.userGroup}}</td>
<td><select class="gf-form-input gf-size-auto" ng-model="permission.permissions" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions" ng-change="ctrl.updatePermission(permission)"></select></td>
<td class="text-right">
<a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
<tr ng-repeat="role in ctrl.roles" class="permissionlist__item">
<td></td>
<td>{{role.name}}</td>
<td><select class="gf-form-input gf-size-auto" ng-model="role.permissions" ng-options="p.value as p.text for p in ctrl.roleOptions" ng-change="ctrl.updatePermission(role)"></select></td>
<td class="text-right">
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div>
<!-- <br> -->
<!-- <br> -->
<!-- <br> -->
<!-- -->
<!-- <div class="permissionlist"> -->
<!-- <div class="permissionlist__section"> -->
<!-- <div class="permissionlist__section&#45;header"> -->
<!-- <h6>Permissions</h6> -->
<!-- </div> -->
<!-- <table class="filter&#45;table form&#45;inline"> -->
<!-- <thead> -->
<!-- <tr> -->
<!-- <th style="width: 50px;"></th> -->
<!-- <th>Name</th> -->
<!-- <th style="width: 220px;">Permission</th> -->
<!-- <th style="width: 120px"></th> -->
<!-- </tr> -->
<!-- </thead> -->
<!-- <tbody> -->
<!-- <tr ng&#45;repeat="permission in ctrl.userPermissions" class="permissionlist__item"> -->
<!-- <td><i class="fa fa&#45;fw fa&#45;user"></i></td> -->
<!-- <td>{{permission.userLogin}}</td> -->
<!-- <td class="text&#45;right"> -->
<!-- <a ng&#45;click="ctrl.removePermission(permission)" class="btn btn&#45;danger btn&#45;small"> -->
<!-- <i class="fa fa&#45;remove"></i> -->
<!-- </a> -->
<!-- </td> -->
<!-- </tr> -->
<!-- <tr ng&#45;repeat="permission in ctrl.userGroupPermissions" class="permissionlist__item"> -->
<!-- <td><i class="fa fa&#45;fw fa&#45;users"></i></td> -->
<!-- <td>{{permission.userGroup}}</td> -->
<!-- <td><select class="gf&#45;form&#45;input gf&#45;size&#45;auto" ng&#45;model="permission.permissions" ng&#45;options="p.value as p.text for p in ctrl.permissionTypeOptions" ng&#45;change="ctrl.updatePermission(permission)"></select></td> -->
<!-- <td class="text&#45;right"> -->
<!-- <a ng&#45;click="ctrl.removePermission(permission)" class="btn btn&#45;danger btn&#45;small"> -->
<!-- <i class="fa fa&#45;remove"></i> -->
<!-- </a> -->
<!-- </td> -->
<!-- </tr> -->
<!-- <tr ng&#45;repeat="role in ctrl.roles" class="permissionlist__item"> -->
<!-- <td></td> -->
<!-- <td>{{role.name}}</td> -->
<!-- <td><select class="gf&#45;form&#45;input gf&#45;size&#45;auto" ng&#45;model="role.permissions" ng&#45;options="p.value as p.text for p in ctrl.roleOptions" ng&#45;change="ctrl.updatePermission(role)"></select></td> -->
<!-- <td class="text&#45;right"> -->
<!-- -->
<!-- </td> -->
<!-- </tr> -->
<!-- </tbody> -->
<!-- </table> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
...@@ -5,117 +5,126 @@ import appEvents from 'app/core/app_events'; ...@@ -5,117 +5,126 @@ import appEvents from 'app/core/app_events';
import _ from 'lodash'; import _ from 'lodash';
export class AclCtrl { export class AclCtrl {
tabIndex: any;
dashboard: any; dashboard: any;
userPermissions: Permission[]; aclItems: DashboardAcl[];
userGroupPermissions: Permission[]; permissionOptions = [
permissionTypeOptions = [
{value: 1, text: 'View'}, {value: 1, text: 'View'},
{value: 2, text: 'Read-only Edit'}, {value: 2, text: 'Edit'},
{value: 4, text: 'Edit'} {value: 4, text: 'Admin'}
]; ];
aclTypes = [
roleOptions = [ {value: 'Group', text: 'User Group'},
{value: 0, text: 'None'}, {value: 'User', text: 'User'},
{value: 1, text: 'View'}, {value: 'Viewer', text: 'Everyone With Viewer Role'},
{value: 2, text: 'Read-only Edit'}, {value: 'Editor', text: 'Everyone With Editor Role'}
{value: 4, text: 'Edit'}
]; ];
roles = []; newType: string;
canUpdate: boolean;
type = 'User Group';
permission = 1;
userId: number;
userGroupId: number;
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, private $scope) { constructor(private backendSrv, private dashboardSrv, private $sce, private $scope) {
this.tabIndex = 0; this.aclItems = [];
this.userPermissions = []; this.resetNewType();
this.userGroupPermissions = []; this.dashboard = dashboardSrv.getCurrent();
this.get(this.dashboard.id); this.get(this.dashboard.id);
} }
resetNewType() {
this.newType = 'Group';
}
get(dashboardId: number) { get(dashboardId: number) {
return this.backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`) return this.backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`)
.then(result => { .then(result => {
this.userPermissions = _.filter(result, p => { return p.userId > 0;}); this.aclItems = _.map(result, this.prepareViewModel.bind(this));
this.userGroupPermissions = _.filter(result, p => { return p.userGroupId > 0;});
this.roles = this.setRoles(result);
}); });
} }
setRoles(result: any) { prepareViewModel(item: DashboardAcl): DashboardAcl {
return [ if (item.userId > 0) {
{name: 'Org Viewer', permissions: 1}, item.icon = "fa fa-fw fa-user";
{name: 'Org Read Only Editor', permissions: 2}, item.nameHtml = this.$sce.trustAsHtml(item.userLogin);
{name: 'Org Editor', permissions: 4}, } else if (item.userGroupId > 0) {
{name: 'Org Admin', permissions: 4} item.icon = "fa fa-fw fa-users";
]; item.nameHtml = this.$sce.trustAsHtml(item.userGroup);
} } else if (item.role) {
item.icon = "fa fa-fw fa-street-view";
addPermission() { item.nameHtml = this.$sce.trustAsHtml(`Everyone with <span class="query-keyword">${item.role}</span> Role`);
if (this.type === 'User') {
if (!this.userId) {
return;
}
return this.addOrUpdateUserPermission(this.userId, this.permission).then(() => {
this.userId = null;
return this.get(this.dashboard.id);
});
} else {
if (!this.userGroupId) {
return;
}
return this.addOrUpdateUserGroupPermission(this.userGroupId, this.permission).then(() => {
this.userGroupId = null;
return this.get(this.dashboard.id);
});
} }
}
addOrUpdateUserPermission(userId: number, permissionType: number) { return item;
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, {
userId: userId,
permissions: permissionType
});
} }
addOrUpdateUserGroupPermission(userGroupId: number, permissionType: number) { update() {
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, { return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, {
userGroupId: userGroupId, acl: this.aclItems.map(item => {
permissions: permissionType return {
id: item.id,
userId: item.userId,
userGroupId: item.userGroupId,
role: item.role,
permission: item.permission,
};
})
}); });
} }
updatePermission(permission: any) { typeChanged() {
if (permission.userId > 0) { if (this.newType === 'Viewer' || this.newType === 'Editor') {
return this.addOrUpdateUserPermission(permission.userId, permission.permissions); this.aclItems.push(this.prepareViewModel({
} else { permission: 1,
if (!permission.userGroupId) { role: this.newType
return; }));
}
return this.addOrUpdateUserGroupPermission(permission.userGroupId, permission.permissions); this.canUpdate = true;
this.resetNewType();
} }
} }
removePermission(permission: Permission) { permissionChanged() {
return this.backendSrv.delete(`/api/dashboards/id/${permission.dashboardId}/acl/${permission.id}`).then(() => { this.canUpdate = true;
return this.get(permission.dashboardId); }
});
userPicked(user) {
this.aclItems.push(this.prepareViewModel({
userId: user.id,
userLogin: user.login,
permission: 1,
}));
this.canUpdate = true;
this.$scope.$broadcast('user-picker-reset');
}
groupPicked(group) {
console.log(group);
this.aclItems.push(this.prepareViewModel({
userGroupId: group.id,
userGroup: group.name,
permission: 1,
}));
this.canUpdate = true;
this.$scope.$broadcast('user-group-picker-reset');
}
removeItem(index) {
this.aclItems.splice(index, 1);
this.canUpdate = true;
} }
} }
export function aclSettings() { export function dashAclModal() {
return { return {
restrict: 'E', restrict: 'E',
templateUrl: 'public/app/features/dashboard/acl/acl.html', templateUrl: 'public/app/features/dashboard/acl/acl.html',
controller: AclCtrl, controller: AclCtrl,
bindToController: true, bindToController: true,
controllerAs: 'ctrl', controllerAs: 'ctrl',
scope: { dashboard: "=" } scope: {
dismiss: "&"
}
}; };
} }
...@@ -126,19 +135,19 @@ export interface FormModel { ...@@ -126,19 +135,19 @@ export interface FormModel {
PermissionType: number; PermissionType: number;
} }
export interface Permission { export interface DashboardAcl {
id: number; id?: number;
orgId: number; dashboardId?: number;
dashboardId: number; userId?: number;
created: Date; userLogin?: number;
updated: Date; userEmail?: string;
userId: number; userGroupId?: number;
userLogin: number; userGroup?: string;
userEmail: string; permission?: number;
userGroupId: number; permissionName?: string;
userGroup: string; role?: string;
permissions: string[]; icon?: string;
permissionType: number[]; nameHtml?: string;
} }
coreModule.directive('aclSettings', aclSettings); coreModule.directive('dashAclModal', dashAclModal);
<div class="navbar"> <navbar model="ctrl.navModel">
<div class="navbar-inner">
<a class="navbar-brand-btn pointer" ng-click="ctrl.toggleSideMenu()">
<span class="navbar-brand-btn-background">
<img src="public/img/grafana_icon.svg"></img>
</span>
<i class="icon-gf icon-gf-grafana_wordmark"></i>
<i class="fa fa-caret-down"></i>
<i class="fa fa-chevron-left"></i>
</a>
<div class="navbar-section-wrapper"> <ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
<a class="navbar-page-btn" ng-click="ctrl.showSearch()"> <li>
<i class="icon-gf icon-gf-dashboard"></i> <a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
{{ctrl.dashboard.title}} </li>
<i class="fa fa-caret-down"></i> <li>
</a> <a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
</div> </li>
<li>
<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
</li>
</ul>
<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying"> <ul class="nav pull-left dashnav-action-icons">
<li> <li ng-show="::ctrl.dashboard.meta.canStar">
<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a> <a class="pointer" ng-click="ctrl.starDashboard()">
</li> <i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
</a>
</li>
<li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown">
<a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
<ul class="dropdown-menu">
<li> <li>
<a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a> <a class="pointer" ng-click="ctrl.shareDashboard(0)">
<i class="fa fa-link"></i> Link to Dashboard
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
</a>
</li> </li>
<li> <li>
<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a> <a class="pointer" ng-click="ctrl.shareDashboard(1)">
</li> <i class="icon-gf icon-gf-snapshot"></i>Snapshot
</ul> <div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
<ul class="nav pull-left dashnav-action-icons">
<li ng-show="::ctrl.dashboard.meta.canStar">
<a class="pointer" ng-click="ctrl.starDashboard()">
<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
</a> </a>
</li> </li>
<li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown"> <li>
<a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a> <a class="pointer" ng-click="ctrl.shareDashboard(2)">
<ul class="dropdown-menu"> <i class="fa fa-cloud-upload"></i>Export
<li> <div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
<a class="pointer" ng-click="ctrl.shareDashboard(0)">
<i class="fa fa-link"></i> Link to Dashboard
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(1)">
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(2)">
<i class="fa fa-cloud-upload"></i>Export
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
</a>
</li>
</ul>
</li>
<li ng-show="::ctrl.dashboard.meta.canSave">
<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
</li>
<li class="dropdown">
<a class="pointer" data-toggle="dropdown">
<i class="fa fa-cog"></i>
</a> </a>
<ul class="dropdown-menu dropdown-menu--navbar">
<li ng-repeat="navItem in ::ctrl.navModel.menu" ng-class="{active: navItem.active}">
<a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
<i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
{{::navItem.title}}
</a>
</li>
</ul>
</li> </li>
</ul> </ul>
</li>
<li ng-show="::ctrl.dashboard.meta.canSave">
<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
</li>
</ul>
<ul class="nav pull-right"> <ul class="nav pull-right">
<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard"> <li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
<a ng-click="ctrl.exitFullscreen()"> <a ng-click="ctrl.exitFullscreen()">
Back to dashboard Back to dashboard
</a> </a>
</li> </li>
<li> <li>
<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker> <gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
</li> </li>
</ul> </ul>
</div>
</div> </navbar>
<dashboard-search></dashboard-search>
...@@ -143,17 +143,6 @@ export class DashNavCtrl { ...@@ -143,17 +143,6 @@ export class DashNavCtrl {
onFolderChange(parentId) { onFolderChange(parentId) {
this.dashboard.parentId = parentId; this.dashboard.parentId = parentId;
} }
showSearch() {
this.$rootScope.appEvent('show-dash-search');
}
navItemClicked(navItem, evt) {
if (navItem.clickHandler) {
navItem.clickHandler();
evt.preventDefault();
}
}
} }
export function dashNavDirective() { export function dashNavDirective() {
......
<div class="modal-body">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-header-title"> <h2 class="modal-header-title">
......
...@@ -275,3 +275,22 @@ ...@@ -275,3 +275,22 @@
content: "\f11c"; content: "\f11c";
} }
} }
.dropdown-menu.dropdown-menu--new {
li a {
padding: $spacer/2 $spacer;
border-left: 2px solid $side-menu-bg;
background: $side-menu-bg;
i {
display: inline-block;
padding-right: 21px;
}
&:hover {
@include left-brand-border-gradient();
color: $link-hover-color;
background: $input-label-bg;
}
}
}
...@@ -149,7 +149,6 @@ ...@@ -149,7 +149,6 @@
} }
.dropdown-menu.dropdown-menu--navbar { .dropdown-menu.dropdown-menu--navbar {
top: 100%;
min-width: 100%; min-width: 100%;
margin-top: 0px; margin-top: 0px;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment