Просмотр исходного кода

Merge branch 'dashboard-acl' into dashboard_folders

Torkel Ödegaard 8 лет назад
Родитель
Сommit
aab6c98e45

+ 2 - 1
pkg/api/dashboard_acl.go

@@ -23,7 +23,8 @@ func GetDashboardAclList(c *middleware.Context) Response {
 		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 {

+ 11 - 11
pkg/api/dashboard_acl_test.go

@@ -13,16 +13,16 @@ import (
 func TestDashboardAclApiEndpoint(t *testing.T) {
 	Convey("Given a dashboard acl", t, func() {
 		mockResult := []*models.DashboardAcl{
-			{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permissions: models.PERMISSION_EDIT},
-			{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permissions: models.PERMISSION_VIEW},
-			{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permissions: models.PERMISSION_EDIT},
-			{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permissions: models.PERMISSION_READ_ONLY_EDIT},
+			{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: models.PERMISSION_EDIT},
+			{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: models.PERMISSION_VIEW},
+			{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permission: models.PERMISSION_EDIT},
+			{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permission: models.PERMISSION_READ_ONLY_EDIT},
 		}
 		dtoRes := []*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},
-			{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permissions: models.PERMISSION_EDIT},
-			{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permissions: models.PERMISSION_READ_ONLY_EDIT},
+			{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: models.PERMISSION_EDIT},
+			{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: models.PERMISSION_VIEW},
+			{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permission: models.PERMISSION_EDIT},
+			{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permission: models.PERMISSION_READ_ONLY_EDIT},
 		}
 
 		bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error {
@@ -59,7 +59,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 
 		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) {
-				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() {
 					sc.handlerFunc = GetDashboardAclList
@@ -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) {
-				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 {
 					return nil
@@ -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) {
-				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 {
 					return nil
 				})

+ 3 - 3
pkg/api/dashboard_test.go

@@ -174,7 +174,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 		aclMockResp := []*models.DashboardAcl{
 			{
 				DashboardId: 1,
-				Permissions: models.PERMISSION_EDIT,
+				Permission:  models.PERMISSION_EDIT,
 				UserId:      200,
 			},
 		}
@@ -273,7 +273,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			role := models.ROLE_VIEWER
 
 			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 {
@@ -315,7 +315,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			role := models.ROLE_EDITOR
 
 			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 {

+ 8 - 7
pkg/models/dashboard_acl.go

@@ -9,15 +9,15 @@ type PermissionType int
 
 const (
 	PERMISSION_VIEW PermissionType = 1 << iota
-	PERMISSION_READ_ONLY_EDIT
 	PERMISSION_EDIT
+	PERMISSION_ADMIN
 )
 
 func (p PermissionType) String() string {
 	names := map[int]string{
-		int(PERMISSION_VIEW):           "View",
-		int(PERMISSION_READ_ONLY_EDIT): "Read-only Edit",
-		int(PERMISSION_EDIT):           "Edit",
+		int(PERMISSION_VIEW):  "View",
+		int(PERMISSION_EDIT):  "Edit",
+		int(PERMISSION_ADMIN): "Admin",
 	}
 	return names[int(p)]
 }
@@ -36,7 +36,7 @@ type DashboardAcl struct {
 
 	UserId      int64
 	UserGroupId int64
-	Permissions PermissionType
+	Permission  PermissionType
 
 	Created time.Time
 	Updated time.Time
@@ -55,7 +55,8 @@ type DashboardAclInfoDTO struct {
 	UserEmail      string         `json:"userEmail"`
 	UserGroupId    int64          `json:"userGroupId"`
 	UserGroup      string         `json:"userGroup"`
-	Permissions    PermissionType `json:"permissions"`
+	Role           RoleType       `json:"role"`
+	Permission     PermissionType `json:"permission"`
 	PermissionName string         `json:"permissionName"`
 }
 
@@ -68,7 +69,7 @@ type SetDashboardAclCommand struct {
 	OrgId       int64          `json:"-"`
 	UserId      int64          `json:"userId"`
 	UserGroupId int64          `json:"userGroupId"`
-	Permissions PermissionType `json:"permissions" binding:"Required"`
+	Permission  PermissionType `json:"permission" binding:"Required"`
 
 	Result DashboardAcl `json:"-"`
 }

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

@@ -29,7 +29,7 @@ func (g *DashboardGuardian) CanSave() (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) {
@@ -57,12 +57,12 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType, fallbackR
 	}
 
 	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
 		}
 
 		for _, ug := range userGroups {
-			if ug.Id == p.UserGroupId && p.Permissions >= permission {
+			if ug.Id == p.UserGroupId && p.Permission >= permission {
 				return true, nil
 			}
 		}

+ 49 - 25
pkg/services/sqlstore/dashboard_acl.go

@@ -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 {
 			return err
 		} else if len(res) == 1 {
+
 			entity := m.DashboardAcl{
-				Permissions: cmd.Permissions,
-				Updated:     time.Now(),
+				Permission: cmd.Permission,
+				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
 			}
 
@@ -45,10 +47,10 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
 			Created:     time.Now(),
 			Updated:     time.Now(),
 			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 {
 			cols = append(cols, "user_id")
@@ -58,12 +60,12 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
 			cols = append(cols, "user_group_id")
 		}
 
-		entityId, err := sess.Cols(cols...).Insert(&entity)
+		_, err := sess.Cols(cols...).Insert(&entity)
 		if err != nil {
 			return err
 		}
+
 		cmd.Result = entity
-		cmd.Result.Id = entityId
 
 		// Update dashboard HasAcl flag
 		dashboard := m.Dashboard{
@@ -97,7 +99,7 @@ func GetInheritedDashboardAcl(query *m.GetInheritedDashboardAclQuery) error {
   da.dashboard_id,
   da.user_id,
   da.user_group_id,
-  da.permissions,
+  da.permission,
   da.created,
   da.updated
   FROM dashboard_acl as da
@@ -112,29 +114,51 @@ func GetInheritedDashboardAcl(query *m.GetInheritedDashboardAclQuery) error {
 }
 
 func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
-	rawSQL := `SELECT
-  da.id,
-  da.org_id,
-  da.dashboard_id,
-  da.user_id,
-  da.user_group_id,
-  da.permissions,
-  da.created,
-  da.updated,
-  u.login AS user_login,
-  u.email AS user_email,
-  ug.name AS user_group
+	rawSQL := `
+	SELECT
+		da.id,
+		da.org_id,
+		da.dashboard_id,
+		da.user_id,
+		da.user_group_id,
+		da.permission,
+		da.role,
+		da.created,
+		da.updated,
+		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=?`
+		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 = ?
+
+	-- 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)
 
-	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 {
-		p.PermissionName = p.Permissions.String()
+		p.PermissionName = p.Permission.String()
 	}
 
 	return err

+ 39 - 29
pkg/services/sqlstore/dashboard_acl_test.go

@@ -20,7 +20,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
 				err := SetDashboardAcl(&m.SetDashboardAclCommand{
 					OrgId:       1,
 					DashboardId: savedFolder.Id,
-					Permissions: m.PERMISSION_EDIT,
+					Permission:  m.PERMISSION_EDIT,
 				})
 				So(err, ShouldEqual, m.ErrDashboardAclInfoMissing)
 			})
@@ -30,7 +30,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
 					OrgId:       1,
 					UserId:      currentUser.Id,
 					DashboardId: savedFolder.Id,
-					Permissions: m.PERMISSION_EDIT,
+					Permission:  m.PERMISSION_EDIT,
 				})
 				So(err, ShouldBeNil)
 
@@ -49,7 +49,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
 						OrgId:       1,
 						UserId:      currentUser.Id,
 						DashboardId: childDash.Id,
-						Permissions: m.PERMISSION_EDIT,
+						Permission:  m.PERMISSION_EDIT,
 					})
 					So(err, ShouldBeNil)
 
@@ -67,23 +67,29 @@ func TestDashboardAclDataAccess(t *testing.T) {
 			})
 
 			Convey("Should be able to add dashboard permission", func() {
-				err := SetDashboardAcl(&m.SetDashboardAclCommand{
+				setDashAclCmd := m.SetDashboardAclCommand{
 					OrgId:       1,
 					UserId:      currentUser.Id,
 					DashboardId: savedFolder.Id,
-					Permissions: m.PERMISSION_EDIT,
-				})
+					Permission:  m.PERMISSION_EDIT,
+				}
+
+				err := SetDashboardAcl(&setDashAclCmd)
 				So(err, ShouldBeNil)
 
+				So(setDashAclCmd.Result.Id, ShouldEqual, 3)
+
 				q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
 				err = GetDashboardAclInfoList(q1)
 				So(err, ShouldBeNil)
+
 				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].UserId, ShouldEqual, currentUser.Id)
 				So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login)
 				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() {
 					q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}}
@@ -98,8 +104,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
 						OrgId:       1,
 						UserId:      1,
 						DashboardId: savedFolder.Id,
-						Permissions: m.PERMISSION_READ_ONLY_EDIT,
+						Permission:  m.PERMISSION_ADMIN,
 					})
+
 					So(err, ShouldBeNil)
 
 					q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
@@ -107,7 +114,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
 					So(err, ShouldBeNil)
 					So(len(q3.Result), ShouldEqual, 1)
 					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)
 
 				})
@@ -115,8 +122,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
 				Convey("Should be able to delete an existing permission", func() {
 					err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
 						OrgId: 1,
-						AclId: 1,
+						AclId: setDashAclCmd.Result.Id,
 					})
+
 					So(err, ShouldBeNil)
 
 					q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
@@ -132,20 +140,35 @@ func TestDashboardAclDataAccess(t *testing.T) {
 				So(err, ShouldBeNil)
 
 				Convey("Should be able to add a user permission for a user group", func() {
-					err := SetDashboardAcl(&m.SetDashboardAclCommand{
+					setDashAclCmd := m.SetDashboardAclCommand{
 						OrgId:       1,
 						UserGroupId: group1.Result.Id,
 						DashboardId: savedFolder.Id,
-						Permissions: m.PERMISSION_EDIT,
-					})
+						Permission:  m.PERMISSION_EDIT,
+					}
+
+					err := SetDashboardAcl(&setDashAclCmd)
 					So(err, ShouldBeNil)
 
 					q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
 					err = GetDashboardAclInfoList(q1)
 					So(err, ShouldBeNil)
 					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)
+
+					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() {
@@ -153,7 +176,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
 						OrgId:       1,
 						UserGroupId: group1.Result.Id,
 						DashboardId: savedFolder.Id,
-						Permissions: m.PERMISSION_READ_ONLY_EDIT,
+						Permission:  m.PERMISSION_ADMIN,
 					})
 					So(err, ShouldBeNil)
 
@@ -162,23 +185,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
 					So(err, ShouldBeNil)
 					So(len(q3.Result), ShouldEqual, 1)
 					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)
-
 				})
 
-				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)
-				})
 			})
 		})
 	})

+ 1 - 1
pkg/services/sqlstore/dashboard_test.go

@@ -384,7 +384,7 @@ func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.Permis
 		OrgId:       1,
 		UserId:      userId,
 		DashboardId: dashId,
-		Permissions: permissions,
+		Permission:  permissions,
 	})
 	So(err, ShouldBeNil)
 }

+ 25 - 4
pkg/services/sqlstore/migrations/dashboard_acl.go

@@ -11,21 +11,42 @@ func addDashboardAclMigrations(mg *Migrator) {
 			{Name: "dashboard_id", Type: DB_BigInt},
 			{Name: "user_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: "updated", Type: DB_DateTime, Nullable: false},
 		},
 		Indices: []*Index{
-			{Cols: []string{"org_id"}},
+			{Cols: []string{"dashboard_id"}},
 			{Cols: []string{"dashboard_id", "user_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 ------------------
-	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_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))
 }

+ 2 - 2
pkg/services/sqlstore/org_test.go

@@ -174,10 +174,10 @@ func TestAccountDataAccess(t *testing.T) {
 					So(err, ShouldBeNil)
 					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)
 
-					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)
 
 					Convey("When org user is deleted", func() {

+ 1 - 1
pkg/services/sqlstore/user_group_test.go

@@ -94,7 +94,7 @@ func TestUserGroupCommandsAndQueries(t *testing.T) {
 				So(err, ShouldBeNil)
 				err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: groupId, UserId: userIds[2]})
 				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})
 				So(err, ShouldBeNil)

+ 1 - 1
pkg/services/sqlstore/user_test.go

@@ -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})
 				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)
 
 				err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})

+ 3 - 3
public/app/core/components/navbar/navbar.html

@@ -8,9 +8,9 @@
 		<i class="fa fa-chevron-left"></i>
 	</a>
 
-  <!-- <a class="navbar&#45;page&#45;btn navbar&#45;page&#45;btn&#45;&#45;search" ng&#45;click="ctrl.showSearch()"> -->
-	<!-- 	<i class="fa fa&#45;search"></i> -->
-	<!-- </a> -->
+  <a class="navbar-page-btn navbar-page-btn--search" ng-click="ctrl.showSearch()">
+		<i class="fa fa-search"></i>
+	</a>
 
 	<div ng-if="::!ctrl.hasMenu">
 		<a href="{{::ctrl.section.url}}" class="navbar-page-btn">

+ 22 - 50
public/app/core/components/user_group_picker.ts

@@ -4,66 +4,38 @@ import _ from 'lodash';
 
 const template = `
 <div class="dropdown">
-  <metric-segment segment="ctrl.userGroupSegment"
-    get-options="ctrl.debouncedSearchUserGroups($query)"
-    on-change="ctrl.onChange()"></metric-segment>
-  </div>
+  <gf-form-dropdown model="ctrl.group"
+                    get-options="ctrl.debouncedSearchGroups($query)"
+                    css-class="gf-size-auto"
+                    on-change="ctrl.onChange($option)"
+  </gf-form-dropdown>
+</div>
 `;
 export class UserGroupPickerCtrl {
-  userGroupSegment: any;
-  userGroupId: number;
-  debouncedSearchUserGroups: any;
+  group: any;
+  userGroupPicked: any;
+  debouncedSearchGroups: any;
 
   /** @ngInject */
   constructor(private backendSrv, private $scope, $sce, private uiSegmentSrv) {
-    this.debouncedSearchUserGroups = _.debounce(this.searchUserGroups, 500, {'leading': true, 'trailing': false});
-    this.resetUserGroupSegment();
+    this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {'leading': true, 'trailing': false});
+    this.reset();
   }
 
-  resetUserGroupSegment() {
-    this.userGroupId = 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();
-    }
+  reset() {
+    this.group = {text: 'Choose', value: null};
   }
 
-  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 _.map(result.userGroups, ug => { return this.uiSegmentSrv.newSegment(ug.name); });
+      return _.map(result.userGroups, ug => {
+        return {text: ug.name, value: ug};
+      });
     }));
   }
 
-  onChange() {
-    this.backendSrv.get('/api/user-groups/search?perpage=10&page=1&query=' + this.userGroupSegment.value)
-      .then(result => {
-        if (!result) {
-          return;
-        }
-
-        result.userGroups.forEach(ug => {
-          if (ug.name === this.userGroupSegment.value) {
-            this.userGroupId = ug.id;
-          }
-        });
-    });
+  onChange(option) {
+    this.userGroupPicked({$group: option.value});
   }
 }
 
@@ -75,11 +47,11 @@ export function userGroupPicker() {
     bindToController: true,
     controllerAs: 'ctrl',
     scope: {
-      userGroupId: '=',
+      userGroupPicked: '&',
     },
     link: function(scope, elem, attrs, ctrl) {
-      scope.$watch("ctrl.userGroupId", (newVal, oldVal) => {
-        ctrl.userGroupIdChanged(newVal);
+      scope.$on("user-group-picker-reset", () => {
+        ctrl.reset();
       });
     }
   };

+ 11 - 13
public/app/core/components/user_picker.ts

@@ -7,37 +7,35 @@ const template = `
   <gf-form-dropdown model="ctrl.user"
                     get-options="ctrl.debouncedSearchUsers($query)"
                     css-class="gf-size-auto"
-                    on-change="ctrl.onChange()"
+                    on-change="ctrl.onChange($option)"
   </gf-form-dropdown>
 </div>
 `;
 export class UserPickerCtrl {
   user: any;
-  userId: number;
   debouncedSearchUsers: any;
+  userPicked: any;
 
   /** @ngInject */
   constructor(private backendSrv, private $scope, $sce) {
-    this.user = {text: 'Choose', value: null};
+    this.reset();
     this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {'leading': true, 'trailing': false});
   }
 
   searchUsers(query: string) {
     return Promise.resolve(this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => {
       return _.map(result.users, user => {
-        return {text: user.login + ' -  ' + user.email, value: user.id};
+        return {text: user.login + ' -  ' + user.email, value: user};
       });
     }));
   }
 
-  onChange() {
-    this.userId = this.user.value;
+  onChange(option) {
+    this.userPicked({$user: option.value});
   }
 
-  userIdChanged() {
-    if (this.userId === null) {
-      this.user = {text: 'Choose', value: null};
-    }
+  reset() {
+    this.user = {text: 'Choose', value: null};
   }
 }
 
@@ -56,11 +54,11 @@ export function userPicker() {
     bindToController: true,
     controllerAs: 'ctrl',
     scope: {
-      userId: '=',
+      userPicked: '&',
     },
     link: function(scope, elem, attrs, ctrl) {
-      scope.$watch("ctrl.userId", (newVal, oldVal) => {
-        ctrl.userIdChanged(newVal);
+      scope.$on("user-picker-reset", () => {
+        ctrl.reset();
       });
     }
   };

+ 20 - 8
public/app/core/directives/dash_edit_link.js

@@ -2,8 +2,9 @@ define([
   'jquery',
   'angular',
   '../core_module',
+  'lodash',
 ],
-function ($, angular, coreModule) {
+function ($, angular, coreModule, _) {
   'use strict';
 
   var editViewMap = {
@@ -12,7 +13,8 @@ function ($, angular, coreModule) {
     'templating':  { src: 'public/app/features/templating/partials/editor.html'},
     'history':     { html: '<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>'},
     '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) {
@@ -20,6 +22,7 @@ function ($, angular, coreModule) {
       restrict: 'A',
       link: function(scope, elem) {
         var editorScope;
+        var modalScope;
         var lastEditView;
 
         function hideEditorPane(hideToShowOtherView) {
@@ -31,8 +34,7 @@ function ($, angular, coreModule) {
 
         function showEditorPane(evt, options) {
           if (options.editview) {
-            options.src = editViewMap[options.editview].src;
-            options.html = editViewMap[options.editview].html;
+            _.defaults(options, editViewMap[options.editview]);
           }
 
           if (lastEditView && lastEditView === options.editview) {
@@ -46,6 +48,11 @@ function ($, angular, coreModule) {
           editorScope = options.scope ? options.scope.$new() : scope.$new();
 
           editorScope.dismiss = function(hideToShowOtherView) {
+            if (modalScope) {
+              modalScope.dismiss();
+              modalScope = null;
+            }
+
             editorScope.$destroy();
             lastEditView = null;
             editorScope = null;
@@ -61,19 +68,24 @@ function ($, angular, coreModule) {
               var urlParams = $location.search();
               if (options.editview === 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') {
-            var modalScope = $rootScope.$new();
+          if (options.isModal) {
+            modalScope = $rootScope.$new();
             modalScope.$on("$destroy", function() {
               editorScope.dismiss();
             });
 
             $rootScope.appEvent('show-modal', {
-              templateHtml: '<dash-import></dash-import>',
+              templateHtml: options.html,
               scope: modalScope,
               backdrop: 'static'
             });

+ 7 - 1
public/app/core/nav_model_srv.ts

@@ -168,6 +168,12 @@ export class NavModelSrv {
         clickHandler: () => dashNavCtrl.openEditView('annotations')
       });
 
+      menu.push({
+        title: 'Permissions...',
+        icon: 'fa fa-fw fa-lock',
+        clickHandler: () => dashNavCtrl.openEditView('permissions')
+      });
+
       if (!dashboard.meta.isHome) {
         menu.push({
           title: 'Version history',
@@ -199,7 +205,7 @@ export class NavModelSrv {
 
     if (this.contextSrv.isEditor && !dashboard.meta.isFolder) {
       menu.push({
-        title: 'Save As ...',
+        title: 'Save As...',
         icon: 'fa fa-fw fa-save',
         clickHandler: () => dashNavCtrl.saveDashboardAs()
       });

+ 111 - 69
public/app/features/dashboard/acl/acl.html

@@ -1,74 +1,116 @@
-<div class="editor-row">
-	<h5 class="section-heading">Add New Permission</h5>
-	<form name="addPermission" class="gf-form-group">
-		<div class="gf-form-inline">
-      <div class="gf-form">
-				<span class="gf-form-label">Type</span>
-				<select class="gf-form-input gf-size-auto" ng-model="ctrl.type" ng-options="r for r in ['User Group', 'User']"></select>
-			</div>
-      <div class="gf-form" ng-show="ctrl.type === 'User'">
-        <span class="gf-form-label">User</span>
-        <user-picker user-id="ctrl.userId"></user-picker>
-      </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="modal-body">
+  <div class="modal-header">
+    <h2 class="modal-header-title">
+      <i class="fa fa-lock"></i>
+      <span class="p-l-1">Permissions</span>
+    </h2>
+
+    <a class="modal-header-close" ng-click="ctrl.dismiss();">
+      <i class="fa fa-remove"></i>
+    </a>
+  </div>
 
-  <div class="permissionlist">
-    <div class="permissionlist__section">
-      <div class="permissionlist__section-header">
-        <h6>Permissions</h6>
+  <div class="modal-content">
+    <table class="filter-table gf-form-group">
+      <tr ng-repeat="acl in ctrl.aclItems">
+        <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>
-      <table class="filter-table form-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-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">
+    </form>
+
+    <div class="gf-form-button-row text-center">
+      <button type="button" class="btn btn-danger" ng-disabled="!ctrl.canUpdate" ng-click="ctrl.update()">
+        Update Permissions
+      </button>
+      <a class="btn-text" ng-click="ctrl.dismiss();">Close</a>
+    </div>
 
-            </td>
-          </tr>
-        </tbody>
-      </table>
     </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> -->

+ 98 - 89
public/app/features/dashboard/acl/acl.ts

@@ -5,117 +5,126 @@ import appEvents from 'app/core/app_events';
 import _ from 'lodash';
 
 export class AclCtrl {
-  tabIndex: any;
   dashboard: any;
-  userPermissions: Permission[];
-  userGroupPermissions: Permission[];
-  permissionTypeOptions = [
+  aclItems: DashboardAcl[];
+  permissionOptions = [
     {value: 1, text: 'View'},
-    {value: 2, text: 'Read-only Edit'},
-    {value: 4, text: 'Edit'}
+    {value: 2, text: 'Edit'},
+    {value: 4, text: 'Admin'}
   ];
-
-  roleOptions = [
-    {value: 0, text: 'None'},
-    {value: 1, text: 'View'},
-    {value: 2, text: 'Read-only Edit'},
-    {value: 4, text: 'Edit'}
+  aclTypes = [
+    {value: 'Group', text: 'User Group'},
+    {value: 'User',  text: 'User'},
+    {value: 'Viewer', text: 'Everyone With Viewer Role'},
+    {value: 'Editor', text: 'Everyone With Editor Role'}
   ];
 
-  roles = [];
-
-  type = 'User Group';
-  permission = 1;
-  userId: number;
-  userGroupId: number;
+  newType: string;
+  canUpdate: boolean;
 
   /** @ngInject */
-  constructor(private backendSrv, private $scope) {
-    this.tabIndex = 0;
-    this.userPermissions = [];
-    this.userGroupPermissions = [];
+  constructor(private backendSrv, private dashboardSrv, private $sce, private $scope) {
+    this.aclItems = [];
+    this.resetNewType();
+    this.dashboard = dashboardSrv.getCurrent();
     this.get(this.dashboard.id);
   }
 
+  resetNewType() {
+    this.newType = 'Group';
+  }
+
   get(dashboardId: number) {
     return this.backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`)
       .then(result => {
-        this.userPermissions = _.filter(result, p => { return p.userId > 0;});
-        this.userGroupPermissions = _.filter(result, p => { return p.userGroupId > 0;});
-        this.roles = this.setRoles(result);
+        this.aclItems = _.map(result, this.prepareViewModel.bind(this));
       });
   }
 
-  setRoles(result: any) {
-    return [
-      {name: 'Org Viewer', permissions: 1},
-      {name: 'Org Read Only Editor', permissions: 2},
-      {name: 'Org Editor', permissions: 4},
-      {name: 'Org Admin', permissions: 4}
-    ];
-  }
-
-  addPermission() {
-    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);
-      });
+  prepareViewModel(item: DashboardAcl): DashboardAcl {
+    if (item.userId > 0) {
+      item.icon = "fa fa-fw fa-user";
+      item.nameHtml = this.$sce.trustAsHtml(item.userLogin);
+    } else if (item.userGroupId > 0) {
+      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";
+      item.nameHtml = this.$sce.trustAsHtml(`Everyone with <span class="query-keyword">${item.role}</span> Role`);
     }
-  }
 
-  addOrUpdateUserPermission(userId: number, permissionType: number) {
-    return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, {
-      userId: userId,
-      permissions: permissionType
-    });
+    return item;
   }
 
-  addOrUpdateUserGroupPermission(userGroupId: number, permissionType: number) {
+  update() {
     return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, {
-      userGroupId: userGroupId,
-      permissions: permissionType
+      acl: this.aclItems.map(item => {
+        return {
+          id: item.id,
+          userId: item.userId,
+          userGroupId: item.userGroupId,
+          role: item.role,
+          permission: item.permission,
+        };
+      })
     });
   }
 
-  updatePermission(permission: any) {
-    if (permission.userId > 0) {
-      return this.addOrUpdateUserPermission(permission.userId, permission.permissions);
-    } else {
-      if (!permission.userGroupId) {
-        return;
-      }
-      return this.addOrUpdateUserGroupPermission(permission.userGroupId, permission.permissions);
+  typeChanged() {
+    if (this.newType === 'Viewer' || this.newType === 'Editor') {
+      this.aclItems.push(this.prepareViewModel({
+        permission: 1,
+        role: this.newType
+      }));
+
+      this.canUpdate = true;
+      this.resetNewType();
     }
   }
 
-  removePermission(permission: Permission) {
-    return this.backendSrv.delete(`/api/dashboards/id/${permission.dashboardId}/acl/${permission.id}`).then(() => {
-      return this.get(permission.dashboardId);
-    });
+  permissionChanged() {
+    this.canUpdate = true;
+  }
+
+  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 {
     restrict: 'E',
     templateUrl: 'public/app/features/dashboard/acl/acl.html',
     controller: AclCtrl,
     bindToController: true,
     controllerAs: 'ctrl',
-    scope: { dashboard: "=" }
+    scope: {
+      dismiss: "&"
+    }
   };
 }
 
@@ -126,19 +135,19 @@ export interface FormModel {
   PermissionType: number;
 }
 
-export interface Permission {
-  id: number;
-  orgId: number;
-  dashboardId: number;
-  created: Date;
-  updated: Date;
-  userId: number;
-  userLogin: number;
-  userEmail: string;
-  userGroupId: number;
-  userGroup: string;
-  permissions: string[];
-  permissionType: number[];
+export interface DashboardAcl {
+  id?: number;
+  dashboardId?: number;
+  userId?: number;
+  userLogin?: number;
+  userEmail?: string;
+  userGroupId?: number;
+  userGroup?: string;
+  permission?: number;
+  permissionName?: string;
+  role?: string;
+  icon?: string;
+  nameHtml?: string;
 }
 
-coreModule.directive('aclSettings', aclSettings);
+coreModule.directive('dashAclModal', dashAclModal);

+ 52 - 83
public/app/features/dashboard/dashnav/dashnav.html

@@ -1,95 +1,64 @@
-<div class="navbar">
-	<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>
+<navbar model="ctrl.navModel">
 
-		<div class="navbar-section-wrapper">
-			<a class="navbar-page-btn" ng-click="ctrl.showSearch()">
-				<i class="icon-gf icon-gf-dashboard"></i>
-				{{ctrl.dashboard.title}}
-				<i class="fa fa-caret-down"></i>
-			</a>
-		</div>
+<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
+	<li>
+		<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
+	</li>
+	<li>
+		<a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
+	</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">
-			<li>
-				<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
-			</li>
+<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>
+	</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>
-				<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>
-				<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
-			</li>
-		</ul>
-
-		<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 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 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>
-						<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>
+			<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>
-				<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>
 		</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">
-			<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
-				<a ng-click="ctrl.exitFullscreen()">
-					Back to dashboard
-				</a>
-			</li>
-			<li>
-				<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
-			</li>
-		</ul>
-	</div>
-</div>
+<ul class="nav pull-right">
+	<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
+		<a ng-click="ctrl.exitFullscreen()">
+			Back to dashboard
+		</a>
+	</li>
+	<li>
+		<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
+	</li>
+</ul>
+
+</navbar>
 
-<dashboard-search></dashboard-search>

+ 0 - 11
public/app/features/dashboard/dashnav/dashnav.ts

@@ -143,17 +143,6 @@ export class DashNavCtrl {
     onFolderChange(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() {

+ 0 - 1
public/app/features/dashboard/import/dash_import.html

@@ -1,4 +1,3 @@
-<div class="modal-body">
 
 	<div class="modal-header">
 		<h2 class="modal-header-title">

+ 19 - 0
public/sass/components/_dropdown.scss

@@ -275,3 +275,22 @@
     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;
+    }
+  }
+}

+ 0 - 1
public/sass/components/_navbar.scss

@@ -149,7 +149,6 @@
 }
 
 .dropdown-menu.dropdown-menu--navbar {
-  top: 100%;
   min-width: 100%;
   margin-top: 0px;