Commit 957e1c18 by Torkel Ödegaard

Merge branch 'develop' into develop-newgrid

parents a0a2eda5 eb18bfea
......@@ -132,6 +132,18 @@ func (hs *HttpServer) registerRoutes() {
r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
}, reqGrafanaAdmin)
// user group (admin permission required)
r.Group("/user-groups", func() {
r.Get("/:userGroupId", wrap(GetUserGroupById))
r.Get("/search", wrap(SearchUserGroups))
r.Post("/", quota("user-groups"), bind(m.CreateUserGroupCommand{}), wrap(CreateUserGroup))
r.Put("/:userGroupId", bind(m.UpdateUserGroupCommand{}), wrap(UpdateUserGroup))
r.Delete("/:userGroupId", wrap(DeleteUserGroupById))
r.Get("/:userGroupId/members", wrap(GetUserGroupMembers))
r.Post("/:userGroupId/members", quota("user-groups"), bind(m.AddUserGroupMemberCommand{}), wrap(AddUserGroupMember))
r.Delete("/:userGroupId/members/:userId", wrap(RemoveUserGroupMember))
}, reqOrgAdmin)
// org information available to all users.
r.Group("/org", func() {
r.Get("/", wrap(GetOrgCurrent))
......@@ -222,20 +234,30 @@ func (hs *HttpServer) registerRoutes() {
// Dashboard
r.Group("/dashboards", func() {
r.Get("/db/:slug", GetDashboard)
r.Delete("/db/:slug", reqEditorRole, DeleteDashboard)
r.Get("/db/:slug", wrap(GetDashboard))
r.Delete("/db/:slug", wrap(DeleteDashboard))
r.Post("/db", bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
r.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
r.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
r.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
r.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
r.Get("/file/:file", GetDashboardFromJsonFile)
r.Get("/home", wrap(GetHomeDashboard))
r.Get("/tags", GetDashboardTags)
r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
r.Group("/id/:dashboardId", func() {
r.Get("/versions", wrap(GetDashboardVersions))
r.Get("/versions/:id", wrap(GetDashboardVersion))
r.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
r.Group("/acl", func() {
r.Get("/", wrap(GetDashboardAclList))
r.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
r.Delete("/:aclId", wrap(DeleteDashboardAcl))
})
}, reqSignedIn)
})
// Dashboard snapshots
......
package api
import (
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetDashboardAclList(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
acl, err := guardian.GetAcl()
if err != nil {
return ApiError(500, "Failed to get dashboard acl", err)
}
return Json(200, acl)
}
func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.UpdateDashboardAclCommand{}
cmd.DashboardId = dashId
for _, item := range apiCmd.Items {
cmd.Items = append(cmd.Items, &m.DashboardAcl{
OrgId: c.OrgId,
DashboardId: dashId,
UserId: item.UserId,
UserGroupId: item.UserGroupId,
Role: item.Role,
Permission: item.Permission,
Created: time.Now(),
Updated: time.Now(),
})
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
return ApiError(409, err.Error(), err)
}
return ApiError(500, "Failed to create permission", err)
}
metrics.M_Api_Dashboard_Acl_Update.Inc(1)
return ApiSuccess("Dashboard acl updated")
}
func DeleteDashboardAcl(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
aclId := c.ParamsInt64(":aclId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete permission for user", err)
}
return Json(200, "")
}
package api
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardAclApiEndpoint(t *testing.T) {
Convey("Given a dashboard acl", t, func() {
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permission: m.PERMISSION_VIEW},
{Id: 5, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permission: m.PERMISSION_ADMIN},
}
dtoRes := transformDashboardAclsToDTOs(mockResult)
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = dtoRes
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
userGroupResp := []*m.UserGroup{}
bus.AddHandler("test", func(query *m.GetUserGroupsByUserQuery) error {
query.Result = userGroupResp
return nil
})
Convey("When user is org admin", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 5)
So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
})
})
})
Convey("When user is editor and has admin permission in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is a member of a user group in the ACL with admin permission", func() {
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
userGroupResp = append(userGroupResp, &m.UserGroup{Id: 2, OrgId: 1, Name: "UG2"})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
Convey("When user is editor and has edit permission in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
Convey("Should not be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be not be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is editor and not in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
Convey("Should not be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be not be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
})
}
func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardAclInfoDTO {
dtos := make([]*m.DashboardAclInfoDTO, 0)
for _, acl := range acls {
dto := &m.DashboardAclInfoDTO{
Id: acl.Id,
OrgId: acl.OrgId,
DashboardId: acl.DashboardId,
Permission: acl.Permission,
UserId: acl.UserId,
UserGroupId: acl.UserGroupId,
}
dtos = append(dtos, dto)
}
return dtos
}
......@@ -56,6 +56,10 @@ func TestDataSourcesProxy(t *testing.T) {
}
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn)
}
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
......@@ -77,7 +81,7 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = models.ROLE_EDITOR
sc.context.OrgRole = role
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)
}
......@@ -85,7 +89,12 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
return nil
})
sc.m.Get(url, sc.defaultHandler)
switch method {
case "GET":
sc.m.Get(routePattern, sc.defaultHandler)
case "DELETE":
sc.m.Delete(routePattern, sc.defaultHandler)
}
fn(sc)
})
......
package dtos
import (
m "github.com/grafana/grafana/pkg/models"
)
type UpdateDashboardAclCommand struct {
Items []DashboardAclUpdateItem `json:"items"`
}
type DashboardAclUpdateItem struct {
UserId int64 `json:"userId"`
UserGroupId int64 `json:"userGroupId"`
Role *m.RoleType `json:"role,omitempty"`
Permission m.PermissionType `json:"permission"`
}
......@@ -7,20 +7,25 @@ import (
)
type DashboardMeta struct {
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"`
}
type DashboardFullWithMeta struct {
......
......@@ -84,16 +84,23 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
data.User.LightTheme = true
}
dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/"},
{Text: "Playlists", Url: setting.AppSubUrl + "/playlists"},
{Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"},
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "New",
Icon: "fa fa-fw fa-plus",
Url: "",
Children: []*dtos.NavLink{
{Text: "Dashboard", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new"},
{Text: "Folder", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"},
{Text: "Import", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"},
},
})
}
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true})
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"})
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"})
dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home"},
{Text: "Playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"},
{Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-snapshot"},
}
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
......@@ -105,8 +112,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
alertChildNavs := []*dtos.NavLink{
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"},
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"},
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list", Icon: "fa fa-fw fa-list-ul"},
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "fa fa-fw fa-bell-o"},
}
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
......@@ -122,12 +129,20 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Text: "Data Sources",
Icon: "icon-gf icon-gf-datasources",
Url: setting.AppSubUrl + "/datasources",
Children: []*dtos.NavLink{
{Text: "List", Url: setting.AppSubUrl + "/datasources", Icon: "icon-gf icon-gf-datasources"},
{Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"},
},
})
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Plugins",
Icon: "icon-gf icon-gf-apps",
Url: setting.AppSubUrl + "/plugins",
Children: []*dtos.NavLink{
{Text: "Panels", Url: setting.AppSubUrl + "/plugins?type=panel", Icon: "fa fa-fw fa-stop"},
{Text: "Data sources", Url: setting.AppSubUrl + "/plugins?type=datasource", Icon: "icon-gf icon-gf-datasources"},
{Text: "Apps", Url: setting.AppSubUrl + "/plugins?type=app", Icon: "icon-gf icon-gf-apps"},
},
})
}
......
......@@ -130,7 +130,7 @@ func GetPlaylistItems(c *middleware.Context) Response {
func GetPlaylistDashboards(c *middleware.Context) Response {
playlistId := c.ParamsInt64(":id")
playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId)
playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId)
if err != nil {
return ApiError(500, "Could not load dashboards", err)
}
......
......@@ -34,18 +34,18 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
return result, nil
}
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
result := make(dtos.PlaylistDashboardsSlice, 0)
if len(dashboardByTag) > 0 {
for _, tag := range dashboardByTag {
searchQuery := search.Query{
Title: "",
Tags: []string{tag},
UserId: userId,
Limit: 100,
IsStarred: false,
OrgId: orgId,
Title: "",
Tags: []string{tag},
SignedInUser: signedInUser,
Limit: 100,
IsStarred: false,
OrgId: orgId,
}
if err := bus.Dispatch(&searchQuery); err == nil {
......@@ -64,7 +64,7 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb
return result
}
func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := LoadPlaylistItems(playlistId)
dashboardByIds := make([]int64, 0)
......@@ -89,7 +89,7 @@ func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashb
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
result = append(result, k...)
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...)
result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...)
sort.Sort(result)
return result, nil
......
......@@ -17,8 +17,10 @@ func RenderToPng(c *middleware.Context) {
Path: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "60"),
OrgId: c.OrgId,
UserId: c.UserId,
OrgRole: c.OrgRole,
Timezone: queryReader.Get("tz", ""),
}
......
......@@ -14,14 +14,16 @@ func Search(c *middleware.Context) {
tags := c.QueryStrings("tag")
starred := c.Query("starred")
limit := c.QueryInt("limit")
dashboardType := c.Query("type")
folderId := c.QueryInt64("folderId")
if limit == 0 {
limit = 1000
}
dbids := make([]int, 0)
dbids := make([]int64, 0)
for _, id := range c.QueryStrings("dashboardIds") {
dashboardId, err := strconv.Atoi(id)
dashboardId, err := strconv.ParseInt(id, 10, 64)
if err == nil {
dbids = append(dbids, dashboardId)
}
......@@ -30,11 +32,13 @@ func Search(c *middleware.Context) {
searchQuery := search.Query{
Title: query,
Tags: tags,
UserId: c.UserId,
SignedInUser: c.SignedInUser,
Limit: limit,
IsStarred: starred == "true",
OrgId: c.OrgId,
DashboardIds: dbids,
Type: dashboardType,
FolderId: folderId,
}
err := bus.Dispatch(&searchQuery)
......
......@@ -219,7 +219,7 @@ func SearchUsers(c *middleware.Context) Response {
return Json(200, query.Result.Users)
}
// GET /api/search
// GET /api/users/search
func SearchUsersWithPaging(c *middleware.Context) Response {
query, err := searchUser(c)
if err != nil {
......
package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
// POST /api/user-groups
func CreateUserGroup(c *middleware.Context, cmd m.CreateUserGroupCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrUserGroupNameTaken {
return ApiError(409, "User Group name taken", err)
}
return ApiError(500, "Failed to create User Group", err)
}
metrics.M_Api_UserGroup_Create.Inc(1)
return Json(200, &util.DynMap{
"userGroupId": cmd.Result.Id,
"message": "User Group created",
})
}
// PUT /api/user-groups/:userGroupId
func UpdateUserGroup(c *middleware.Context, cmd m.UpdateUserGroupCommand) Response {
cmd.Id = c.ParamsInt64(":userGroupId")
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrUserGroupNameTaken {
return ApiError(400, "User Group name taken", err)
}
return ApiError(500, "Failed to update User Group", err)
}
return ApiSuccess("User Group updated")
}
// DELETE /api/user-groups/:userGroupId
func DeleteUserGroupById(c *middleware.Context) Response {
if err := bus.Dispatch(&m.DeleteUserGroupCommand{Id: c.ParamsInt64(":userGroupId")}); err != nil {
if err == m.ErrUserGroupNotFound {
return ApiError(404, "Failed to delete User Group. ID not found", nil)
}
return ApiError(500, "Failed to update User Group", err)
}
return ApiSuccess("User Group deleted")
}
// GET /api/user-groups/search
func SearchUserGroups(c *middleware.Context) Response {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
query := m.SearchUserGroupsQuery{
Query: c.Query("query"),
Name: c.Query("name"),
Page: page,
Limit: perPage,
OrgId: c.OrgId,
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search User Groups", err)
}
query.Result.Page = page
query.Result.PerPage = perPage
return Json(200, query.Result)
}
// GET /api/user-groups/:userGroupId
func GetUserGroupById(c *middleware.Context) Response {
query := m.GetUserGroupByIdQuery{Id: c.ParamsInt64(":userGroupId")}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrUserGroupNotFound {
return ApiError(404, "User Group not found", err)
}
return ApiError(500, "Failed to get User Group", err)
}
return Json(200, &query.Result)
}
package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
// GET /api/user-groups/:userGroupId/members
func GetUserGroupMembers(c *middleware.Context) Response {
query := m.GetUserGroupMembersQuery{UserGroupId: c.ParamsInt64(":userGroupId")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get User Group Members", err)
}
return Json(200, query.Result)
}
// POST /api/user-groups/:userGroupId/members
func AddUserGroupMember(c *middleware.Context, cmd m.AddUserGroupMemberCommand) Response {
cmd.UserGroupId = c.ParamsInt64(":userGroupId")
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrUserGroupMemberAlreadyAdded {
return ApiError(400, "User is already added to this user group", err)
}
return ApiError(500, "Failed to add Member to User Group", err)
}
return Json(200, &util.DynMap{
"message": "Member added to User Group",
})
}
// DELETE /api/user-groups/:userGroupId/members/:userId
func RemoveUserGroupMember(c *middleware.Context) Response {
if err := bus.Dispatch(&m.RemoveUserGroupMemberCommand{UserGroupId: c.ParamsInt64(":userGroupId"), UserId: c.ParamsInt64(":userId")}); err != nil {
return ApiError(500, "Failed to remove Member from User Group", err)
}
return ApiSuccess("User Group Member removed")
}
package api
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserGroupApiEndpoint(t *testing.T) {
Convey("Given two user groups", t, func() {
mockResult := models.SearchUserGroupQueryResult{
UserGroups: []*models.UserGroup{
{Name: "userGroup1"},
{Name: "userGroup2"},
},
TotalCount: 2,
}
Convey("When searching with no parameters", func() {
loggedInUserScenario("When calling GET on", "/api/user-groups/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUserGroupsQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUserGroups
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
So(len(respJSON.Get("userGroups").MustArray()), ShouldEqual, 2)
})
})
Convey("When searching with page and perpage parameters", func() {
loggedInUserScenario("When calling GET on", "/api/user-groups/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUserGroupsQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUserGroups
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
})
})
}
......@@ -16,17 +16,21 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type RenderOpts struct {
Path string
Width string
Height string
Timeout string
OrgId int64
Timezone string
Path string
Width string
Height string
Timeout string
OrgId int64
UserId int64
OrgRole models.RoleType
Timezone string
IsAlertContext bool
}
var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
......@@ -74,7 +78,11 @@ func RenderToPng(params *RenderOpts) (string, error) {
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
pngPath = pngPath + ".png"
renderKey := middleware.AddRenderAuthKey(params.OrgId)
orgRole := params.OrgRole
if params.IsAlertContext {
orgRole = models.ROLE_ADMIN
}
renderKey := middleware.AddRenderAuthKey(params.OrgId, params.UserId, orgRole)
defer middleware.RemoveRenderAuthKey(renderKey)
timeout, err := strconv.Atoi(params.Timeout)
......
......@@ -35,6 +35,8 @@ var (
M_Api_Dashboard_Snapshot_Create Counter
M_Api_Dashboard_Snapshot_External Counter
M_Api_Dashboard_Snapshot_Get Counter
M_Api_UserGroup_Create Counter
M_Api_Dashboard_Acl_Update Counter
M_Models_Dashboard_Insert Counter
M_Alerting_Result_State_Alerting Counter
M_Alerting_Result_State_Ok Counter
......@@ -93,6 +95,9 @@ func initMetricVars(settings *MetricSettings) {
M_Api_User_SignUpCompleted = RegCounter("api.user.signup_completed")
M_Api_User_SignUpInvite = RegCounter("api.user.signup_invite")
M_Api_UserGroup_Create = RegCounter("api.usergroup.create")
M_Api_Dashboard_Acl_Update = RegCounter("api.dashboard.acl.update")
M_Api_Dashboard_Save = RegTimer("api.dashboard.save")
M_Api_Dashboard_Get = RegTimer("api.dashboard.get")
M_Api_Dashboard_Search = RegTimer("api.dashboard.search")
......
......@@ -33,14 +33,15 @@ func initContextWithRenderAuth(ctx *Context) bool {
type renderContextFunc func(key string) (string, error)
func AddRenderAuthKey(orgId int64) string {
func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) string {
renderKeysLock.Lock()
key := util.GetRandomString(32)
renderKeys[key] = &m.SignedInUser{
OrgId: orgId,
OrgRole: m.ROLE_VIEWER,
OrgRole: orgRole,
UserId: userId,
}
renderKeysLock.Unlock()
......
package models
import (
"errors"
"time"
)
type PermissionType int
const (
PERMISSION_VIEW PermissionType = 1 << iota
PERMISSION_EDIT
PERMISSION_ADMIN
)
func (p PermissionType) String() string {
names := map[int]string{
int(PERMISSION_VIEW): "View",
int(PERMISSION_EDIT): "Edit",
int(PERMISSION_ADMIN): "Admin",
}
return names[int(p)]
}
// Typed errors
var (
ErrDashboardAclInfoMissing = errors.New("User id and user group id cannot both be empty for a dashboard permission.")
ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.")
)
// Dashboard ACL model
type DashboardAcl struct {
Id int64
OrgId int64
DashboardId int64
UserId int64
UserGroupId int64
Role *RoleType // pointer to be nullable
Permission PermissionType
Created time.Time
Updated time.Time
}
type DashboardAclInfoDTO struct {
Id int64 `json:"id"`
OrgId int64 `json:"-"`
DashboardId int64 `json:"dashboardId"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UserId int64 `json:"userId"`
UserLogin string `json:"userLogin"`
UserEmail string `json:"userEmail"`
UserGroupId int64 `json:"userGroupId"`
UserGroup string `json:"userGroup"`
Role *RoleType `json:"role,omitempty"`
Permission PermissionType `json:"permission"`
PermissionName string `json:"permissionName"`
}
//
// COMMANDS
//
type UpdateDashboardAclCommand struct {
DashboardId int64
Items []*DashboardAcl
}
type SetDashboardAclCommand struct {
DashboardId int64
OrgId int64
UserId int64
UserGroupId int64
Permission PermissionType
Result DashboardAcl
}
type RemoveDashboardAclCommand struct {
AclId int64
OrgId int64
}
//
// QUERIES
//
type GetDashboardAclInfoListQuery struct {
DashboardId int64
OrgId int64
Result []*DashboardAclInfoDTO
}
package models
import (
"testing"
"fmt"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardAclModel(t *testing.T) {
Convey("When printing a PermissionType", t, func() {
view := PERMISSION_VIEW
printed := fmt.Sprint(view)
Convey("Should output a friendly name", func() {
So(printed, ShouldEqual, "View")
})
})
}
......@@ -11,11 +11,12 @@ import (
// Typed errors
var (
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
)
type UpdatePluginDashboardError struct {
......@@ -47,6 +48,9 @@ type Dashboard struct {
UpdatedBy int64
CreatedBy int64
FolderId int64
IsFolder bool
HasAcl bool
Title string
Data *simplejson.Json
......@@ -111,6 +115,8 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash.UpdatedBy = userId
dash.OrgId = cmd.OrgId
dash.PluginId = cmd.PluginId
dash.IsFolder = cmd.IsFolder
dash.FolderId = cmd.FolderId
dash.UpdateSlug()
return dash
}
......@@ -138,12 +144,14 @@ type SaveDashboardCommand struct {
OrgId int64 `json:"-"`
RestoredFrom int `json:"-"`
PluginId string `json:"-"`
FolderId int64 `json:"folderId"`
IsFolder bool `json:"isFolder"`
Result *Dashboard
}
type DeleteDashboardCommand struct {
Slug string
Id int64
OrgId int64
}
......
......@@ -28,4 +28,27 @@ func TestDashboardModel(t *testing.T) {
})
})
Convey("Given a new dashboard folder", t, func() {
json := simplejson.New()
json.Set("title", "test dash")
cmd := &SaveDashboardCommand{Dashboard: json, IsFolder: true}
dash := cmd.GetDashboardModel()
Convey("Should set IsFolder to true", func() {
So(dash.IsFolder, ShouldBeTrue)
})
})
Convey("Given a child dashboard", t, func() {
json := simplejson.New()
json.Set("title", "test dash")
cmd := &SaveDashboardCommand{Dashboard: json, FolderId: 1}
dash := cmd.GetDashboardModel()
Convey("Should set FolderId", func() {
So(dash.FolderId, ShouldEqual, 1)
})
})
}
......@@ -32,11 +32,20 @@ func (r RoleType) Includes(other RoleType) bool {
if r == ROLE_ADMIN {
return true
}
if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR {
return other != ROLE_ADMIN
if other == ROLE_READ_ONLY_EDITOR {
return r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
}
if other == ROLE_EDITOR {
return r == ROLE_EDITOR
}
if other == ROLE_VIEWER {
return r == ROLE_READ_ONLY_EDITOR || r == ROLE_EDITOR || r == ROLE_VIEWER
}
return r == other
return false
}
func (r *RoleType) UnmarshalJSON(data []byte) error {
......
......@@ -162,6 +162,14 @@ type SignedInUser struct {
HelpFlags1 HelpFlags1
}
func (user *SignedInUser) HasRole(role RoleType) bool {
if user.IsGrafanaAdmin {
return true
}
return user.OrgRole.Includes(role)
}
type UserProfileDTO struct {
Id int64 `json:"id"`
Email string `json:"email"`
......
package models
import (
"errors"
"time"
)
// Typed errors
var (
ErrUserGroupNotFound = errors.New("User Group not found")
ErrUserGroupNameTaken = errors.New("User Group name is taken")
)
// UserGroup model
type UserGroup struct {
Id int64 `json:"id"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ---------------------
// COMMANDS
type CreateUserGroupCommand struct {
Name string `json:"name" binding:"Required"`
OrgId int64 `json:"-"`
Result UserGroup `json:"-"`
}
type UpdateUserGroupCommand struct {
Id int64
Name string
}
type DeleteUserGroupCommand struct {
Id int64
}
type GetUserGroupByIdQuery struct {
Id int64
Result *UserGroup
}
type GetUserGroupsByUserQuery struct {
UserId int64 `json:"userId"`
Result []*UserGroup `json:"userGroups"`
}
type SearchUserGroupsQuery struct {
Query string
Name string
Limit int
Page int
OrgId int64
Result SearchUserGroupQueryResult
}
type SearchUserGroupQueryResult struct {
TotalCount int64 `json:"totalCount"`
UserGroups []*UserGroup `json:"userGroups"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
package models
import (
"errors"
"time"
)
// Typed errors
var (
ErrUserGroupMemberAlreadyAdded = errors.New("User is already added to this user group")
)
// UserGroupMember model
type UserGroupMember struct {
Id int64
OrgId int64
UserGroupId int64
UserId int64
Created time.Time
Updated time.Time
}
// ---------------------
// COMMANDS
type AddUserGroupMemberCommand struct {
UserId int64 `json:"userId" binding:"Required"`
OrgId int64 `json:"-"`
UserGroupId int64 `json:"-"`
}
type RemoveUserGroupMemberCommand struct {
UserId int64
UserGroupId int64
}
// ----------------------
// QUERIES
type GetUserGroupMembersQuery struct {
UserGroupId int64
Result []*UserGroupMemberDTO
}
// ----------------------
// Projections and DTOs
type UserGroupMemberDTO struct {
OrgId int64 `json:"orgId"`
UserGroupId int64 `json:"userGroupId"`
UserId int64 `json:"userId"`
Email string `json:"email"`
Login string `json:"login"`
}
......@@ -15,6 +15,7 @@ type PluginDashboardInfoDTO struct {
Imported bool `json:"imported"`
ImportedUri string `json:"importedUri"`
Slug string `json:"slug"`
DashboardId int64 `json:"dashboardId"`
ImportedRevision int64 `json:"importedRevision"`
Revision int64 `json:"revision"`
Description string `json:"description"`
......@@ -60,6 +61,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
// find existing dashboard
for _, existingDash := range query.Result {
if existingDash.Slug == dashboard.Slug {
res.DashboardId = existingDash.Id
res.Imported = true
res.ImportedUri = "db/" + existingDash.Slug
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
......@@ -74,8 +76,9 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
for _, dash := range query.Result {
if _, exists := existingMatches[dash.Id]; !exists {
result = append(result, &PluginDashboardInfoDTO{
Slug: dash.Slug,
Removed: true,
Slug: dash.Slug,
DashboardId: dash.Id,
Removed: true,
})
}
}
......
......@@ -75,7 +75,7 @@ func syncPluginDashboards(pluginDef *PluginBase, orgId int64) {
if dash.Removed {
plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Slug: dash.Slug}
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Id: dash.DashboardId}
if err := bus.Dispatch(&deleteCmd); err != nil {
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
return
......@@ -124,7 +124,7 @@ func handlePluginStateChanged(event *m.PluginStateChangedEvent) error {
return err
} else {
for _, dash := range query.Result {
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Slug: dash.Slug}
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
......
......@@ -79,10 +79,11 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
}
renderOpts := &renderer.RenderOpts{
Width: "800",
Height: "400",
Timeout: "30",
OrgId: context.Rule.OrgId,
Width: "800",
Height: "400",
Timeout: "30",
OrgId: context.Rule.OrgId,
IsAlertContext: true,
}
if slug, err := context.GetDashboardSlug(); err != nil {
......
package guardian
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
type DashboardGuardian struct {
user *m.SignedInUser
dashId int64
orgId int64
acl []*m.DashboardAclInfoDTO
groups []*m.UserGroup
log log.Logger
}
func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *DashboardGuardian {
return &DashboardGuardian{
user: user,
dashId: dashId,
orgId: orgId,
log: log.New("guardians.dashboard"),
}
}
func (g *DashboardGuardian) CanSave() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanEdit() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanView() (bool, error) {
return g.HasPermission(m.PERMISSION_VIEW)
}
func (g *DashboardGuardian) CanAdmin() (bool, error) {
return g.HasPermission(m.PERMISSION_ADMIN)
}
func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
}
acl, err := g.GetAcl()
if err != nil {
return false, err
}
orgRole := g.user.OrgRole
if orgRole == m.ROLE_READ_ONLY_EDITOR {
orgRole = m.ROLE_VIEWER
}
userGroupAclItems := []*m.DashboardAclInfoDTO{}
for _, p := range acl {
// user match
if p.UserId == g.user.UserId && p.Permission >= permission {
return true, nil
}
// role match
if p.Role != nil {
if *p.Role == orgRole && p.Permission >= permission {
return true, nil
}
}
// remember this rule for later
if p.UserGroupId > 0 {
userGroupAclItems = append(userGroupAclItems, p)
}
}
// do we have group rules?
if len(userGroupAclItems) == 0 {
return false, nil
}
// load groups
userGroups, err := g.getUserGroups()
if err != nil {
return false, err
}
// evalute group rules
for _, p := range acl {
for _, ug := range userGroups {
if ug.Id == p.UserGroupId && p.Permission >= permission {
return true, nil
}
}
}
return false, nil
}
// Returns dashboard acl
func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
if g.acl != nil {
return g.acl, nil
}
query := m.GetDashboardAclInfoListQuery{DashboardId: g.dashId, OrgId: g.orgId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
g.acl = query.Result
return g.acl, nil
}
func (g *DashboardGuardian) getUserGroups() ([]*m.UserGroup, error) {
if g.groups != nil {
return g.groups, nil
}
query := m.GetUserGroupsByUserQuery{UserId: g.user.UserId}
err := bus.Dispatch(&query)
g.groups = query.Result
return query.Result, err
}
package search
import (
"log"
"path/filepath"
"sort"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
var jsonDashIndex *JsonDashIndex
func Init() {
bus.AddHandler("search", searchHandler)
jsonIndexCfg, _ := setting.Cfg.GetSection("dashboards.json")
if jsonIndexCfg == nil {
log.Fatal("Config section missing: dashboards.json")
return
}
jsonIndexEnabled := jsonIndexCfg.Key("enabled").MustBool(false)
if jsonIndexEnabled {
jsonFilesPath := jsonIndexCfg.Key("path").String()
if !filepath.IsAbs(jsonFilesPath) {
jsonFilesPath = filepath.Join(setting.HomePath, jsonFilesPath)
}
jsonDashIndex = NewJsonDashIndex(jsonFilesPath)
go jsonDashIndex.updateLoop()
}
}
func searchHandler(query *Query) error {
hits := make(HitList, 0)
dashQuery := FindPersistedDashboardsQuery{
Title: query.Title,
UserId: query.UserId,
SignedInUser: query.SignedInUser,
IsStarred: query.IsStarred,
OrgId: query.OrgId,
DashboardIds: query.DashboardIds,
Type: query.Type,
FolderId: query.FolderId,
Tags: query.Tags,
Limit: query.Limit,
}
if err := bus.Dispatch(&dashQuery); err != nil {
return err
}
hits := make(HitList, 0)
hits = append(hits, dashQuery.Result...)
if jsonDashIndex != nil {
jsonHits, err := jsonDashIndex.Search(query)
if err != nil {
return err
}
hits = append(hits, jsonHits...)
}
// filter out results with tag filter
if len(query.Tags) > 0 {
filtered := HitList{}
for _, hit := range hits {
if hasRequiredTags(query.Tags, hit.Tags) {
filtered = append(filtered, hit)
}
}
hits = filtered
}
// sort main result array
sort.Sort(hits)
......@@ -85,7 +43,7 @@ func searchHandler(query *Query) error {
}
// add isStarred info
if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil {
if err := setIsStarredFlagOnSearchResults(query.SignedInUser.UserId, hits); err != nil {
return err
}
......@@ -93,25 +51,6 @@ func searchHandler(query *Query) error {
return nil
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
func hasRequiredTags(queryTags, hitTags []string) bool {
for _, queryTag := range queryTags {
if !stringInSlice(queryTag, hitTags) {
return false
}
}
return true
}
func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
query := m.GetUserStarsQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
......@@ -126,10 +65,3 @@ func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
return nil
}
func GetDashboardFromJsonIndex(filename string) *m.Dashboard {
if jsonDashIndex == nil {
return nil
}
return jsonDashIndex.GetDashboard(filename)
}
......@@ -11,14 +11,14 @@ import (
func TestSearch(t *testing.T) {
Convey("Given search query", t, func() {
jsonDashIndex = NewJsonDashIndex("../../../public/dashboards/")
query := Query{Limit: 2000}
query := Query{Limit: 2000, SignedInUser: &m.SignedInUser{IsGrafanaAdmin: true}}
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
query.Result = HitList{
&Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}},
&Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}},
&Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}},
&Hit{Id: 16, Title: "CCAA", Type: "dash-db", Tags: []string{"BB", "AA"}},
&Hit{Id: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}},
&Hit{Id: 15, Title: "BBAA", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
&Hit{Id: 25, Title: "bbAAa", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
&Hit{Id: 17, Title: "FOLDER", Type: "dash-folder"},
}
return nil
})
......@@ -28,34 +28,29 @@ func TestSearch(t *testing.T) {
return nil
})
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{IsGrafanaAdmin: true}
return nil
})
Convey("That is empty", func() {
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return sorted results", func() {
So(query.Result[0].Title, ShouldEqual, "AABB")
So(query.Result[1].Title, ShouldEqual, "BBAA")
So(query.Result[2].Title, ShouldEqual, "CCAA")
So(query.Result[0].Title, ShouldEqual, "FOLDER")
So(query.Result[1].Title, ShouldEqual, "AABB")
So(query.Result[2].Title, ShouldEqual, "BBAA")
So(query.Result[3].Title, ShouldEqual, "bbAAa")
So(query.Result[4].Title, ShouldEqual, "CCAA")
})
Convey("should return sorted tags", func() {
So(query.Result[1].Tags[0], ShouldEqual, "AA")
So(query.Result[1].Tags[1], ShouldEqual, "BB")
So(query.Result[1].Tags[2], ShouldEqual, "EE")
So(query.Result[3].Tags[0], ShouldEqual, "AA")
So(query.Result[3].Tags[1], ShouldEqual, "BB")
So(query.Result[3].Tags[2], ShouldEqual, "EE")
})
})
Convey("That filters by tag", func() {
query.Tags = []string{"BB", "AA"}
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return correct results", func() {
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Title, ShouldEqual, "BBAA")
So(query.Result[1].Title, ShouldEqual, "CCAA")
})
})
})
}
package search
import (
"os"
"path/filepath"
"strings"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
type JsonDashIndex struct {
path string
items []*JsonDashIndexItem
}
type JsonDashIndexItem struct {
TitleLower string
TagsCsv string
Path string
Dashboard *m.Dashboard
}
func NewJsonDashIndex(path string) *JsonDashIndex {
log.Info("Creating json dashboard index for path: %v", path)
index := JsonDashIndex{}
index.path = path
index.updateIndex()
return &index
}
func (index *JsonDashIndex) updateLoop() {
ticker := time.NewTicker(time.Minute)
for {
select {
case <-ticker.C:
if err := index.updateIndex(); err != nil {
log.Error(3, "Failed to update dashboard json index %v", err)
}
}
}
}
func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) {
results := make([]*Hit, 0)
if query.IsStarred {
return results, nil
}
queryStr := strings.ToLower(query.Title)
for _, item := range index.items {
if len(results) > query.Limit {
break
}
// add results with matchig title filter
if strings.Contains(item.TitleLower, queryStr) {
results = append(results, &Hit{
Type: DashHitJson,
Title: item.Dashboard.Title,
Tags: item.Dashboard.GetTags(),
Uri: "file/" + item.Path,
})
}
}
return results, nil
}
func (index *JsonDashIndex) GetDashboard(path string) *m.Dashboard {
for _, item := range index.items {
if item.Path == path {
return item.Dashboard
}
}
return nil
}
func (index *JsonDashIndex) updateIndex() error {
var items = make([]*JsonDashIndexItem, 0)
visitor := func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if f.IsDir() {
return nil
}
if strings.HasSuffix(f.Name(), ".json") {
dash, err := loadDashboardFromFile(path)
if err != nil {
return err
}
items = append(items, dash)
}
return nil
}
if err := filepath.Walk(index.path, visitor); err != nil {
return err
}
index.items = items
return nil
}
func loadDashboardFromFile(filename string) (*JsonDashIndexItem, error) {
reader, err := os.Open(filename)
if err != nil {
return nil, err
}
defer reader.Close()
data, err := simplejson.NewFromReader(reader)
if err != nil {
return nil, err
}
stat, _ := os.Stat(filename)
item := &JsonDashIndexItem{}
item.Dashboard = m.NewDashboardFromJson(data)
item.TitleLower = strings.ToLower(item.Dashboard.Title)
item.TagsCsv = strings.Join(item.Dashboard.GetTags(), ",")
item.Path = stat.Name()
return item, nil
}
package search
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestJsonDashIndex(t *testing.T) {
Convey("Given the json dash index", t, func() {
index := NewJsonDashIndex("../../../public/dashboards/")
Convey("Should be able to update index", func() {
err := index.updateIndex()
So(err, ShouldBeNil)
})
Convey("Should be able to search index", func() {
res, err := index.Search(&Query{Title: "", Limit: 20})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 3)
})
Convey("Should be able to search index by title", func() {
res, err := index.Search(&Query{Title: "home", Limit: 20})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 1)
So(res[0].Title, ShouldEqual, "Home")
})
Convey("Should not return when starred is filtered", func() {
res, err := index.Search(&Query{Title: "", IsStarred: true})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 0)
})
})
}
package search
import "strings"
import "github.com/grafana/grafana/pkg/models"
type HitType string
const (
DashHitDB HitType = "dash-db"
DashHitHome HitType = "dash-home"
DashHitJson HitType = "dash-json"
DashHitScripted HitType = "dash-scripted"
DashHitDB HitType = "dash-db"
DashHitHome HitType = "dash-home"
DashHitFolder HitType = "dash-folder"
)
type Hit struct {
Id int64 `json:"id"`
Title string `json:"title"`
Uri string `json:"uri"`
Type HitType `json:"type"`
Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"`
Id int64 `json:"id"`
Title string `json:"title"`
Uri string `json:"uri"`
Type HitType `json:"type"`
Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"`
FolderId int64 `json:"folderId,omitempty"`
FolderTitle string `json:"folderTitle,omitempty"`
FolderSlug string `json:"folderSlug,omitempty"`
}
type HitList []*Hit
func (s HitList) Len() int { return len(s) }
func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title }
func (s HitList) Len() int { return len(s) }
func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s HitList) Less(i, j int) bool {
if s[i].Type == "dash-folder" && s[j].Type == "dash-db" {
return true
}
if s[i].Type == "dash-db" && s[j].Type == "dash-folder" {
return false
}
return strings.ToLower(s[i].Title) < strings.ToLower(s[j].Title)
}
type Query struct {
Title string
Tags []string
OrgId int64
UserId int64
SignedInUser *models.SignedInUser
Limit int
IsStarred bool
DashboardIds []int
Type string
DashboardIds []int64
FolderId int64
Result HitList
}
......@@ -39,9 +56,13 @@ type Query struct {
type FindPersistedDashboardsQuery struct {
Title string
OrgId int64
UserId int64
SignedInUser *models.SignedInUser
IsStarred bool
DashboardIds []int
DashboardIds []int64
Type string
FolderId int64
Tags []string
Limit int
Result HitList
}
......@@ -12,7 +12,7 @@ func TestAlertingDataAccess(t *testing.T) {
Convey("Testing Alerting data access", t, func() {
InitTestDB(t)
testDash := insertTestDashboard("dashboard with alerts", 1, "alert")
testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
items := []*m.Alert{
{
......@@ -192,7 +192,7 @@ func TestAlertingDataAccess(t *testing.T) {
err = DeleteDashboard(&m.DeleteDashboardCommand{
OrgId: 1,
Slug: testDash.Slug,
Id: testDash.Id,
})
So(err, ShouldBeNil)
......
......@@ -3,6 +3,7 @@ package sqlstore
import (
"bytes"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
......@@ -70,6 +71,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
}
}
err = setHasAcl(sess, dash)
if err != nil {
return err
}
parentVersion := dash.Version
affectedRows := int64(0)
......@@ -79,9 +85,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
dash.Data.Set("version", dash.Version)
affectedRows, err = sess.Insert(dash)
} else {
dash.Version += 1
dash.Version++
dash.Data.Set("version", dash.Version)
affectedRows, err = sess.Id(dash.Id).Update(dash)
affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash)
}
if err != nil {
......@@ -110,7 +116,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
return m.ErrDashboardNotFound
}
// delete existing tabs
// delete existing tags
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
if err != nil {
return err
......@@ -125,13 +131,37 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
}
}
}
cmd.Result = dash
return err
})
}
func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
// check if parent has acl
if dash.FolderId > 0 {
var parent m.Dashboard
if hasParent, err := sess.Where("folder_id=?", dash.FolderId).Get(&parent); err != nil {
return err
} else if hasParent && parent.HasAcl {
dash.HasAcl = true
}
}
// check if dash has its own acl
if dash.Id > 0 {
if res, err := sess.Query("SELECT 1 from dashboard_acl WHERE dashboard_id =?", dash.Id); err != nil {
return err
} else {
if len(res) > 0 {
dash.HasAcl = true
}
}
}
return nil
}
func GetDashboard(query *m.GetDashboardQuery) error {
dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id}
has, err := x.Get(&dashboard)
......@@ -148,48 +178,94 @@ func GetDashboard(query *m.GetDashboardQuery) error {
}
type DashboardSearchProjection struct {
Id int64
Title string
Slug string
Term string
Id int64
Title string
Slug string
Term string
IsFolder bool
FolderId int64
FolderSlug string
FolderTitle string
}
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
limit := query.Limit
if limit == 0 {
limit = 1000
}
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`SELECT
dashboard.id,
dashboard.title,
dashboard.slug,
dashboard_tag.term
FROM dashboard
LEFT OUTER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id`)
sql.WriteString(`
SELECT
dashboard.id,
dashboard.title,
dashboard.slug,
dashboard_tag.term,
dashboard.is_folder,
dashboard.folder_id,
folder.slug as folder_slug,
folder.title as folder_title
FROM (
SELECT
dashboard.id FROM dashboard
LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id
`)
// add tags filter
if len(query.Tags) > 0 {
sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(query.Tags)-1) + `)`)
for _, tag := range query.Tags {
params = append(params, tag)
}
}
// this ends the inner select (tag filtered part)
sql.WriteString(`
GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
ORDER BY dashboard.title ASC LIMIT ?) as ids`)
params = append(params, len(query.Tags))
params = append(params, limit)
sql.WriteString(`
INNER JOIN dashboard on ids.id = dashboard.id
LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id
LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`)
if query.IsStarred {
sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
}
sql.WriteString(` WHERE dashboard.org_id=?`)
params = append(params, query.OrgId)
params = append(params, query.SignedInUser.OrgId)
if query.IsStarred {
sql.WriteString(` AND star.user_id=?`)
params = append(params, query.UserId)
params = append(params, query.SignedInUser.UserId)
}
if len(query.DashboardIds) > 0 {
sql.WriteString(" AND (")
for i, dashboardId := range query.DashboardIds {
if i != 0 {
sql.WriteString(" OR")
}
sql.WriteString(" dashboard.id = ?")
sql.WriteString(` AND dashboard.id IN (?` + strings.Repeat(",?", len(query.DashboardIds)-1) + `)`)
for _, dashboardId := range query.DashboardIds {
params = append(params, dashboardId)
}
sql.WriteString(")")
}
if query.SignedInUser.OrgRole != m.ROLE_ADMIN {
allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in (
SELECT distinct d.id AS DashboardId
FROM dashboard AS d
LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
LEFT JOIN user_group_member as ugm on ugm.user_group_id = da.user_group_id
LEFT JOIN org_user ou on ou.role = da.role
WHERE
d.has_acl = 1 and
(da.user_id = ? or ugm.user_id = ? or ou.id is not null)
and d.org_id = ?
))`
sql.WriteString(allowedDashboardsSubQuery)
params = append(params, query.SignedInUser.UserId, query.SignedInUser.UserId, query.SignedInUser.OrgId)
}
if len(query.Title) > 0 {
......@@ -197,15 +273,54 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
params = append(params, "%"+query.Title+"%")
}
if len(query.Type) > 0 && query.Type == "dash-folder" {
sql.WriteString(" AND dashboard.is_folder = 1")
}
if len(query.Type) > 0 && query.Type == "dash-db" {
sql.WriteString(" AND dashboard.is_folder = 0")
}
if query.FolderId > 0 {
sql.WriteString(" AND dashboard.folder_id = ?")
params = append(params, query.FolderId)
}
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
var res []DashboardSearchProjection
err := x.Sql(sql.String(), params...).Find(&res)
if err != nil {
return nil, err
}
return res, nil
}
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
res, err := findDashboards(query)
if err != nil {
return err
}
makeQueryResult(query, res)
return nil
}
func getHitType(item DashboardSearchProjection) search.HitType {
var hitType search.HitType
if item.IsFolder {
hitType = search.DashHitFolder
} else {
hitType = search.DashHitDB
}
return hitType
}
func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []DashboardSearchProjection) {
query.Result = make([]*search.Hit, 0)
hits := make(map[int64]*search.Hit)
......@@ -213,11 +328,14 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
hit, exists := hits[item.Id]
if !exists {
hit = &search.Hit{
Id: item.Id,
Title: item.Title,
Uri: "db/" + item.Slug,
Type: search.DashHitDB,
Tags: []string{},
Id: item.Id,
Title: item.Title,
Uri: "db/" + item.Slug,
Type: getHitType(item),
FolderId: item.FolderId,
FolderTitle: item.FolderTitle,
FolderSlug: item.FolderSlug,
Tags: []string{},
}
query.Result = append(query.Result, hit)
hits[item.Id] = hit
......@@ -226,8 +344,6 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
hit.Tags = append(hit.Tags, item.Term)
}
}
return err
}
func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
......@@ -247,7 +363,7 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
return inTransaction(func(sess *DBSession) error {
dashboard := m.Dashboard{Slug: cmd.Slug, OrgId: cmd.OrgId}
dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
has, err := sess.Get(&dashboard)
if err != nil {
return err
......@@ -261,6 +377,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
"DELETE FROM dashboard WHERE id = ?",
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
"DELETE FROM dashboard WHERE folder_id = ?",
}
for _, sql := range deletes {
......
package sqlstore
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
func init() {
bus.AddHandler("sql", SetDashboardAcl)
bus.AddHandler("sql", UpdateDashboardAcl)
bus.AddHandler("sql", RemoveDashboardAcl)
bus.AddHandler("sql", GetDashboardAclInfoList)
}
func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
// delete existing items
_, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", cmd.DashboardId)
if err != nil {
return err
}
for _, item := range cmd.Items {
if item.UserId == 0 && item.UserGroupId == 0 && !item.Role.IsValid() {
return m.ErrDashboardAclInfoMissing
}
if item.DashboardId == 0 {
return m.ErrDashboardPermissionDashboardEmpty
}
sess.Nullable("user_id", "user_group_id")
if _, err := sess.Insert(item); err != nil {
return err
}
}
// Update dashboard HasAcl flag
dashboard := m.Dashboard{HasAcl: true}
if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
return err
}
return nil
})
}
func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
if cmd.UserId == 0 && cmd.UserGroupId == 0 {
return m.ErrDashboardAclInfoMissing
}
if cmd.DashboardId == 0 {
return m.ErrDashboardPermissionDashboardEmpty
}
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{
Permission: cmd.Permission,
Updated: time.Now(),
}
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 nil
}
entity := m.DashboardAcl{
OrgId: cmd.OrgId,
UserGroupId: cmd.UserGroupId,
UserId: cmd.UserId,
Created: time.Now(),
Updated: time.Now(),
DashboardId: cmd.DashboardId,
Permission: cmd.Permission,
}
cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"}
if cmd.UserId != 0 {
cols = append(cols, "user_id")
}
if cmd.UserGroupId != 0 {
cols = append(cols, "user_group_id")
}
_, err := sess.Cols(cols...).Insert(&entity)
if err != nil {
return err
}
cmd.Result = entity
// Update dashboard HasAcl flag
dashboard := m.Dashboard{
HasAcl: true,
}
if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
return err
}
return nil
})
}
func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?"
_, err := sess.Exec(rawSQL, cmd.OrgId, cmd.AclId)
if err != nil {
return err
}
return err
})
}
func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
dashboardFilter := fmt.Sprintf(`IN (
SELECT %d
UNION
SELECT folder_id from dashboard where id = %d
)`, query.DashboardId, query.DashboardId)
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 ` + dashboardFilter + ` AND da.org_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
LEFT JOIN dashboard folder on dash.folder_id = folder.id
WHERE dash.id = ? AND (dash.has_acl = 0 or folder.has_acl = 0) AND da.dashboard_id = -1
`
query.Result = make([]*m.DashboardAclInfoDTO, 0)
err := x.SQL(rawSQL, query.OrgId, query.DashboardId).Find(&query.Result)
for _, p := range query.Result {
p.PermissionName = p.Permission.String()
}
return err
}
package sqlstore
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestDashboardAclDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
Convey("Given a dashboard folder and a user", func() {
currentUser := createUser("viewer", "Viewer", false)
savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp")
Convey("When adding dashboard permission with userId and userGroupId set to 0", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldEqual, m.ErrDashboardAclInfoMissing)
})
Convey("Given dashboard folder with default permissions", func() {
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
})
})
Convey("Given dashboard folder permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
})
Convey("Given child dashboard permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: childDash.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include acl for parent folder and child", func() {
query := m.GetDashboardAclInfoListQuery{OrgId: 1, DashboardId: childDash.Id}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(query.Result[1].DashboardId, ShouldEqual, childDash.Id)
})
})
})
Convey("Given child dashboard permission in folder with no permissions", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: childDash.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include default acl for parent folder and the child acl", func() {
query := m.GetDashboardAclInfoListQuery{OrgId: 1, DashboardId: childDash.Id}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
defaultPermissionsId := -1
So(len(query.Result), ShouldEqual, 3)
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[2].DashboardId, ShouldEqual, childDash.Id)
})
})
Convey("Should be able to add dashboard permission", func() {
setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
}
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil)
So(setDashAclCmd.Result.Id, ShouldEqual, 3)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
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}}
err := GetDashboards(q2)
So(err, ShouldBeNil)
So(q2.Result[0].HasAcl, ShouldBeTrue)
So(q2.Result[1].HasAcl, ShouldBeTrue)
})
Convey("Should be able to update an existing permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: 1,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].UserId, ShouldEqual, 1)
})
Convey("Should be able to delete an existing permission", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1,
AclId: setDashAclCmd.Result.Id,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
})
Convey("Given a user group", func() {
group1 := m.CreateUserGroupCommand{Name: "group1 name", OrgId: 1}
err := CreateUserGroup(&group1)
So(err, ShouldBeNil)
Convey("Should be able to add a user permission for a user group", func() {
setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1,
UserGroupId: group1.Result.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
}
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
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, OrgId: 1}
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() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserGroupId: group1.Result.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].UserGroupId, ShouldEqual, group1.Result.Id)
})
})
})
})
}
......@@ -32,6 +32,10 @@ func GetDashboardVersion(query *m.GetDashboardVersionQuery) error {
// GetDashboardVersions gets all dashboard versions for the given dashboard ID.
func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error {
if query.Limit == 0 {
query.Limit = 1000
}
err := x.Table("dashboard_version").
Select(`dashboard_version.id,
dashboard_version.dashboard_id,
......
......@@ -28,7 +28,7 @@ func TestGetDashboardVersion(t *testing.T) {
InitTestDB(t)
Convey("Get a Dashboard ID and version ID", func() {
savedDash := insertTestDashboard("test dash 26", 1, "diff")
savedDash := insertTestDashboard("test dash 26", 1, 0, false, "diff")
query := m.GetDashboardVersionQuery{
DashboardId: savedDash.Id,
......@@ -69,7 +69,7 @@ func TestGetDashboardVersion(t *testing.T) {
func TestGetDashboardVersions(t *testing.T) {
Convey("Testing dashboard versions retrieval", t, func() {
InitTestDB(t)
savedDash := insertTestDashboard("test dash 43", 1, "diff-all")
savedDash := insertTestDashboard("test dash 43", 1, 0, false, "diff-all")
Convey("Get all versions for a given Dashboard ID", func() {
query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
......
......@@ -15,6 +15,7 @@ func InitTestDB(t *testing.T) {
x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
// x.ShowSQL()
// x.ShowSQL()
......
package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addDashboardAclMigrations(mg *Migrator) {
dashboardAclV1 := Table{
Name: "dashboard_acl",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: DB_BigInt},
{Name: "dashboard_id", Type: DB_BigInt},
{Name: "user_id", Type: DB_BigInt, Nullable: true},
{Name: "user_group_id", Type: DB_BigInt, Nullable: true},
{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{"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))
//------- indexes ------------------
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))
}
......@@ -136,4 +136,18 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Update dashboard_tag table charset", NewTableCharsetMigration("dashboard_tag", []*Column{
{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false},
}))
// add column to store folder_id for dashboard folder structure
mg.AddMigration("Add column folder_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "folder_id", Type: DB_BigInt, Nullable: true,
}))
mg.AddMigration("Add column isFolder in dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "is_folder", Type: DB_Bool, Nullable: false, Default: "0",
}))
// add column to flag if dashboard has an ACL
mg.AddMigration("Add column has_acl in dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "has_acl", Type: DB_Bool, Nullable: false, Default: "0",
}))
}
......@@ -26,6 +26,8 @@ func AddMigrations(mg *Migrator) {
addAnnotationMig(mg)
addTestDataMigrations(mg)
addDashboardVersionMigration(mg)
addUserGroupMigrations(mg)
addDashboardAclMigrations(mg)
}
func addMigrationLogMigrations(mg *Migrator) {
......
package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addUserGroupMigrations(mg *Migrator) {
userGroupV1 := Table{
Name: "user_group",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "org_id", Type: DB_BigInt},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"org_id"}},
{Cols: []string{"org_id", "name"}, Type: UniqueIndex},
},
}
mg.AddMigration("create user group table", NewAddTableMigration(userGroupV1))
//------- indexes ------------------
mg.AddMigration("add index user_group.org_id", NewAddIndexMigration(userGroupV1, userGroupV1.Indices[0]))
mg.AddMigration("add unique index user_group_org_id_name", NewAddIndexMigration(userGroupV1, userGroupV1.Indices[1]))
userGroupMemberV1 := Table{
Name: "user_group_member",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: DB_BigInt},
{Name: "user_group_id", Type: DB_BigInt},
{Name: "user_id", Type: DB_BigInt},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"org_id"}},
{Cols: []string{"org_id", "user_group_id", "user_id"}, Type: UniqueIndex},
},
}
mg.AddMigration("create user group member table", NewAddTableMigration(userGroupMemberV1))
//------- indexes ------------------
mg.AddMigration("add index user_group_member.org_id", NewAddIndexMigration(userGroupMemberV1, userGroupMemberV1.Indices[0]))
mg.AddMigration("add unique index user_group_member_org_id_user_group_id_user_id", NewAddIndexMigration(userGroupMemberV1, userGroupMemberV1.Indices[1]))
}
......@@ -154,6 +154,57 @@ func TestAccountDataAccess(t *testing.T) {
So(err, ShouldEqual, m.ErrLastOrgAdmin)
})
Convey("Given an org user with dashboard permissions", func() {
ac3cmd := m.CreateUserCommand{Login: "ac3", Email: "ac3@test.com", Name: "ac3 name", IsAdmin: false}
err := CreateUser(&ac3cmd)
So(err, ShouldBeNil)
ac3 := ac3cmd.Result
orgUserCmd := m.AddOrgUserCommand{
OrgId: ac1.OrgId,
UserId: ac3.Id,
Role: m.ROLE_VIEWER,
}
err = AddOrgUser(&orgUserCmd)
So(err, ShouldBeNil)
query := m.GetOrgUsersQuery{OrgId: ac1.OrgId}
err = GetOrgUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 3)
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, Permission: m.PERMISSION_EDIT})
So(err, ShouldBeNil)
Convey("When org user is deleted", func() {
cmdRemove := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac3.Id}
err := RemoveOrgUser(&cmdRemove)
So(err, ShouldBeNil)
Convey("Should remove dependent permissions for deleted org user", func() {
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: ac1.OrgId}
err = GetDashboardAclInfoList(permQuery)
So(err, ShouldBeNil)
So(len(permQuery.Result), ShouldEqual, 0)
})
Convey("Should not remove dashboard permissions for same user in another org", func() {
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 2, OrgId: ac3.OrgId}
err = GetDashboardAclInfoList(permQuery)
So(err, ShouldBeNil)
So(len(permQuery.Result), ShouldEqual, 1)
So(permQuery.Result[0].OrgId, ShouldEqual, ac3.OrgId)
So(permQuery.Result[0].UserId, ShouldEqual, ac3.Id)
})
})
})
})
})
})
......
......@@ -80,10 +80,17 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error {
func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
return inTransaction(func(sess *DBSession) error {
var rawSql = "DELETE FROM org_user WHERE org_id=? and user_id=?"
_, err := sess.Exec(rawSql, cmd.OrgId, cmd.UserId)
if err != nil {
return err
deletes := []string{
"DELETE FROM org_user WHERE org_id=? and user_id=?",
"DELETE FROM dashboard_acl WHERE org_id=? and user_id = ?",
"DELETE FROM user_group_member WHERE org_id=? and user_id = ?",
}
for _, sql := range deletes {
_, err := sess.Exec(sql, cmd.OrgId, cmd.UserId)
if err != nil {
return err
}
}
return validateOneAdminLeftInOrg(cmd.OrgId, sess)
......
......@@ -396,6 +396,10 @@ func DeleteUser(cmd *m.DeleteUserCommand) error {
deletes := []string{
"DELETE FROM star WHERE user_id = ?",
"DELETE FROM " + dialect.Quote("user") + " WHERE id = ?",
"DELETE FROM org_user WHERE user_id = ?",
"DELETE FROM dashboard_acl WHERE user_id = ?",
"DELETE FROM preferences WHERE user_id = ?",
"DELETE FROM user_group_member WHERE user_id = ?",
}
for _, sql := range deletes {
......
package sqlstore
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
func init() {
bus.AddHandler("sql", CreateUserGroup)
bus.AddHandler("sql", UpdateUserGroup)
bus.AddHandler("sql", DeleteUserGroup)
bus.AddHandler("sql", SearchUserGroups)
bus.AddHandler("sql", GetUserGroupById)
bus.AddHandler("sql", GetUserGroupsByUser)
bus.AddHandler("sql", AddUserGroupMember)
bus.AddHandler("sql", RemoveUserGroupMember)
bus.AddHandler("sql", GetUserGroupMembers)
}
func CreateUserGroup(cmd *m.CreateUserGroupCommand) error {
return inTransaction(func(sess *DBSession) error {
if isNameTaken, err := isUserGroupNameTaken(cmd.Name, 0, sess); err != nil {
return err
} else if isNameTaken {
return m.ErrUserGroupNameTaken
}
userGroup := m.UserGroup{
Name: cmd.Name,
OrgId: cmd.OrgId,
Created: time.Now(),
Updated: time.Now(),
}
_, err := sess.Insert(&userGroup)
cmd.Result = userGroup
return err
})
}
func UpdateUserGroup(cmd *m.UpdateUserGroupCommand) error {
return inTransaction(func(sess *DBSession) error {
if isNameTaken, err := isUserGroupNameTaken(cmd.Name, cmd.Id, sess); err != nil {
return err
} else if isNameTaken {
return m.ErrUserGroupNameTaken
}
userGroup := m.UserGroup{
Name: cmd.Name,
Updated: time.Now(),
}
affectedRows, err := sess.Id(cmd.Id).Update(&userGroup)
if err != nil {
return err
}
if affectedRows == 0 {
return m.ErrUserGroupNotFound
}
return nil
})
}
func DeleteUserGroup(cmd *m.DeleteUserGroupCommand) error {
return inTransaction(func(sess *DBSession) error {
if res, err := sess.Query("SELECT 1 from user_group WHERE id=?", cmd.Id); err != nil {
return err
} else if len(res) != 1 {
return m.ErrUserGroupNotFound
}
deletes := []string{
"DELETE FROM user_group_member WHERE user_group_id = ?",
"DELETE FROM user_group WHERE id = ?",
"DELETE FROM dashboard_acl WHERE user_group_id = ?",
}
for _, sql := range deletes {
_, err := sess.Exec(sql, cmd.Id)
if err != nil {
return err
}
}
return nil
})
}
func isUserGroupNameTaken(name string, existingId int64, sess *DBSession) (bool, error) {
var userGroup m.UserGroup
exists, err := sess.Where("name=?", name).Get(&userGroup)
if err != nil {
return false, nil
}
if exists && existingId != userGroup.Id {
return true, nil
}
return false, nil
}
func SearchUserGroups(query *m.SearchUserGroupsQuery) error {
query.Result = m.SearchUserGroupQueryResult{
UserGroups: make([]*m.UserGroup, 0),
}
queryWithWildcards := "%" + query.Query + "%"
sess := x.Table("user_group")
sess.Where("org_id=?", query.OrgId)
if query.Query != "" {
sess.Where("name LIKE ?", queryWithWildcards)
}
if query.Name != "" {
sess.Where("name=?", query.Name)
}
sess.Asc("name")
offset := query.Limit * (query.Page - 1)
sess.Limit(query.Limit, offset)
sess.Cols("id", "name")
if err := sess.Find(&query.Result.UserGroups); err != nil {
return err
}
userGroup := m.UserGroup{}
countSess := x.Table("user_group")
if query.Query != "" {
countSess.Where("name LIKE ?", queryWithWildcards)
}
if query.Name != "" {
countSess.Where("name=?", query.Name)
}
count, err := countSess.Count(&userGroup)
query.Result.TotalCount = count
return err
}
func GetUserGroupById(query *m.GetUserGroupByIdQuery) error {
var userGroup m.UserGroup
exists, err := x.Id(query.Id).Get(&userGroup)
if err != nil {
return err
}
if !exists {
return m.ErrUserGroupNotFound
}
query.Result = &userGroup
return nil
}
func GetUserGroupsByUser(query *m.GetUserGroupsByUserQuery) error {
query.Result = make([]*m.UserGroup, 0)
sess := x.Table("user_group")
sess.Join("INNER", "user_group_member", "user_group.id=user_group_member.user_group_id")
sess.Where("user_group_member.user_id=?", query.UserId)
err := sess.Find(&query.Result)
if err != nil {
return err
}
return nil
}
func AddUserGroupMember(cmd *m.AddUserGroupMemberCommand) error {
return inTransaction(func(sess *DBSession) error {
if res, err := sess.Query("SELECT 1 from user_group_member WHERE user_group_id=? and user_id=?", cmd.UserGroupId, cmd.UserId); err != nil {
return err
} else if len(res) == 1 {
return m.ErrUserGroupMemberAlreadyAdded
}
if res, err := sess.Query("SELECT 1 from user_group WHERE id=?", cmd.UserGroupId); err != nil {
return err
} else if len(res) != 1 {
return m.ErrUserGroupNotFound
}
entity := m.UserGroupMember{
OrgId: cmd.OrgId,
UserGroupId: cmd.UserGroupId,
UserId: cmd.UserId,
Created: time.Now(),
Updated: time.Now(),
}
_, err := sess.Insert(&entity)
return err
})
}
func RemoveUserGroupMember(cmd *m.RemoveUserGroupMemberCommand) error {
return inTransaction(func(sess *DBSession) error {
var rawSql = "DELETE FROM user_group_member WHERE user_group_id=? and user_id=?"
_, err := sess.Exec(rawSql, cmd.UserGroupId, cmd.UserId)
if err != nil {
return err
}
return err
})
}
func GetUserGroupMembers(query *m.GetUserGroupMembersQuery) error {
query.Result = make([]*m.UserGroupMemberDTO, 0)
sess := x.Table("user_group_member")
sess.Join("INNER", "user", fmt.Sprintf("user_group_member.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("user_group_member.user_group_id=?", query.UserGroupId)
sess.Cols("user.org_id", "user_group_member.user_group_id", "user_group_member.user_id", "user.email", "user.login")
sess.Asc("user.login", "user.email")
err := sess.Find(&query.Result)
return err
}
package sqlstore
import (
"fmt"
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestUserGroupCommandsAndQueries(t *testing.T) {
Convey("Testing User Group commands & queries", t, func() {
InitTestDB(t)
Convey("Given saved users and two user groups", func() {
var userIds []int64
for i := 0; i < 5; i++ {
userCmd := &m.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
}
err := CreateUser(userCmd)
So(err, ShouldBeNil)
userIds = append(userIds, userCmd.Result.Id)
}
group1 := m.CreateUserGroupCommand{Name: "group1 name"}
group2 := m.CreateUserGroupCommand{Name: "group2 name"}
err := CreateUserGroup(&group1)
So(err, ShouldBeNil)
err = CreateUserGroup(&group2)
So(err, ShouldBeNil)
Convey("Should be able to create user groups and add users", func() {
query := &m.SearchUserGroupsQuery{Name: "group1 name", Page: 1, Limit: 10}
err = SearchUserGroups(query)
So(err, ShouldBeNil)
So(query.Page, ShouldEqual, 1)
userGroup1 := query.Result.UserGroups[0]
So(userGroup1.Name, ShouldEqual, "group1 name")
err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: userGroup1.Id, UserId: userIds[0]})
So(err, ShouldBeNil)
q1 := &m.GetUserGroupMembersQuery{UserGroupId: userGroup1.Id}
err = GetUserGroupMembers(q1)
So(err, ShouldBeNil)
So(q1.Result[0].UserGroupId, ShouldEqual, userGroup1.Id)
So(q1.Result[0].Login, ShouldEqual, "loginuser0")
})
Convey("Should be able to search for user groups", func() {
query := &m.SearchUserGroupsQuery{Query: "group", Page: 1}
err = SearchUserGroups(query)
So(err, ShouldBeNil)
So(len(query.Result.UserGroups), ShouldEqual, 2)
So(query.Result.TotalCount, ShouldEqual, 2)
query2 := &m.SearchUserGroupsQuery{Query: ""}
err = SearchUserGroups(query2)
So(err, ShouldBeNil)
So(len(query2.Result.UserGroups), ShouldEqual, 2)
})
Convey("Should be able to return all user groups a user is member of", func() {
groupId := group2.Result.Id
err := AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: groupId, UserId: userIds[0]})
query := &m.GetUserGroupsByUserQuery{UserId: userIds[0]}
err = GetUserGroupsByUser(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Name, ShouldEqual, "group2 name")
})
Convey("Should be able to remove users from a group", func() {
err = RemoveUserGroupMember(&m.RemoveUserGroupMemberCommand{UserGroupId: group1.Result.Id, UserId: userIds[0]})
So(err, ShouldBeNil)
q1 := &m.GetUserGroupMembersQuery{UserGroupId: group1.Result.Id}
err = GetUserGroupMembers(q1)
So(err, ShouldBeNil)
So(len(q1.Result), ShouldEqual, 0)
})
Convey("Should be able to remove a group with users and permissions", func() {
groupId := group2.Result.Id
err := AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: groupId, UserId: userIds[1]})
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, Permission: m.PERMISSION_EDIT, UserGroupId: groupId})
err = DeleteUserGroup(&m.DeleteUserGroupCommand{Id: groupId})
So(err, ShouldBeNil)
query := &m.GetUserGroupByIdQuery{Id: groupId}
err = GetUserGroupById(query)
So(err, ShouldEqual, m.ErrUserGroupNotFound)
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1}
err = GetDashboardAclInfoList(permQuery)
So(err, ShouldBeNil)
So(len(permQuery.Result), ShouldEqual, 0)
})
})
})
}
......@@ -6,7 +6,7 @@ import (
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
)
func TestUserDataAccess(t *testing.T) {
......@@ -14,80 +14,134 @@ func TestUserDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
var err error
for i := 0; i < 5; i++ {
err = CreateUser(&models.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
Convey("Given 5 users", func() {
var err error
var cmd *m.CreateUserCommand
users := []m.User{}
for i := 0; i < 5; i++ {
cmd = &m.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
}
err = CreateUser(cmd)
So(err, ShouldBeNil)
users = append(users, cmd.Result)
}
Convey("Can return the first page of users and a total count", func() {
query := m.SearchUsersQuery{Query: "", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
})
So(err, ShouldBeNil)
}
Convey("Can return the first page of users and a total count", func() {
query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3}
err = SearchUsers(&query)
Convey("Can return the second page of users and a total count", func() {
query := m.SearchUsersQuery{Query: "", Page: 2, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
})
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 2)
So(query.Result.TotalCount, ShouldEqual, 5)
})
Convey("Can return the second page of users and a total count", func() {
query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3}
err = SearchUsers(&query)
Convey("Can return list of users matching query on user name", func() {
query := m.SearchUsersQuery{Query: "use", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 2)
So(query.Result.TotalCount, ShouldEqual, 5)
})
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
Convey("Can return list of users matching query on user name", func() {
query := models.SearchUsersQuery{Query: "use", Page: 1, Limit: 3}
err = SearchUsers(&query)
query = m.SearchUsersQuery{Query: "ser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
query = models.SearchUsersQuery{Query: "ser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
query = m.SearchUsersQuery{Query: "USER1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
query = models.SearchUsersQuery{Query: "USER1", Page: 1, Limit: 3}
err = SearchUsers(&query)
query = m.SearchUsersQuery{Query: "idontexist", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 0)
So(query.Result.TotalCount, ShouldEqual, 0)
})
query = models.SearchUsersQuery{Query: "idontexist", Page: 1, Limit: 3}
err = SearchUsers(&query)
Convey("Can return list of users matching query on email", func() {
query := m.SearchUsersQuery{Query: "ser1@test.com", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 0)
So(query.Result.TotalCount, ShouldEqual, 0)
})
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
})
Convey("Can return list of users matching query on email", func() {
query := models.SearchUsersQuery{Query: "ser1@test.com", Page: 1, Limit: 3}
err = SearchUsers(&query)
Convey("Can return list of users matching query on login name", func() {
query := m.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
})
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
})
Convey("when a user is an org member and has been assigned permissions", func() {
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, Permission: m.PERMISSION_EDIT})
So(err, ShouldBeNil)
Convey("Can return list of users matching query on login name", func() {
query := models.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})
So(err, ShouldBeNil)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
Convey("when the user is deleted", func() {
err = DeleteUser(&m.DeleteUserCommand{UserId: users[0].Id})
So(err, ShouldBeNil)
Convey("Should delete connected org users and permissions", func() {
query := &m.GetOrgUsersQuery{OrgId: 1}
err = GetOrgUsersForTest(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1}
err = GetDashboardAclInfoList(permQuery)
So(err, ShouldBeNil)
So(len(permQuery.Result), ShouldEqual, 0)
prefsQuery := &m.GetPreferencesQuery{OrgId: users[0].OrgId, UserId: users[0].Id}
err = GetPreferences(prefsQuery)
So(err, ShouldBeNil)
So(prefsQuery.Result.OrgId, ShouldEqual, 0)
So(prefsQuery.Result.UserId, ShouldEqual, 0)
})
})
})
})
})
}
func GetOrgUsersForTest(query *m.GetOrgUsersQuery) error {
query.Result = make([]*m.OrgUserDTO, 0)
sess := x.Table("org_user")
sess.Join("LEFT ", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("org_user.org_id=?", query.OrgId)
sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role")
err := sess.Find(&query.Result)
return err
}
......@@ -199,14 +199,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
}
}
// hide menus
var openMenus = body.find('.navbar-page-btn--open');
if (openMenus.length > 0) {
if (target.parents('.navbar-page-btn--open').length === 0) {
openMenus.removeClass('navbar-page-btn--open');
}
}
// hide sidemenu
if (!ignoreSideMenuHide && !contextSrv.pinned && body.find('.sidemenu').length > 0) {
if (target.parents('.sidemenu').length === 0) {
......
......@@ -3,15 +3,8 @@
<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>
<!-- <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> -->
<div ng-if="::!ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn">
<i class="{{::ctrl.section.icon}}" ng-show="::ctrl.section.icon"></i>
......@@ -20,7 +13,7 @@
</a>
</div>
<div class="dropdown navbar-section-wrapper" ng-if="::ctrl.hasMenu">
<div class="dropdown navbar-page-btn-wrapper" ng-if="::ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn" data-toggle="dropdown">
<i class="{{::ctrl.section.icon}}" ng-show="::ctrl.section.icon"></i>
<img ng-src="{{::ctrl.section.iconUrl}}" ng-show="::ctrl.section.iconUrl"></i>
......@@ -28,7 +21,7 @@
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu--navbar">
<li ng-repeat="navItem in ::ctrl.model.menu" ng-class="{active: navItem.active}">
<li ng-repeat="navItem in ::ctrl.model.menu">
<a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
<i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
{{::navItem.title}}
......
......@@ -4,9 +4,6 @@
<div class="search-container" ng-if="ctrl.isOpen">
<div class="search-field-wrapper">
<div class="search-field-icon pointer" ng-click="ctrl.closeSearch()">
<i class="fa fa-search"></i>
</div>
<input type="text" placeholder="Find dashboards by name" give-focus="ctrl.giveSearchFocus" tabindex="1"
ng-keydown="ctrl.keyDown($event)"
......@@ -56,36 +53,21 @@
<div class="search-results-container" ng-if="!ctrl.tagsMode">
<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
<a class="search-item pointer search-item-{{row.type}}" bindonce ng-repeat="row in ctrl.results"
ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
<span class="search-result-tags">
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
</span>
<span class="search-result-link">
<i class="fa search-result-icon"></i>
<span bo-text="row.title"></span>
</span>
</a>
</div>
<div ng-repeat="row in ctrl.results">
<a class="search-item search-item--{{::row.type}}" ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
<span class="search-result-tags">
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
</span>
<div class="search-button-row">
<a class="btn btn-secondary" href="dashboard/new" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
<i class="fa fa-plus"></i>&nbsp; New Dashboard
</a>
<a class="btn btn-inverse" href="dashboard/new/?editview=import" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
<i class="fa fa-upload"></i>&nbsp; Import Dashboard
</a>
<a class="search-button-row-explore-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
Find <img src="public/img/icn-dashboard-tiny.svg" width="14" /> dashboards on Grafana.com
</a>
</div>
<span class="search-result-link">
<i class="fa search-result-icon"></i>
{{::row.title}}
</span>
</a>
</div>
</div>
</div>
......@@ -30,19 +30,21 @@ export class SearchCtrl {
closeSearch() {
this.isOpen = this.ignoreClose;
this.openCompleted = false;
this.contextSrv.isSearching = this.isOpen;
}
openSearch(evt, payload) {
if (this.isOpen) {
this.isOpen = false;
this.closeSearch();
return;
}
this.isOpen = true;
this.contextSrv.isSearching = true;
this.giveSearchFocus = 0;
this.selectedIndex = -1;
this.results = [];
this.query = { query: '', tag: [], starred: false };
this.query = { query: '', tag: [], starred: false, mode: 'tree' };
this.currentSearchId = 0;
this.ignoreClose = true;
......@@ -104,17 +106,49 @@ export class SearchCtrl {
this.currentSearchId = this.currentSearchId + 1;
var localSearchId = this.currentSearchId;
return this.backendSrv.search(this.query).then((results) => {
return this.backendSrv.search(this.query).then(results => {
if (localSearchId < this.currentSearchId) { return; }
this.results = _.map(results, function(dash) {
dash.url = 'dashboard/' + dash.uri;
return dash;
let byId = _.groupBy(results, 'id');
let byFolderId = _.groupBy(results, 'folderId');
let finalList = [];
// add missing parent folders
_.each(results, (hit, index) => {
if (hit.folderId && !byId[hit.folderId]) {
const folder = {
id: hit.folderId,
uri: `db/${hit.folderSlug}`,
title: hit.folderTitle,
type: 'dash-folder'
};
byId[hit.folderId] = folder;
results.splice(index, 0, folder);
}
});
if (this.queryHasNoFilters()) {
this.results.unshift({ title: 'Home', url: config.appSubUrl + '/', type: 'dash-home' });
// group by folder
for (let hit of results) {
if (hit.folderId) {
hit.type = "dash-child";
} else {
finalList.push(hit);
}
hit.url = 'dashboard/' + hit.uri;
if (hit.type === 'dash-folder') {
if (!byFolderId[hit.id]) {
continue;
}
for (let child of byFolderId[hit.id]) {
finalList.push(child);
}
}
}
this.results = finalList;
});
}
......
<ul class="sidemenu">
<li class="sidemenu-org-section" ng-if="::ctrl.isSignedIn" class="dropdown">
<a class="sidemenu-org" href="profile">
<div class="sidemenu-org-avatar">
<img ng-src="{{::ctrl.user.gravatarUrl}}">
<span class="sidemenu-org-avatar--missing">
<i class="fa fa-fw fa-user"></i>
</span>
</div>
<div class="sidemenu-org-details">
<span class="sidemenu-org-user sidemenu-item-text">{{::ctrl.user.name}}</span>
<span class="sidemenu-org-name sidemenu-item-text">{{::ctrl.user.orgName}}</span>
</div>
<li>
<a class="sidemenu-item" ng-click="ctrl.search()">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-search"></i></span>
</a>
<i class="fa fa-caret-right"></i>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="menuItem in ctrl.orgMenu" ng-class="::menuItem.cssClass">
<span ng-show="::menuItem.section">{{::menuItem.section}}</span>
<a href="{{::menuItem.url}}" ng-show="::menuItem.url" target="{{::menuItem.target}}">
<i class="{{::menuItem.icon}}" ng-show="::menuItem.icon"></i>
{{::menuItem.text}}
</a>
</li>
<li ng-show="ctrl.orgs.length > ctrl.maxShownOrgs" style="margin-left: 10px;width: 90%">
<span class="sidemenu-item-text">Max shown : {{::ctrl.maxShownOrgs}}</span>
<input ng-model="::ctrl.orgFilter" style="padding-left: 5px" type="text" ng-change="::ctrl.loadOrgsItems();" class="gf-input-small width-12" placeholder="Filter">
</li>
<li ng-repeat="orgItem in ctrl.orgItems" ng-class="::orgItem.cssClass">
<a href="{{::orgItem.url}}" ng-show="::orgItem.url" target="{{::orgItem.target}}">
<i class="{{::orgItem.icon}}" ng-show="::orgItem.icon"></i>
{{::orgItem.text}}
</a>
</li>
</ul>
</li>
<li ng-repeat="item in ::ctrl.mainLinks" class="dropdown">
<a href="{{::item.url}}" class="sidemenu-item sidemenu-main-link" target="{{::item.target}}">
<a href="{{::item.url}}" class="sidemenu-item" target="{{::item.target}}">
<span class="icon-circle sidemenu-icon">
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
<img ng-src="{{::item.img}}" ng-show="::item.img">
</span>
<span class="sidemenu-item-text">{{::item.text}}</span>
<span class="fa fa-caret-right" ng-if="::item.children"></span>
</a>
<ul class="dropdown-menu" role="menu" ng-if="::item.children">
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
<li class="side-menu-header">
<span class="sidemenu-item-text">{{::item.text}}</span>
</li>
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
<a href="{{::child.url}}">
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
......@@ -55,17 +27,45 @@
</li>
<li ng-show="::!ctrl.isSignedIn">
<a href="{{ctrl.loginUrl}}" class="sidemenu-item" target="_self">
<a href="{{ctrl.loginUrl}}" class="sidemenu-item" target="_self">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-sign-in"></i></span>
<span class="sidemenu-item-text">Sign in</span>
</a>
</li>
<li>
<a class="sidemenu-item" target="_self" ng-hide="ctrl.contextSrv.pinned" ng-click="ctrl.contextSrv.setPinnedState(true)">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-thumb-tack"></i></span>
<span class="sidemenu-item-text">Pin</span>
<li class="sidemenu-org-section" ng-if="::ctrl.isSignedIn" class="dropdown">
<a class="sidemenu-item" href="profile">
<span class="icon-circle sidemenu-icon sidemenu-org-avatar">
<img ng-src="{{::ctrl.user.gravatarUrl}}">
<span class="sidemenu-org-avatar--missing">
<i class="fa fa-fw fa-user"></i>
</span>
</div>
</a>
<ul class="dropdown-menu dropdown-menu--sidemenu dropup" role="menu">
<li class="side-menu-header">
<span class="sidemenu-org-user sidemenu-item-text">{{::ctrl.user.name}}</span>
<span class="sidemenu-org-name sidemenu-item-text">{{::ctrl.user.orgName}}</span>
</li>
<li ng-repeat="menuItem in ctrl.orgMenu" ng-class="::menuItem.cssClass">
<span ng-show="::menuItem.section">{{::menuItem.section}}</span>
<a href="{{::menuItem.url}}" ng-show="::menuItem.url" target="{{::menuItem.target}}">
<i class="{{::menuItem.icon}}" ng-show="::menuItem.icon"></i>
{{::menuItem.text}}
</a>
</li>
<li ng-show="ctrl.orgs.length > ctrl.maxShownOrgs" style="margin-left: 10px;width: 90%">
<span class="sidemenu-item-text">Max shown : {{::ctrl.maxShownOrgs}}</span>
<input ng-model="::ctrl.orgFilter" style="padding-left: 5px" type="text" ng-change="::ctrl.loadOrgsItems();" class="gf-input-small width-12" placeholder="Filter">
</li>
<li ng-repeat="orgItem in ctrl.orgItems" ng-class="::orgItem.cssClass">
<a href="{{::orgItem.url}}" ng-show="::orgItem.url" target="{{::orgItem.target}}">
<i class="{{::orgItem.icon}}" ng-show="::orgItem.icon"></i>
{{::orgItem.text}}
</a>
</li>
</ul>
</li>
</ul>
......@@ -19,7 +19,7 @@ export class SideMenuCtrl {
maxShownOrgs: number;
/** @ngInject */
constructor(private $scope, private $location, private contextSrv, private backendSrv, private $element) {
constructor(private $scope, private $rootScope, private $location, private contextSrv, private backendSrv, private $element) {
this.isSignedIn = contextSrv.isSignedIn;
this.user = contextSrv.user;
this.appSubUrl = config.appSubUrl;
......@@ -44,6 +44,10 @@ export class SideMenuCtrl {
return config.appSubUrl + url;
}
search() {
this.$rootScope.appEvent('show-dash-search');
}
openUserDropdown() {
this.orgMenu = [
{section: 'You', cssClass: 'dropdown-menu-title'},
......@@ -65,6 +69,10 @@ export class SideMenuCtrl {
url: this.getUrl("/org/users")
});
this.orgMenu.push({
text: "User Groups",
url: this.getUrl("/org/user-groups")
});
this.orgMenu.push({
text: "API Keys",
url: this.getUrl("/org/apikeys")
});
......
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
const template = `
<div class="dropdown">
<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 {
group: any;
userGroupPicked: any;
debouncedSearchGroups: any;
/** @ngInject */
constructor(private backendSrv, private $scope, $sce, private uiSegmentSrv) {
this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {'leading': true, 'trailing': false});
this.reset();
}
reset() {
this.group = {text: 'Choose', value: null};
}
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 {text: ug.name, value: ug};
});
}));
}
onChange(option) {
this.userGroupPicked({$group: option.value});
}
}
export function userGroupPicker() {
return {
restrict: 'E',
template: template,
controller: UserGroupPickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
userGroupPicked: '&',
},
link: function(scope, elem, attrs, ctrl) {
scope.$on("user-group-picker-reset", () => {
ctrl.reset();
});
}
};
}
coreModule.directive('userGroupPicker', userGroupPicker);
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
const template = `
<div class="dropdown">
<gf-form-dropdown model="ctrl.user"
get-options="ctrl.debouncedSearchUsers($query)"
css-class="gf-size-auto"
on-change="ctrl.onChange($option)"
</gf-form-dropdown>
</div>
`;
export class UserPickerCtrl {
user: any;
debouncedSearchUsers: any;
userPicked: any;
/** @ngInject */
constructor(private backendSrv, private $scope, $sce) {
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};
});
}));
}
onChange(option) {
this.userPicked({$user: option.value});
}
reset() {
this.user = {text: 'Choose', value: null};
}
}
export interface User {
id: number;
name: string;
login: string;
email: string;
}
export function userPicker() {
return {
restrict: 'E',
template: template,
controller: UserPickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
userPicked: '&',
},
link: function(scope, elem, attrs, ctrl) {
scope.$on("user-picker-reset", () => {
ctrl.reset();
});
}
};
}
coreModule.directive('userPicker', userPicker);
......@@ -49,6 +49,8 @@ import {helpModal} from './components/help/help';
import {collapseBox} from './components/collapse_box';
import {JsonExplorer} from './components/json_explorer/json_explorer';
import {NavModelSrv, NavModel} from './nav_model_srv';
import {userPicker} from './components/user_picker';
import {userGroupPicker} from './components/user_group_picker';
export {
arrayJoin,
......@@ -77,4 +79,6 @@ export {
JsonExplorer,
NavModelSrv,
NavModel,
userPicker,
userGroupPicker,
};
......@@ -2,8 +2,9 @@ define([
'jquery',
'angular',
'../core_module',
'lodash',
],
function ($, angular, coreModule) {
function ($, angular, coreModule, _) {
'use strict';
var editViewMap = {
......@@ -12,7 +13,13 @@ 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 },
'new-folder': {
isModal: true,
html: '<folder-modal dismiss="dismiss()"></folder-modal>',
modalClass: 'modal--narrow'
}
};
coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) {
......@@ -20,6 +27,7 @@ function ($, angular, coreModule) {
restrict: 'A',
link: function(scope, elem) {
var editorScope;
var modalScope;
var lastEditView;
function hideEditorPane(hideToShowOtherView) {
......@@ -30,8 +38,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) {
......@@ -45,6 +52,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;
......@@ -73,16 +85,17 @@ function ($, angular, coreModule) {
}
};
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'
backdrop: 'static',
modalClass: options.modalClass,
});
return;
......
......@@ -96,6 +96,7 @@ export class NavModelSrv {
{title: 'Preferences', active: subPage === 0, url: 'org', icon: 'fa fa-fw fa-cog'},
{title: 'Org Users', active: subPage === 1, url: 'org/users', icon: 'fa fa-fw fa-users'},
{title: 'API Keys', active: subPage === 2, url: 'org/apikeys', icon: 'fa fa-fw fa-key'},
{title: 'Org User Groups', active: subPage === 3, url: 'org/user-groups', icon: 'fa fa-fw fa-users'},
]
};
}
......@@ -167,6 +168,14 @@ export class NavModelSrv {
clickHandler: () => dashNavCtrl.openEditView('annotations')
});
if (dashboard.meta.canAdmin) {
menu.push({
title: 'Permissions...',
icon: 'fa fa-fw fa-lock',
clickHandler: () => dashNavCtrl.openEditView('permissions')
});
}
if (!dashboard.meta.isHome) {
menu.push({
title: 'Version history',
......@@ -196,9 +205,9 @@ export class NavModelSrv {
clickHandler: () => dashNavCtrl.showHelpModal()
});
if (this.contextSrv.isEditor) {
if (this.contextSrv.isEditor && !dashboard.meta.isFolder) {
menu.push({
title: 'Save As ...',
title: 'Save As...',
icon: 'fa fa-fw fa-save',
clickHandler: () => dashNavCtrl.saveDashboardAs()
});
......
......@@ -34,7 +34,7 @@ function (coreModule) {
rows: [
{
title: 'Dashboard Row',
height: '250px',
height: '350px',
panels:[],
isNew: true,
}
......
......@@ -83,6 +83,18 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
controller : 'OrgApiKeysCtrl',
resolve: loadOrgBundle,
})
.when('/org/user-groups', {
templateUrl: 'public/app/features/org/partials/user_groups.html',
controller : 'UserGroupsCtrl',
controllerAs: 'ctrl',
resolve: loadOrgBundle,
})
.when('/org/user-groups/edit/:id', {
templateUrl: 'public/app/features/org/partials/user_group_details.html',
controller : 'UserGroupDetailsCtrl',
controllerAs: 'ctrl',
resolve: loadOrgBundle,
})
.when('/profile', {
templateUrl: 'public/app/features/org/partials/profile.html',
controller : 'ProfileCtrl',
......
......@@ -211,10 +211,64 @@ export class BackendSrv {
return this.post('/api/dashboards/db/', {
dashboard: dash,
folderId: dash.folderId,
overwrite: options.overwrite === true,
message: options.message || '',
});
}
createDashboardFolder(name) {
const dash = {
title: name,
editable: true,
hideControls: true,
rows: [
{
panels: [
{
folderId: 0,
headings: false,
limit: 1000,
links: [],
query: '',
recent: false,
search: true,
span: 4,
starred: false,
tags: [],
title: 'Dashboards in this folder',
type: 'dashlist'
},
{
onlyAlertsOnDashboard: true,
span: 4,
title: 'Alerts in this folder',
type: 'alertlist'
},
{
span: 4,
title: 'Permissions for this folder',
type: 'permissionlist',
folderId: 0
}
],
showTitle: true,
title: name,
titleSize: 'h1'
}
]
};
return this.post('/api/dashboards/db/', {dashboard: dash, isFolder: true, overwrite: false})
.then(res => {
return this.getDashboard('db', res.slug);
})
.then(res => {
res.dashboard.rows[0].panels[0].folderId = res.dashboard.id;
res.dashboard.rows[0].panels[2].folderId = res.dashboard.id;
return this.saveDashboard(res.dashboard, {overwrite: false});
});
}
}
coreModule.service('backendSrv', BackendSrv);
......@@ -64,9 +64,7 @@ export class ContextSrv {
toggleSideMenu() {
this.sidemenu = !this.sidemenu;
if (!this.sidemenu) {
this.setPinnedState(false);
}
this.setPinnedState(this.sidemenu);
}
}
......
......@@ -7,11 +7,6 @@
<i class="fa fa-info-circle"></i>
How to add an alert
</a>
<a class="btn btn-inverse" href="alerting/notifications" >
<i class="fa fa-cog"></i>
Configure notifications
</a>
</div>
<div class="gf-form-group">
......
<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="modal-content">
<table class="filter-table gf-form-group">
<tr ng-repeat="acl in ctrl.items" ng-class="{'gf-form-disabled': acl.inherited}">
<td style="width: 100%;">
<i class="{{acl.icon}}"></i>
<span ng-bind-html="acl.nameHtml"></span>
</td>
<td>
<em class="muted no-wrap" ng-show="acl.inherited">Inherited from folder</em>
</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)" ng-disabled="acl.inherited"></select>
</div>
</td>
<td>
<a class="btn btn-inverse btn-small" ng-click="ctrl.removeItem($index)" ng-hide="acl.inherited">
<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>
<div class="gf-form-inline">
<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>
</form>
<div class="gf-form width-17">
<span ng-if="ctrl.error" class="text-error p-l-1">
<i class="fa fa-warning"></i>
{{ctrl.error}}
</span>
</div>
</div>
<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>
</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> -->
///<reference path="../../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
export class AclCtrl {
dashboard: any;
items: DashboardAcl[];
permissionOptions = [
{value: 1, text: 'View'},
{value: 2, text: 'Edit'},
{value: 4, text: 'Admin'}
];
aclTypes = [
{value: 'Group', text: 'User Group'},
{value: 'User', text: 'User'},
{value: 'Viewer', text: 'Everyone With Viewer Role'},
{value: 'Editor', text: 'Everyone With Editor Role'}
];
dismiss: () => void;
newType: string;
canUpdate: boolean;
error: string;
readonly duplicateError = 'This permission exists already.';
/** @ngInject */
constructor(private backendSrv, private dashboardSrv, private $sce, private $scope) {
this.items = [];
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.items = _.map(result, this.prepareViewModel.bind(this));
this.sortItems();
});
}
sortItems() {
this.items = _.orderBy(this.items, ['sortRank', 'sortName'], ['desc', 'asc']);
}
prepareViewModel(item: DashboardAcl): DashboardAcl {
item.inherited = !this.dashboard.meta.isFolder && this.dashboard.id !== item.dashboardId;
item.sortRank = 0;
if (item.userId > 0) {
item.icon = "fa fa-fw fa-user";
item.nameHtml = this.$sce.trustAsHtml(item.userLogin);
item.sortName = item.userLogin;
item.sortRank = 10;
} else if (item.userGroupId > 0) {
item.icon = "fa fa-fw fa-users";
item.nameHtml = this.$sce.trustAsHtml(item.userGroup);
item.sortName = item.userGroup;
item.sortRank = 20;
} 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`);
item.sortName = item.role;
item.sortRank = 30;
if (item.role === 'Viewer') {
item.sortRank += 1;
}
}
if (item.inherited) {
item.sortRank += 100;
}
return item;
}
update() {
var updated = [];
for (let item of this.items) {
if (item.inherited) {
continue;
}
updated.push({
id: item.id,
userId: item.userId,
userGroupId: item.userGroupId,
role: item.role,
permission: item.permission,
});
}
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, { items: updated }).then(() => {
return this.dismiss();
});
}
typeChanged() {
if (this.newType === 'Viewer' || this.newType === 'Editor') {
this.addNewItem({permission: 1, role: this.newType});
this.canUpdate = true;
this.resetNewType();
}
}
permissionChanged() {
this.canUpdate = true;
}
addNewItem(item) {
if (!this.isValid(item)) {
return;
}
this.error = '';
item.dashboardId = this.dashboard.id;
this.items.push(this.prepareViewModel(item));
this.sortItems();
this.canUpdate = true;
}
isValid(item) {
const dupe = _.find(this.items, (it) => { return this.isDuplicate(it, item); });
if (dupe) {
this.error = this.duplicateError;
return false;
}
return true;
}
isDuplicate(origItem, newItem) {
if (origItem.inherited) {
return false;
}
return (origItem.role && newItem.role && origItem.role === newItem.role) ||
(origItem.userId && newItem.userId && origItem.userId === newItem.userId) ||
(origItem.userGroupId && newItem.userGroupId && origItem.userGroupId === newItem.userGroupId);
}
userPicked(user) {
this.addNewItem({userId: user.id, userLogin: user.login, permission: 1,});
this.$scope.$broadcast('user-picker-reset');
}
groupPicked(group) {
this.addNewItem({userGroupId: group.id, userGroup: group.name, permission: 1});
this.$scope.$broadcast('user-group-picker-reset');
}
removeItem(index) {
this.items.splice(index, 1);
this.canUpdate = true;
}
}
export function dashAclModal() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/acl/acl.html',
controller: AclCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
dismiss: "&"
}
};
}
export interface FormModel {
dashboardId: number;
userId?: number;
userGroupId?: number;
PermissionType: number;
}
export interface DashboardAcl {
id?: number;
dashboardId?: number;
userId?: number;
userLogin?: string;
userEmail?: string;
userGroupId?: number;
userGroup?: string;
permission?: number;
permissionName?: string;
role?: string;
icon?: string;
nameHtml?: string;
inherited?: boolean;
sortName?: string;
sortRank?: number;
}
coreModule.directive('dashAclModal', dashAclModal);
import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common';
import {AclCtrl} from '../acl';
describe('AclCtrl', () => {
const ctx: any = {};
const backendSrv = {
get: sinon.stub().returns(Promise.resolve([])),
post: sinon.stub().returns(Promise.resolve([]))
};
const dashboardSrv = {
getCurrent: sinon.stub().returns({id: 1, meta: { isFolder: false }})
};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.inject(($rootScope, $controller, $q, $compile) => {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
AclCtrl.prototype.dashboard = {dashboard: {id: 1}};
ctx.ctrl = $controller(AclCtrl, {
$scope: ctx.scope,
backendSrv: backendSrv,
dashboardSrv: dashboardSrv
}, {
dismiss: () => { return; }
});
}));
describe('when permissions are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
const userItem = {
id: 2,
login: 'user2',
};
ctx.ctrl.userPicked(userItem);
const userGroupItem = {
id: 2,
name: 'ug1',
};
ctx.ctrl.groupPicked(userGroupItem);
ctx.ctrl.newType = 'Editor';
ctx.ctrl.typeChanged();
ctx.ctrl.newType = 'Viewer';
ctx.ctrl.typeChanged();
});
it('should sort the result by role, user group and user', () => {
expect(ctx.ctrl.items[0].role).to.eql('Viewer');
expect(ctx.ctrl.items[1].role).to.eql('Editor');
expect(ctx.ctrl.items[2].userGroupId).to.eql(2);
expect(ctx.ctrl.items[3].userId).to.eql(2);
});
it('should save permissions to db', (done) => {
ctx.ctrl.update().then(() => {
done();
});
expect(backendSrv.post.getCall(0).args[0]).to.eql('/api/dashboards/id/1/acl');
expect(backendSrv.post.getCall(0).args[1].items[0].role).to.eql('Viewer');
expect(backendSrv.post.getCall(0).args[1].items[0].permission).to.eql(1);
expect(backendSrv.post.getCall(0).args[1].items[1].role).to.eql('Editor');
expect(backendSrv.post.getCall(0).args[1].items[1].permission).to.eql(1);
expect(backendSrv.post.getCall(0).args[1].items[2].userGroupId).to.eql(2);
expect(backendSrv.post.getCall(0).args[1].items[2].permission).to.eql(1);
expect(backendSrv.post.getCall(0).args[1].items[3].userId).to.eql(2);
expect(backendSrv.post.getCall(0).args[1].items[3].permission).to.eql(1);
});
});
describe('when duplicate role permissions are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
ctx.ctrl.items = [];
ctx.ctrl.newType = 'Editor';
ctx.ctrl.typeChanged();
ctx.ctrl.newType = 'Editor';
ctx.ctrl.typeChanged();
});
it('should throw a validation error', () => {
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
});
it('should not add the duplicate permission', () => {
expect(ctx.ctrl.items.length).to.eql(1);
});
});
describe('when duplicate user permissions are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
ctx.ctrl.items = [];
const userItem = {
id: 2,
login: 'user2',
};
ctx.ctrl.userPicked(userItem);
ctx.ctrl.userPicked(userItem);
});
it('should throw a validation error', () => {
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
});
it('should not add the duplicate permission', () => {
expect(ctx.ctrl.items.length).to.eql(1);
});
});
describe('when duplicate user group permissions are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
ctx.ctrl.items = [];
const userGroupItem = {
id: 2,
name: 'ug1',
};
ctx.ctrl.groupPicked(userGroupItem);
ctx.ctrl.groupPicked(userGroupItem);
});
it('should throw a validation error', () => {
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
});
it('should not add the duplicate permission', () => {
expect(ctx.ctrl.items.length).to.eql(1);
});
});
describe('when one inherited and one not inherited user group permission are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
ctx.ctrl.items = [];
const inheritedUserGroupItem = {
id: 2,
name: 'ug1',
dashboardId: -1
};
ctx.ctrl.items.push(inheritedUserGroupItem);
const userGroupItem = {
id: 2,
name: 'ug1',
};
ctx.ctrl.groupPicked(userGroupItem);
});
it('should not throw a validation error', () => {
expect(ctx.ctrl.error).to.eql('');
});
it('should add both permissions', () => {
expect(ctx.ctrl.items.length).to.eql(2);
});
});
});
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