Merge branch 'master' into backend_plugins
* master: (584 commits) prometheus: change default resolution to 1/1 fix: viewers can edit now works correctly fix: fixed minor ux and firefox issues, fixes #10228 ux: minor fixes profile: use name or fallback for profile page fix: sidemenu profile main text is now username instead of name build: update master version to 5.0.0-pre1 dashfolder: change to migration text ux:s sidemenu icon rules teams: add team count when searching for team changed background color for infobox and new blues in light theme, light theme now uses blue-dark in panel query (#10211) ux: fixed navbar issue when sidemenu closes ux: minor position change for layout selector, fixes #10217 fix: view json from share modal now works, #10217 ux: used new add data sources icon dashfolders: styling of selected filters dashfolders: styling of selected filters dashfolders: fix moving plugin dashboard to folder changelog: adds note about closing #9170 dashfolders: fix folder selection dropdown in dashboard settings ...
This commit is contained in:
+30
-7
@@ -40,9 +40,11 @@ func (hs *HttpServer) registerRoutes() {
|
||||
r.Get("/datasources/", reqSignedIn, Index)
|
||||
r.Get("/datasources/new", reqSignedIn, Index)
|
||||
r.Get("/datasources/edit/*", reqSignedIn, Index)
|
||||
r.Get("/org/users/", reqSignedIn, Index)
|
||||
r.Get("/org/users/new", reqSignedIn, Index)
|
||||
r.Get("/org/users/invite", reqSignedIn, Index)
|
||||
r.Get("/org/apikeys/", reqSignedIn, Index)
|
||||
r.Get("/dashboard/import/", reqSignedIn, Index)
|
||||
r.Get("/configuration", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/settings", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/users", reqGrafanaAdmin, Index)
|
||||
@@ -62,6 +64,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
r.Get("/dashboard-solo/snapshot/*", Index)
|
||||
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
||||
r.Get("/import/dashboard", reqSignedIn, Index)
|
||||
r.Get("/dashboards/", reqSignedIn, Index)
|
||||
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||
|
||||
r.Get("/playlists/", reqSignedIn, Index)
|
||||
@@ -134,6 +137,18 @@ func (hs *HttpServer) registerRoutes() {
|
||||
usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
|
||||
}, reqGrafanaAdmin)
|
||||
|
||||
// team (admin permission required)
|
||||
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||
teamsRoute.Get("/:teamId", wrap(GetTeamById))
|
||||
teamsRoute.Get("/search", wrap(SearchTeams))
|
||||
teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
||||
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
||||
teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
|
||||
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
|
||||
teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
||||
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// org information available to all users.
|
||||
apiRoute.Group("/org", func(orgRoute RouteRegister) {
|
||||
orgRoute.Get("/", wrap(GetOrgCurrent))
|
||||
@@ -224,12 +239,8 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
// Dashboard
|
||||
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
|
||||
dashboardRoute.Get("/db/:slug", GetDashboard)
|
||||
dashboardRoute.Delete("/db/:slug", reqEditorRole, DeleteDashboard)
|
||||
|
||||
dashboardRoute.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
|
||||
dashboardRoute.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
|
||||
dashboardRoute.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
|
||||
dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard))
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
|
||||
|
||||
@@ -237,6 +248,18 @@ func (hs *HttpServer) registerRoutes() {
|
||||
dashboardRoute.Get("/home", wrap(GetHomeDashboard))
|
||||
dashboardRoute.Get("/tags", GetDashboardTags)
|
||||
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
|
||||
|
||||
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
|
||||
dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
|
||||
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
|
||||
dashIdRoute.Post("/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
|
||||
dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
|
||||
aclRoute.Get("/", wrap(GetDashboardAclList))
|
||||
aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
|
||||
aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Dashboard snapshots
|
||||
|
||||
@@ -25,6 +25,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
var gravatarSource string
|
||||
@@ -92,7 +94,7 @@ func (this *Avatar) Update() (err error) {
|
||||
|
||||
type CacheServer struct {
|
||||
notFound *Avatar
|
||||
cache map[string]*Avatar
|
||||
cache *gocache.Cache
|
||||
}
|
||||
|
||||
func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
|
||||
@@ -110,7 +112,9 @@ func (this *CacheServer) Handler(ctx *macaron.Context) {
|
||||
|
||||
var avatar *Avatar
|
||||
|
||||
if avatar, _ = this.cache[hash]; avatar == nil {
|
||||
if obj, exist := this.cache.Get(hash); exist {
|
||||
avatar = obj.(*Avatar)
|
||||
} else {
|
||||
avatar = New(hash)
|
||||
}
|
||||
|
||||
@@ -124,7 +128,7 @@ func (this *CacheServer) Handler(ctx *macaron.Context) {
|
||||
if avatar.notFound {
|
||||
avatar = this.notFound
|
||||
} else {
|
||||
this.cache[hash] = avatar
|
||||
this.cache.Add(hash, avatar, gocache.DefaultExpiration)
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Add("Content-Type", "image/jpeg")
|
||||
@@ -146,7 +150,7 @@ func NewCacheServer() *CacheServer {
|
||||
|
||||
return &CacheServer{
|
||||
notFound: newNotFound(),
|
||||
cache: make(map[string]*Avatar),
|
||||
cache: gocache.New(time.Hour, time.Hour*2),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+128
-73
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -35,23 +35,34 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func GetDashboard(c *middleware.Context) {
|
||||
slug := strings.ToLower(c.Params(":slug"))
|
||||
|
||||
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
|
||||
err := bus.Dispatch(&query)
|
||||
func dashboardGuardianResponse(err error) Response {
|
||||
if err != nil {
|
||||
c.JsonApiErr(404, "Dashboard not found", nil)
|
||||
return
|
||||
return ApiError(500, "Error while checking dashboard permissions", err)
|
||||
} else {
|
||||
return ApiError(403, "Access denied to this dashboard", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func GetDashboard(c *middleware.Context) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
isStarred, err := isDashboardStarredByUser(c, query.Result.Id)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Error while checking if dashboard was starred by user", err)
|
||||
return
|
||||
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||
if canView, err := guardian.CanView(); err != nil || !canView {
|
||||
fmt.Printf("%v", err)
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
dash := query.Result
|
||||
canEdit, _ := guardian.CanEdit()
|
||||
canSave, _ := guardian.CanSave()
|
||||
canAdmin, _ := guardian.CanAdmin()
|
||||
|
||||
isStarred, err := isDashboardStarredByUser(c, dash.Id)
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking if dashboard was starred by user", err)
|
||||
}
|
||||
|
||||
// Finding creator and last updater of the dashboard
|
||||
updater, creator := "Anonymous", "Anonymous"
|
||||
@@ -62,29 +73,44 @@ func GetDashboard(c *middleware.Context) {
|
||||
creator = getUserLogin(dash.CreatedBy)
|
||||
}
|
||||
|
||||
meta := dtos.DashboardMeta{
|
||||
IsStarred: isStarred,
|
||||
Slug: dash.Slug,
|
||||
Type: m.DashTypeDB,
|
||||
CanStar: c.IsSignedIn,
|
||||
CanSave: canSave,
|
||||
CanEdit: canEdit,
|
||||
CanAdmin: canAdmin,
|
||||
Created: dash.Created,
|
||||
Updated: dash.Updated,
|
||||
UpdatedBy: updater,
|
||||
CreatedBy: creator,
|
||||
Version: dash.Version,
|
||||
HasAcl: dash.HasAcl,
|
||||
IsFolder: dash.IsFolder,
|
||||
FolderId: dash.FolderId,
|
||||
FolderTitle: "Root",
|
||||
}
|
||||
|
||||
// lookup folder title
|
||||
if dash.FolderId > 0 {
|
||||
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Dashboard folder could not be read", err)
|
||||
}
|
||||
meta.FolderTitle = query.Result.Title
|
||||
}
|
||||
|
||||
// make sure db version is in sync with json model version
|
||||
dash.Data.Set("version", dash.Version)
|
||||
|
||||
dto := dtos.DashboardFullWithMeta{
|
||||
Dashboard: dash.Data,
|
||||
Meta: dtos.DashboardMeta{
|
||||
IsStarred: isStarred,
|
||||
Slug: slug,
|
||||
Type: m.DashTypeDB,
|
||||
CanStar: c.IsSignedIn,
|
||||
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
|
||||
CanEdit: canEditDashboard(c.OrgRole),
|
||||
Created: dash.Created,
|
||||
Updated: dash.Updated,
|
||||
UpdatedBy: updater,
|
||||
CreatedBy: creator,
|
||||
Version: dash.Version,
|
||||
},
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
// TODO(ben): copy this performance metrics logic for the new API endpoints added
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Get)
|
||||
c.JSON(200, dto)
|
||||
return Json(200, dto)
|
||||
}
|
||||
|
||||
func getUserLogin(userId int64) string {
|
||||
@@ -98,24 +124,32 @@ func getUserLogin(userId int64) string {
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteDashboard(c *middleware.Context) {
|
||||
slug := c.Params(":slug")
|
||||
|
||||
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
|
||||
func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) {
|
||||
query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
c.JsonApiErr(404, "Dashboard not found", nil)
|
||||
return
|
||||
return nil, ApiError(404, "Dashboard not found", err)
|
||||
}
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func DeleteDashboard(c *middleware.Context) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
cmd := m.DeleteDashboardCommand{Slug: slug, OrgId: c.OrgId}
|
||||
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
c.JsonApiErr(500, "Failed to delete dashboard", err)
|
||||
return
|
||||
return ApiError(500, "Failed to delete dashboard", err)
|
||||
}
|
||||
|
||||
var resp = map[string]interface{}{"title": query.Result.Title}
|
||||
|
||||
c.JSON(200, resp)
|
||||
var resp = map[string]interface{}{"title": dash.Title}
|
||||
return Json(200, resp)
|
||||
}
|
||||
|
||||
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
@@ -124,6 +158,20 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
if dash.IsFolder && dash.FolderId > 0 {
|
||||
return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
|
||||
}
|
||||
|
||||
// Check if Title is empty
|
||||
if dash.Title == "" {
|
||||
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
|
||||
}
|
||||
|
||||
if dash.Id == 0 {
|
||||
limitReached, err := middleware.QuotaReached(c, "dashboard")
|
||||
if err != nil {
|
||||
@@ -139,6 +187,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
Message: cmd.Message,
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Overwrite: cmd.Overwrite,
|
||||
}
|
||||
|
||||
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
|
||||
@@ -177,11 +226,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
}
|
||||
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||
return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version})
|
||||
}
|
||||
|
||||
func canEditDashboard(role m.RoleType) bool {
|
||||
return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
|
||||
return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id})
|
||||
}
|
||||
|
||||
func GetHomeDashboard(c *middleware.Context) Response {
|
||||
@@ -209,7 +254,9 @@ func GetHomeDashboard(c *middleware.Context) Response {
|
||||
|
||||
dash := dtos.DashboardFullWithMeta{}
|
||||
dash.Meta.IsHome = true
|
||||
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
|
||||
dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR)
|
||||
dash.Meta.FolderTitle = "Root"
|
||||
|
||||
jsonParser := json.NewDecoder(file)
|
||||
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
|
||||
return ApiError(500, "Failed to load home dashboard", err)
|
||||
@@ -223,39 +270,41 @@ func GetHomeDashboard(c *middleware.Context) Response {
|
||||
}
|
||||
|
||||
func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
||||
rows := dash.Get("rows").MustArray()
|
||||
row := simplejson.NewFromAny(rows[0])
|
||||
panels := dash.Get("panels").MustArray()
|
||||
|
||||
newpanel := simplejson.NewFromAny(map[string]interface{}{
|
||||
"type": "gettingstarted",
|
||||
"id": 123123,
|
||||
"span": 12,
|
||||
"gridPos": map[string]interface{}{
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"w": 24,
|
||||
"h": 4,
|
||||
},
|
||||
})
|
||||
|
||||
panels := row.Get("panels").MustArray()
|
||||
panels = append(panels, newpanel)
|
||||
row.Set("panels", panels)
|
||||
dash.Set("panels", panels)
|
||||
}
|
||||
|
||||
// GetDashboardVersions returns all dashboard versions as JSON
|
||||
func GetDashboardVersions(c *middleware.Context) Response {
|
||||
dashboardId := c.ParamsInt64(":dashboardId")
|
||||
limit := c.QueryInt("limit")
|
||||
start := c.QueryInt("start")
|
||||
dashId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
if limit == 0 {
|
||||
limit = 1000
|
||||
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
query := m.GetDashboardVersionsQuery{
|
||||
OrgId: c.OrgId,
|
||||
DashboardId: dashboardId,
|
||||
Limit: limit,
|
||||
Start: start,
|
||||
DashboardId: dashId,
|
||||
Limit: c.QueryInt("limit"),
|
||||
Start: c.QueryInt("start"),
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashboardId), err)
|
||||
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err)
|
||||
}
|
||||
|
||||
for _, version := range query.Result {
|
||||
@@ -279,17 +328,21 @@ func GetDashboardVersions(c *middleware.Context) Response {
|
||||
|
||||
// GetDashboardVersion returns the dashboard version with the given ID.
|
||||
func GetDashboardVersion(c *middleware.Context) Response {
|
||||
dashboardId := c.ParamsInt64(":dashboardId")
|
||||
version := c.ParamsInt(":id")
|
||||
dashId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
query := m.GetDashboardVersionQuery{
|
||||
OrgId: c.OrgId,
|
||||
DashboardId: dashboardId,
|
||||
Version: version,
|
||||
DashboardId: dashId,
|
||||
Version: c.ParamsInt(":id"),
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", version, dashboardId), err)
|
||||
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err)
|
||||
}
|
||||
|
||||
creator := "Anonymous"
|
||||
@@ -340,19 +393,21 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff
|
||||
|
||||
// RestoreDashboardVersion restores a dashboard to the given version.
|
||||
func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response {
|
||||
dashboardId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
dashQuery := m.GetDashboardQuery{Id: dashboardId, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&dashQuery); err != nil {
|
||||
return ApiError(404, "Dashboard not found", nil)
|
||||
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"))
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
versionQuery := m.GetDashboardVersionQuery{DashboardId: dashboardId, Version: apiCmd.Version, OrgId: c.OrgId}
|
||||
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&versionQuery); err != nil {
|
||||
return ApiError(404, "Dashboard version not found", nil)
|
||||
}
|
||||
|
||||
dashboard := dashQuery.Result
|
||||
version := versionQuery.Result
|
||||
|
||||
saveCmd := m.SaveDashboardCommand{}
|
||||
@@ -360,7 +415,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
|
||||
saveCmd.OrgId = c.OrgId
|
||||
saveCmd.UserId = c.UserId
|
||||
saveCmd.Dashboard = version.Data
|
||||
saveCmd.Dashboard.Set("version", dashboard.Version)
|
||||
saveCmd.Dashboard.Set("version", dash.Version)
|
||||
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
|
||||
|
||||
return PostDashboard(c, saveCmd)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
)
|
||||
|
||||
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,
|
||||
TeamId: item.TeamId,
|
||||
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)
|
||||
}
|
||||
|
||||
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, "")
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
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, TeamId: 1, Permission: m.PERMISSION_VIEW},
|
||||
{Id: 5, OrgId: 1, DashboardId: 1, TeamId: 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
|
||||
})
|
||||
|
||||
teamResp := []*m.Team{}
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = teamResp
|
||||
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 team 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) {
|
||||
teamResp = append(teamResp, &m.Team{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,
|
||||
TeamId: acl.TeamId,
|
||||
}
|
||||
dtos = append(dtos, dto)
|
||||
}
|
||||
|
||||
return dtos
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type fakeDashboardRepo struct {
|
||||
inserted []*dashboards.SaveDashboardItem
|
||||
getDashboard []*m.Dashboard
|
||||
}
|
||||
|
||||
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) {
|
||||
repo.inserted = append(repo.inserted, json)
|
||||
return json.Dashboard, nil
|
||||
}
|
||||
|
||||
var fakeRepo *fakeDashboardRepo
|
||||
|
||||
func TestDashboardApiEndpoint(t *testing.T) {
|
||||
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
|
||||
fakeDash := m.NewDashboard("Child dash")
|
||||
fakeDash.Id = 1
|
||||
fakeDash.FolderId = 1
|
||||
fakeDash.HasAcl = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||
query.Result = fakeDash
|
||||
return nil
|
||||
})
|
||||
|
||||
viewerRole := m.ROLE_VIEWER
|
||||
editorRole := m.ROLE_EDITOR
|
||||
|
||||
aclMockResp := []*m.DashboardAclInfoDTO{
|
||||
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
|
||||
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = aclMockResp
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = []*m.Team{}
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd := m.SaveDashboardCommand{
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"folderId": fakeDash.FolderId,
|
||||
"title": fakeDash.Title,
|
||||
"id": fakeDash.Id,
|
||||
}),
|
||||
}
|
||||
|
||||
Convey("When user is an Org Viewer", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should not be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeFalse)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is an Org Editor", func() {
|
||||
role := m.ROLE_EDITOR
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("When saving a dashboard folder in another folder", func() {
|
||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||
query.Result = fakeDash
|
||||
query.Result.IsFolder = true
|
||||
return nil
|
||||
})
|
||||
invalidCmd := m.SaveDashboardCommand{
|
||||
FolderId: fakeDash.FolderId,
|
||||
IsFolder: true,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"folderId": fakeDash.FolderId,
|
||||
"title": fakeDash.Title,
|
||||
}),
|
||||
}
|
||||
Convey("Should return an error", func() {
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 400)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a dashboard with a parent folder which has an acl", t, func() {
|
||||
fakeDash := m.NewDashboard("Child dash")
|
||||
fakeDash.Id = 1
|
||||
fakeDash.FolderId = 1
|
||||
fakeDash.HasAcl = true
|
||||
setting.ViewersCanEdit = false
|
||||
|
||||
aclMockResp := []*m.DashboardAclInfoDTO{
|
||||
{
|
||||
DashboardId: 1,
|
||||
Permission: m.PERMISSION_EDIT,
|
||||
UserId: 200,
|
||||
},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = aclMockResp
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||
query.Result = fakeDash
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = []*m.Team{}
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd := m.SaveDashboardCommand{
|
||||
FolderId: fakeDash.FolderId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": fakeDash.Id,
|
||||
"folderId": fakeDash.FolderId,
|
||||
"title": fakeDash.Title,
|
||||
}),
|
||||
}
|
||||
|
||||
Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should be denied access", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
|
||||
role := m.ROLE_EDITOR
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should be denied access", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is an Org Viewer but has an edit permission", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
mockResult := []*m.DashboardAclInfoDTO{
|
||||
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = mockResult
|
||||
return nil
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is an Org Viewer and viewers can edit", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
setting.ViewersCanEdit = true
|
||||
|
||||
mockResult := []*m.DashboardAclInfoDTO{
|
||||
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = mockResult
|
||||
return nil
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights but can save should be false", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is an Org Viewer but has an admin permission", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
mockResult := []*m.DashboardAclInfoDTO{
|
||||
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = mockResult
|
||||
return nil
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
So(dash.Meta.CanAdmin, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is an Org Editor but has a view permission", func() {
|
||||
role := m.ROLE_EDITOR
|
||||
|
||||
mockResult := []*m.DashboardAclInfoDTO{
|
||||
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = mockResult
|
||||
return nil
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should not be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeFalse)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
dash := dtos.DashboardFullWithMeta{}
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&dash)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return dash
|
||||
}
|
||||
|
||||
func CallGetDashboardVersion(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
|
||||
query.Result = &m.DashboardVersion{}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = GetDashboardVersion
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallGetDashboardVersions(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(query *m.GetDashboardVersionsQuery) error {
|
||||
query.Result = []*m.DashboardVersionDTO{}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = GetDashboardVersions
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallDeleteDashboard(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = DeleteDashboard
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallPostDashboard(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
|
||||
cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := &scenarioContext{
|
||||
url: url,
|
||||
}
|
||||
viewsPath, _ := filepath.Abs("../../public/views")
|
||||
|
||||
sc.m = macaron.New()
|
||||
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
Directory: viewsPath,
|
||||
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||
}))
|
||||
|
||||
sc.m.Use(middleware.GetContextHandler())
|
||||
sc.m.Use(middleware.Sessioner(&session.Options{}))
|
||||
|
||||
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
sc.context.OrgId = TestOrgID
|
||||
sc.context.OrgRole = role
|
||||
|
||||
return PostDashboard(c, cmd)
|
||||
})
|
||||
|
||||
fakeRepo = &fakeDashboardRepo{}
|
||||
dashboards.SetRepository(fakeRepo)
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package dtos
|
||||
|
||||
import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type UpdateDashboardAclCommand struct {
|
||||
Items []DashboardAclUpdateItem `json:"items"`
|
||||
}
|
||||
|
||||
type DashboardAclUpdateItem struct {
|
||||
UserId int64 `json:"userId"`
|
||||
TeamId int64 `json:"teamId"`
|
||||
Role *m.RoleType `json:"role,omitempty"`
|
||||
Permission m.PermissionType `json:"permission"`
|
||||
}
|
||||
+19
-14
@@ -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 {
|
||||
|
||||
+14
-7
@@ -7,9 +7,10 @@ type IndexViewData struct {
|
||||
AppSubUrl string
|
||||
GoogleAnalyticsId string
|
||||
GoogleTagManagerId string
|
||||
MainNavLinks []*NavLink
|
||||
NavTree []*NavLink
|
||||
BuildVersion string
|
||||
BuildCommit string
|
||||
Theme string
|
||||
NewGrafanaVersionExists bool
|
||||
NewGrafanaVersion string
|
||||
}
|
||||
@@ -20,10 +21,16 @@ type PluginCss struct {
|
||||
}
|
||||
|
||||
type NavLink struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Img string `json:"img,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Divider bool `json:"divider,omitempty"`
|
||||
Children []*NavLink `json:"children,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SubTitle string `json:"subTitle,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Img string `json:"img,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Divider bool `json:"divider,omitempty"`
|
||||
HideFromMenu bool `json:"hideFromMenu,omitempty"`
|
||||
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
||||
Children []*NavLink `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ type AddInviteForm struct {
|
||||
LoginOrEmail string `json:"loginOrEmail" binding:"Required"`
|
||||
Name string `json:"name"`
|
||||
Role m.RoleType `json:"role" binding:"Required"`
|
||||
SkipEmails bool `json:"skipEmails"`
|
||||
SendEmail bool `json:"sendEmail"`
|
||||
}
|
||||
|
||||
type InviteInfo struct {
|
||||
|
||||
@@ -27,6 +27,7 @@ type CurrentUser struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
LightTheme bool `json:"lightTheme"`
|
||||
OrgCount int `json:"orgCount"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
OrgName string `json:"orgName"`
|
||||
OrgRole m.RoleType `json:"orgRole"`
|
||||
|
||||
@@ -143,7 +143,6 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
||||
"disableLoginForm": setting.DisableLoginForm,
|
||||
"disableSignoutMenu": setting.DisableSignoutMenu,
|
||||
"externalUserMngInfo": setting.ExternalUserMngInfo,
|
||||
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
||||
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
||||
|
||||
+148
-43
@@ -50,6 +50,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
Login: c.Login,
|
||||
Email: c.Email,
|
||||
Name: c.Name,
|
||||
OrgCount: c.OrgCount,
|
||||
OrgId: c.OrgId,
|
||||
OrgName: c.OrgName,
|
||||
OrgRole: c.OrgRole,
|
||||
@@ -61,6 +62,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
HelpFlags1: c.HelpFlags1,
|
||||
},
|
||||
Settings: settings,
|
||||
Theme: prefs.Theme,
|
||||
AppUrl: appUrl,
|
||||
AppSubUrl: appSubUrl,
|
||||
GoogleAnalyticsId: setting.GoogleAnalyticsId,
|
||||
@@ -82,52 +84,77 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
themeUrlParam := c.Query("theme")
|
||||
if themeUrlParam == "light" {
|
||||
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"},
|
||||
data.Theme = "light"
|
||||
}
|
||||
|
||||
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"})
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Create",
|
||||
Id: "create",
|
||||
Icon: "fa fa-fw fa-plus",
|
||||
Url: setting.AppSubUrl + "dashboard/new",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
|
||||
{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"},
|
||||
{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
dashboardChildNavs := []*dtos.NavLink{
|
||||
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
|
||||
{Divider: true, HideFromTabs: true},
|
||||
{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"},
|
||||
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"},
|
||||
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"},
|
||||
}
|
||||
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Dashboards",
|
||||
Icon: "icon-gf icon-gf-dashboard",
|
||||
Id: "dashboards",
|
||||
SubTitle: "Manage dashboards & folders",
|
||||
Icon: "gicon gicon-dashboard",
|
||||
Url: setting.AppSubUrl + "/",
|
||||
Children: dashboardChildNavs,
|
||||
})
|
||||
|
||||
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"},
|
||||
if c.IsSignedIn {
|
||||
profileNode := &dtos.NavLink{
|
||||
Text: c.SignedInUser.NameOrFallback(),
|
||||
SubTitle: c.SignedInUser.Login,
|
||||
Id: "profile",
|
||||
Img: data.User.GravatarUrl,
|
||||
Url: setting.AppSubUrl + "/profile",
|
||||
HideFromMenu: true,
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Preferences", Id: "profile-settings", Url: setting.AppSubUrl + "/profile", Icon: "gicon gicon-preferences"},
|
||||
{Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true},
|
||||
},
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Alerting",
|
||||
Icon: "icon-gf icon-gf-alert",
|
||||
Url: setting.AppSubUrl + "/alerting/list",
|
||||
Children: alertChildNavs,
|
||||
})
|
||||
if !setting.DisableSignoutMenu {
|
||||
// add sign out first
|
||||
profileNode.Children = append(profileNode.Children, &dtos.NavLink{
|
||||
Text: "Sign out", Id: "sign-out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self",
|
||||
})
|
||||
}
|
||||
|
||||
data.NavTree = append(data.NavTree, profileNode)
|
||||
}
|
||||
|
||||
if c.OrgRole == m.ROLE_ADMIN {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Data Sources",
|
||||
Icon: "icon-gf icon-gf-datasources",
|
||||
Url: setting.AppSubUrl + "/datasources",
|
||||
})
|
||||
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
||||
alertChildNavs := []*dtos.NavLink{
|
||||
{Text: "Alert Rules", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "gicon gicon-alert-rules"},
|
||||
{Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "gicon gicon-alert-notification-channel"},
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Plugins",
|
||||
Icon: "icon-gf icon-gf-apps",
|
||||
Url: setting.AppSubUrl + "/plugins",
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Alerting",
|
||||
SubTitle: "Alert rules & notifications",
|
||||
Id: "alerting",
|
||||
Icon: "gicon gicon-alert",
|
||||
Url: setting.AppSubUrl + "/alerting/list",
|
||||
Children: alertChildNavs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,6 +167,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
if plugin.Pinned {
|
||||
appLink := &dtos.NavLink{
|
||||
Text: plugin.Name,
|
||||
Id: "plugin-page-" + plugin.Id,
|
||||
Url: plugin.DefaultNavUrl,
|
||||
Img: plugin.Info.Logos.Small,
|
||||
}
|
||||
@@ -168,29 +196,106 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
|
||||
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||
}
|
||||
|
||||
if len(appLink.Children) > 0 {
|
||||
data.MainNavLinks = append(data.MainNavLinks, appLink)
|
||||
data.NavTree = append(data.NavTree, appLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.IsGrafanaAdmin {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Admin",
|
||||
Icon: "fa fa-fw fa-cogs",
|
||||
Url: setting.AppSubUrl + "/admin",
|
||||
if c.OrgRole == m.ROLE_ADMIN {
|
||||
cfgNode := &dtos.NavLink{
|
||||
Id: "cfg",
|
||||
Text: "Configuration",
|
||||
SubTitle: "Organization: " + c.OrgName,
|
||||
Icon: "gicon gicon-cog",
|
||||
Url: setting.AppSubUrl + "/datasources",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"},
|
||||
{Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"},
|
||||
{Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"},
|
||||
{Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"},
|
||||
{
|
||||
Text: "Data Sources",
|
||||
Icon: "gicon gicon-datasources",
|
||||
Description: "Add and configure data sources",
|
||||
Id: "datasources",
|
||||
Url: setting.AppSubUrl + "/datasources",
|
||||
},
|
||||
{
|
||||
Text: "Users",
|
||||
Id: "users",
|
||||
Description: "Manage org members",
|
||||
Icon: "gicon gicon-user",
|
||||
Url: setting.AppSubUrl + "/org/users",
|
||||
},
|
||||
{
|
||||
Text: "Teams",
|
||||
Id: "teams",
|
||||
Description: "Manage org groups",
|
||||
Icon: "gicon gicon-team",
|
||||
Url: setting.AppSubUrl + "/org/teams",
|
||||
},
|
||||
{
|
||||
Text: "Plugins",
|
||||
Id: "plugins",
|
||||
Description: "View and configure plugins",
|
||||
Icon: "gicon gicon-plugins",
|
||||
Url: setting.AppSubUrl + "/plugins",
|
||||
},
|
||||
{
|
||||
Text: "Preferences",
|
||||
Id: "org-settings",
|
||||
Description: "Organization preferences",
|
||||
Icon: "gicon gicon-preferences",
|
||||
Url: setting.AppSubUrl + "/org",
|
||||
},
|
||||
|
||||
{
|
||||
Text: "API Keys",
|
||||
Id: "apikeys",
|
||||
Description: "Create & manage API keys",
|
||||
Icon: "gicon gicon-apikeys",
|
||||
Url: setting.AppSubUrl + "/org/apikeys",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if c.IsGrafanaAdmin {
|
||||
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||
Divider: true, HideFromTabs: true,
|
||||
})
|
||||
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||
Text: "Server Admin",
|
||||
HideFromTabs: true,
|
||||
SubTitle: "Manage all users & orgs",
|
||||
Id: "admin",
|
||||
Icon: "gicon gicon-shield",
|
||||
Url: setting.AppSubUrl + "/admin/users",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
|
||||
{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
|
||||
{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
|
||||
{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
|
||||
{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
data.NavTree = append(data.NavTree, cfgNode)
|
||||
}
|
||||
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Help",
|
||||
Id: "help",
|
||||
Url: "#",
|
||||
Icon: "gicon gicon-question",
|
||||
HideFromMenu: true,
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"},
|
||||
{Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"},
|
||||
{Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"},
|
||||
},
|
||||
})
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
|
||||
}
|
||||
|
||||
// send invite email
|
||||
if !inviteDto.SkipEmails && util.IsEmail(inviteDto.LoginOrEmail) {
|
||||
if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) {
|
||||
emailCmd := m.SendEmailCommand{
|
||||
To: []string{inviteDto.LoginOrEmail},
|
||||
Template: "new_user_invite.html",
|
||||
@@ -99,7 +99,7 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto
|
||||
return ApiError(500, "Error while trying to create org user", err)
|
||||
} else {
|
||||
|
||||
if !inviteDto.SkipEmails && util.IsEmail(user.Email) {
|
||||
if inviteDto.SendEmail && util.IsEmail(user.Email) {
|
||||
emailCmd := m.SendEmailCommand{
|
||||
To: []string{user.Email},
|
||||
Template: "invited_to_org.html",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -31,10 +32,6 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
|
||||
|
||||
userToAdd := userQuery.Result
|
||||
|
||||
// if userToAdd.Id == c.UserId {
|
||||
// return ApiError(400, "Cannot add yourself as user", nil)
|
||||
// }
|
||||
|
||||
cmd.UserId = userToAdd.Id
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
@@ -64,6 +61,10 @@ func getOrgUsersHelper(orgId int64) Response {
|
||||
return ApiError(500, "Failed to get account user", err)
|
||||
}
|
||||
|
||||
for _, user := range query.Result {
|
||||
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -135,9 +135,24 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
req.Header.Add("Authorization", dsAuth)
|
||||
}
|
||||
|
||||
// clear cookie headers
|
||||
// clear cookie header, except for whitelisted cookies
|
||||
var keptCookies []*http.Cookie
|
||||
if proxy.ds.JsonData != nil {
|
||||
if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil {
|
||||
keepCookieNames := keepCookies.MustStringArray()
|
||||
for _, c := range req.Cookies() {
|
||||
for _, v := range keepCookieNames {
|
||||
if c.Name == v {
|
||||
keptCookies = append(keptCookies, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
for _, c := range keptCookies {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
|
||||
// clear X-Forwarded Host/Port/Proto headers
|
||||
req.Header.Del("X-Forwarded-Host")
|
||||
|
||||
@@ -149,6 +149,58 @@ func TestDSRouteRule(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When proxying a data source with no keepCookies specified", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
json, _ := simplejson.NewJson([]byte(`{"keepCookies": []}`))
|
||||
|
||||
ds := &m.DataSource{
|
||||
Type: m.DS_GRAPHITE,
|
||||
Url: "http://graphite:8086",
|
||||
JsonData: json,
|
||||
}
|
||||
|
||||
ctx := &middleware.Context{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl, Header: make(http.Header)}
|
||||
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||
req.Header.Set("Cookie", cookies)
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
Convey("Should clear all cookies", func() {
|
||||
So(req.Header.Get("Cookie"), ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When proxying a data source with keep cookies specified", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
json, _ := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`))
|
||||
|
||||
ds := &m.DataSource{
|
||||
Type: m.DS_GRAPHITE,
|
||||
Url: "http://graphite:8086",
|
||||
JsonData: json,
|
||||
}
|
||||
|
||||
ctx := &middleware.Context{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl, Header: make(http.Header)}
|
||||
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||
req.Header.Set("Cookie", cookies)
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
Convey("Should keep named cookies", func() {
|
||||
So(req.Header.Get("Cookie"), ShouldEqual, "JSESSION_ID=test")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When interpolating string", func() {
|
||||
data := templateData{
|
||||
SecureJsonData: map[string]string{
|
||||
|
||||
+4
-1
@@ -21,9 +21,12 @@ 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", ""),
|
||||
Encoding: queryReader.Get("encoding", ""),
|
||||
}
|
||||
|
||||
pngPath, err := renderer.RenderToPng(renderOpts)
|
||||
|
||||
+14
-3
@@ -14,27 +14,38 @@ func Search(c *middleware.Context) {
|
||||
tags := c.QueryStrings("tag")
|
||||
starred := c.Query("starred")
|
||||
limit := c.QueryInt("limit")
|
||||
dashboardType := c.Query("type")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
folderIds := make([]int64, 0)
|
||||
for _, id := range c.QueryStrings("folderIds") {
|
||||
folderId, err := strconv.ParseInt(id, 10, 64)
|
||||
if err == nil {
|
||||
folderIds = append(folderIds, folderId)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
FolderIds: folderIds,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&searchQuery)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// POST /api/teams
|
||||
func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrTeamNameTaken {
|
||||
return ApiError(409, "Team name taken", err)
|
||||
}
|
||||
return ApiError(500, "Failed to create Team", err)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{
|
||||
"teamId": cmd.Result.Id,
|
||||
"message": "Team created",
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /api/teams/:teamId
|
||||
func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
|
||||
cmd.Id = c.ParamsInt64(":teamId")
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrTeamNameTaken {
|
||||
return ApiError(400, "Team name taken", err)
|
||||
}
|
||||
return ApiError(500, "Failed to update Team", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Team updated")
|
||||
}
|
||||
|
||||
// DELETE /api/teams/:teamId
|
||||
func DeleteTeamById(c *middleware.Context) Response {
|
||||
if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil {
|
||||
if err == m.ErrTeamNotFound {
|
||||
return ApiError(404, "Failed to delete Team. ID not found", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to update Team", err)
|
||||
}
|
||||
return ApiSuccess("Team deleted")
|
||||
}
|
||||
|
||||
// GET /api/teams/search
|
||||
func SearchTeams(c *middleware.Context) Response {
|
||||
perPage := c.QueryInt("perpage")
|
||||
if perPage <= 0 {
|
||||
perPage = 1000
|
||||
}
|
||||
page := c.QueryInt("page")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
query := m.SearchTeamsQuery{
|
||||
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 Teams", err)
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
query.Result.PerPage = perPage
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/teams/:teamId
|
||||
func GetTeamById(c *middleware.Context) Response {
|
||||
query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrTeamNotFound {
|
||||
return ApiError(404, "Team not found", err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to get Team", err)
|
||||
}
|
||||
|
||||
return Json(200, &query.Result)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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/teams/:teamId/members
|
||||
func GetTeamMembers(c *middleware.Context) Response {
|
||||
query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to get Team Members", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
// POST /api/teams/:teamId/members
|
||||
func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response {
|
||||
cmd.TeamId = c.ParamsInt64(":teamId")
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrTeamMemberAlreadyAdded {
|
||||
return ApiError(400, "User is already added to this team", err)
|
||||
}
|
||||
return ApiError(500, "Failed to add Member to Team", err)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{
|
||||
"message": "Member added to Team",
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/teams/:teamId/members/:userId
|
||||
func RemoveTeamMember(c *middleware.Context) Response {
|
||||
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
|
||||
return ApiError(500, "Failed to remove Member from Team", err)
|
||||
}
|
||||
return ApiSuccess("Team Member removed")
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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 TestTeamApiEndpoint(t *testing.T) {
|
||||
Convey("Given two teams", t, func() {
|
||||
mockResult := models.SearchTeamQueryResult{
|
||||
Teams: []*models.SearchTeamDto{
|
||||
{Name: "team1"},
|
||||
{Name: "team2"},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}
|
||||
|
||||
Convey("When searching with no parameters", func() {
|
||||
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchTeamsQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchTeams
|
||||
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("teams").MustArray()), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When searching with page and perpage parameters", func() {
|
||||
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchTeamsQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchTeams
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 10)
|
||||
So(sendPage, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
+6
-1
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -219,7 +220,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 {
|
||||
@@ -247,6 +248,10 @@ func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range query.Result.Users {
|
||||
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
query.Result.PerPage = perPage
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
|
||||
)
|
||||
|
||||
var version = "4.6.0"
|
||||
var version = "5.0.0"
|
||||
var commit = "NA"
|
||||
var buildstamp string
|
||||
var build_date string
|
||||
|
||||
@@ -16,17 +16,22 @@ 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
|
||||
Encoding string
|
||||
}
|
||||
|
||||
var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
|
||||
@@ -74,7 +79,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)
|
||||
@@ -95,6 +104,10 @@ func RenderToPng(params *RenderOpts) (string, error) {
|
||||
"renderKey=" + renderKey,
|
||||
}
|
||||
|
||||
if params.Encoding != "" {
|
||||
cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", params.Encoding)}, cmdArgs...)
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, cmdArgs...)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ func initContextWithAnonymousUser(ctx *Context) bool {
|
||||
|
||||
ctx.IsSignedIn = false
|
||||
ctx.AllowAnonymous = true
|
||||
ctx.SignedInUser = &m.SignedInUser{}
|
||||
ctx.SignedInUser = &m.SignedInUser{IsAnonymous: true}
|
||||
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
|
||||
ctx.OrgId = orgQuery.Result.Id
|
||||
ctx.OrgName = orgQuery.Result.Name
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
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 team 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
|
||||
TeamId 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"`
|
||||
TeamId int64 `json:"teamId"`
|
||||
Team string `json:"team"`
|
||||
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
|
||||
TeamId int64
|
||||
Permission PermissionType
|
||||
|
||||
Result DashboardAcl
|
||||
}
|
||||
|
||||
type RemoveDashboardAclCommand struct {
|
||||
AclId int64
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
//
|
||||
// QUERIES
|
||||
//
|
||||
type GetDashboardAclInfoListQuery struct {
|
||||
DashboardId int64
|
||||
OrgId int64
|
||||
Result []*DashboardAclInfoDTO
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -16,6 +16,7 @@ var (
|
||||
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")
|
||||
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
|
||||
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||
)
|
||||
@@ -49,6 +50,9 @@ type Dashboard struct {
|
||||
|
||||
UpdatedBy int64
|
||||
CreatedBy int64
|
||||
FolderId int64
|
||||
IsFolder bool
|
||||
HasAcl bool
|
||||
|
||||
Title string
|
||||
Data *simplejson.Json
|
||||
@@ -66,6 +70,15 @@ func NewDashboard(title string) *Dashboard {
|
||||
return dash
|
||||
}
|
||||
|
||||
// NewDashboardFolder creates a new dashboard folder
|
||||
func NewDashboardFolder(title string) *Dashboard {
|
||||
folder := NewDashboard(title)
|
||||
folder.Data.Set("schemaVersion", 16)
|
||||
folder.Data.Set("editable", true)
|
||||
folder.Data.Set("hideControls", true)
|
||||
return folder
|
||||
}
|
||||
|
||||
// GetTags turns the tags in data json into go string array
|
||||
func (dash *Dashboard) GetTags() []string {
|
||||
return dash.Data.Get("tags").MustStringArray()
|
||||
@@ -113,6 +126,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
|
||||
}
|
||||
@@ -140,6 +155,8 @@ type SaveDashboardCommand struct {
|
||||
OrgId int64 `json:"-"`
|
||||
RestoredFrom int `json:"-"`
|
||||
PluginId string `json:"-"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
|
||||
UpdatedAt time.Time
|
||||
|
||||
@@ -147,7 +164,7 @@ type SaveDashboardCommand struct {
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,25 +18,25 @@ var (
|
||||
type RoleType string
|
||||
|
||||
const (
|
||||
ROLE_VIEWER RoleType = "Viewer"
|
||||
ROLE_EDITOR RoleType = "Editor"
|
||||
ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor"
|
||||
ROLE_ADMIN RoleType = "Admin"
|
||||
ROLE_VIEWER RoleType = "Viewer"
|
||||
ROLE_EDITOR RoleType = "Editor"
|
||||
ROLE_ADMIN RoleType = "Admin"
|
||||
)
|
||||
|
||||
func (r RoleType) IsValid() bool {
|
||||
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
|
||||
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR
|
||||
}
|
||||
|
||||
func (r RoleType) Includes(other RoleType) bool {
|
||||
if r == ROLE_ADMIN {
|
||||
return true
|
||||
}
|
||||
if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR {
|
||||
|
||||
if r == ROLE_EDITOR {
|
||||
return other != ROLE_ADMIN
|
||||
}
|
||||
|
||||
return r == other
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RoleType) UnmarshalJSON(data []byte) error {
|
||||
@@ -106,6 +106,7 @@ type OrgUserDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
Login string `json:"login"`
|
||||
Role string `json:"role"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrTeamNotFound = errors.New("Team not found")
|
||||
ErrTeamNameTaken = errors.New("Team name is taken")
|
||||
)
|
||||
|
||||
// Team model
|
||||
type Team 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 CreateTeamCommand struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
|
||||
Result Team `json:"-"`
|
||||
}
|
||||
|
||||
type UpdateTeamCommand struct {
|
||||
Id int64
|
||||
Name string
|
||||
}
|
||||
|
||||
type DeleteTeamCommand struct {
|
||||
Id int64
|
||||
}
|
||||
|
||||
type GetTeamByIdQuery struct {
|
||||
Id int64
|
||||
Result *Team
|
||||
}
|
||||
|
||||
type GetTeamsByUserQuery struct {
|
||||
UserId int64 `json:"userId"`
|
||||
Result []*Team `json:"teams"`
|
||||
}
|
||||
|
||||
type SearchTeamsQuery struct {
|
||||
Query string
|
||||
Name string
|
||||
Limit int
|
||||
Page int
|
||||
OrgId int64
|
||||
|
||||
Result SearchTeamQueryResult
|
||||
}
|
||||
|
||||
type SearchTeamDto struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
}
|
||||
|
||||
type SearchTeamQueryResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Teams []*SearchTeamDto `json:"teams"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrTeamMemberAlreadyAdded = errors.New("User is already added to this team")
|
||||
)
|
||||
|
||||
// TeamMember model
|
||||
type TeamMember struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
TeamId int64
|
||||
UserId int64
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
|
||||
type AddTeamMemberCommand struct {
|
||||
UserId int64 `json:"userId" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
TeamId int64 `json:"-"`
|
||||
}
|
||||
|
||||
type RemoveTeamMemberCommand struct {
|
||||
UserId int64
|
||||
TeamId int64
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// QUERIES
|
||||
|
||||
type GetTeamMembersQuery struct {
|
||||
TeamId int64
|
||||
Result []*TeamMemberDTO
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Projections and DTOs
|
||||
|
||||
type TeamMemberDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
TeamId int64 `json:"teamId"`
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
@@ -160,7 +160,9 @@ type SignedInUser struct {
|
||||
Name string
|
||||
Email string
|
||||
ApiKeyId int64
|
||||
OrgCount int
|
||||
IsGrafanaAdmin bool
|
||||
IsAnonymous bool
|
||||
HelpFlags1 HelpFlags1
|
||||
LastSeenAt time.Time
|
||||
}
|
||||
@@ -169,10 +171,28 @@ func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
|
||||
return u.UserId > 0 && time.Since(u.LastSeenAt) > time.Minute*5
|
||||
}
|
||||
|
||||
func (u *SignedInUser) NameOrFallback() string {
|
||||
if u.Name != "" {
|
||||
return u.Name
|
||||
} else if u.Login != "" {
|
||||
return u.Login
|
||||
} else {
|
||||
return u.Email
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateUserLastSeenAtCommand struct {
|
||||
UserId int64
|
||||
}
|
||||
|
||||
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"`
|
||||
@@ -188,6 +208,7 @@ type UserSearchHitDTO struct {
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
LastSeenAtAge string `json:"lastSeenAtAge"`
|
||||
|
||||
@@ -69,6 +69,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
||||
UserId: cmd.UserId,
|
||||
Overwrite: cmd.Overwrite,
|
||||
PluginId: cmd.PluginId,
|
||||
FolderId: dashboard.FolderId,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&saveCmd); err != nil {
|
||||
|
||||
@@ -13,16 +13,9 @@ import (
|
||||
)
|
||||
|
||||
func TestDashboardImport(t *testing.T) {
|
||||
|
||||
Convey("When importing plugin dashboard", t, func() {
|
||||
setting.Cfg = ini.Empty()
|
||||
sec, _ := setting.Cfg.NewSection("plugin.test-app")
|
||||
sec.NewKey("path", "../../tests/test-app")
|
||||
err := Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
pluginScenario("When importing a plugin dashboard", t, func() {
|
||||
var importedDash *m.Dashboard
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
|
||||
importedDash = cmd.GetDashboardModel()
|
||||
cmd.Result = importedDash
|
||||
@@ -39,7 +32,7 @@ func TestDashboardImport(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
err = ImportDashboard(&cmd)
|
||||
err := ImportDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should install dashboard", func() {
|
||||
@@ -59,16 +52,16 @@ func TestDashboardImport(t *testing.T) {
|
||||
|
||||
Convey("When evaling dashboard template", t, func() {
|
||||
template, _ := simplejson.NewJson([]byte(`{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_NAME",
|
||||
"type": "datasource"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"prop": "${DS_NAME}"
|
||||
}
|
||||
}`))
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_NAME",
|
||||
"type": "datasource"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"prop": "${DS_NAME}"
|
||||
}
|
||||
}`))
|
||||
|
||||
evaluator := &DashTemplateEvaluator{
|
||||
template: template,
|
||||
@@ -92,3 +85,16 @@ func TestDashboardImport(t *testing.T) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func pluginScenario(desc string, t *testing.T, fn func()) {
|
||||
Convey("Given a plugin", t, func() {
|
||||
setting.Cfg = ini.Empty()
|
||||
sec, _ := setting.Cfg.NewSection("plugin.test-app")
|
||||
sec.NewKey("path", "../../tests/test-app")
|
||||
err := Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey(desc, fn)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -75,14 +75,6 @@ func (c *EvalContext) ShouldUpdateAlertState() bool {
|
||||
return c.Rule.State != c.PrevAlertState
|
||||
}
|
||||
|
||||
func (c *EvalContext) ShouldSendNotification() bool {
|
||||
if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *EvalContext) GetDurationMs() float64 {
|
||||
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
|
||||
}
|
||||
|
||||
@@ -28,21 +28,5 @@ func TestAlertingEvalContext(t *testing.T) {
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should send notifications", func() {
|
||||
Convey("pending -> ok", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Rule.State = models.AlertStateOK
|
||||
|
||||
So(ctx.ShouldSendNotification(), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.State = models.AlertStateAlerting
|
||||
|
||||
So(ctx.ShouldSendNotification(), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
||||
break
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
firing = cr.Firing
|
||||
noDataFound = cr.NoDataFound
|
||||
}
|
||||
|
||||
// calculating Firing based on operator
|
||||
if cr.Operator == "or" {
|
||||
firing = firing || cr.Firing
|
||||
|
||||
@@ -36,6 +36,16 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
So(context.ConditionEvals, ShouldEqual, "true = true")
|
||||
})
|
||||
|
||||
Convey("Show return triggered with single passing condition2", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
|
||||
})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, true)
|
||||
So(context.ConditionEvals, ShouldEqual, "true = true")
|
||||
})
|
||||
|
||||
Convey("Show return false with not passing asdf", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
@@ -131,6 +141,33 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
|
||||
})
|
||||
|
||||
Convey("Should return false if no condition is firing using OR operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false")
|
||||
})
|
||||
|
||||
Convey("Should retuasdfrn no data if one condition has nodata", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
},
|
||||
})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.NoDataFound, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Should return no data if one condition has nodata", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
|
||||
@@ -15,7 +15,7 @@ type Notifier interface {
|
||||
Notify(evalContext *EvalContext) error
|
||||
GetType() string
|
||||
NeedsImage() bool
|
||||
PassesFilter(rule *Rule) bool
|
||||
ShouldNotify(evalContext *EvalContext) bool
|
||||
|
||||
GetNotifierId() int64
|
||||
GetIsDefault() bool
|
||||
|
||||
@@ -24,7 +24,7 @@ type NotifierPlugin struct {
|
||||
}
|
||||
|
||||
type NotificationService interface {
|
||||
Send(context *EvalContext) error
|
||||
SendIfNeeded(context *EvalContext) error
|
||||
}
|
||||
|
||||
func NewNotificationService() NotificationService {
|
||||
@@ -41,14 +41,12 @@ func newNotificationService() *notificationService {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *notificationService) Send(context *EvalContext) error {
|
||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
func (n *notificationService) SendIfNeeded(context *EvalContext) error {
|
||||
notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
|
||||
|
||||
if len(notifiers) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -67,7 +65,7 @@ func (n *notificationService) sendNotifications(context *EvalContext, notifiers
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
not := notifier //avoid updating scope variable in go routine
|
||||
n.log.Info("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
||||
n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
||||
metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
|
||||
g.Go(func() error { return not.Notify(context) })
|
||||
}
|
||||
@@ -82,10 +80,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 {
|
||||
@@ -109,7 +108,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
||||
func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
@@ -121,7 +120,7 @@ func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64,
|
||||
if not, err := n.createNotifierFor(notification); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if shouldUseNotification(not, context) {
|
||||
if not.ShouldNotify(context) {
|
||||
result = append(result, not)
|
||||
}
|
||||
}
|
||||
@@ -139,18 +138,6 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
|
||||
return notifierPlugin.Factory(model)
|
||||
}
|
||||
|
||||
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
|
||||
if !context.Firing {
|
||||
return true
|
||||
}
|
||||
|
||||
if context.Error != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return notifier.PassesFilter(context.Rule)
|
||||
}
|
||||
|
||||
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||
|
||||
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type FakeNotifier struct {
|
||||
FakeMatchResult bool
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) GetType() string {
|
||||
return "FakeNotifier"
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) NeedsImage() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *FakeNotifier) GetNotifierId() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (n *FakeNotifier) GetIsDefault() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
|
||||
|
||||
func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
|
||||
return fn.FakeMatchResult
|
||||
}
|
||||
|
||||
func TestAlertNotificationExtraction(t *testing.T) {
|
||||
|
||||
Convey("Notifier tests", t, func() {
|
||||
Convey("none firing alerts", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: false,
|
||||
Rule: &Rule{
|
||||
State: m.AlertStateAlerting,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("execution error cannot be ignored", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Error: fmt.Errorf("I used to be a programmer just like you"),
|
||||
Rule: &Rule{
|
||||
State: m.AlertStateOK,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("firing alert that match", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Rule: &Rule{
|
||||
State: models.AlertStateAlerting,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: true}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("firing alert that dont match", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Rule: &Rule{State: m.AlertStateOK},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "prometheus-alertmanager",
|
||||
Name: "Prometheus Alertmanager",
|
||||
Description: "Sends alert to Prometheus Alertmanager",
|
||||
Factory: NewAlertmanagerNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Alertmanager settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="http://localhost:9093"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||
}
|
||||
|
||||
return &AlertmanagerNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.prometheus-alertmanager"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type AlertmanagerNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool {
|
||||
return evalContext.Rule.State == m.AlertStateAlerting
|
||||
}
|
||||
|
||||
func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
alerts := make([]interface{}, 0)
|
||||
for _, match := range evalContext.EvalMatches {
|
||||
alertJSON := simplejson.New()
|
||||
alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339))
|
||||
|
||||
if ruleUrl, err := evalContext.GetRuleUrl(); err == nil {
|
||||
alertJSON.Set("generatorURL", ruleUrl)
|
||||
}
|
||||
|
||||
if evalContext.Rule.Message != "" {
|
||||
alertJSON.SetPath([]string{"annotations", "description"}, evalContext.Rule.Message)
|
||||
}
|
||||
|
||||
tags := make(map[string]string)
|
||||
if len(match.Tags) == 0 {
|
||||
tags["metric"] = match.Metric
|
||||
} else {
|
||||
for k, v := range match.Tags {
|
||||
tags[k] = v
|
||||
}
|
||||
}
|
||||
tags["alertname"] = evalContext.Rule.Name
|
||||
alertJSON.Set("labels", tags)
|
||||
|
||||
alerts = append(alerts, alertJSON)
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.NewFromAny(alerts)
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.Url + "/api/v1/alerts",
|
||||
HttpMethod: "POST",
|
||||
Body: string(body),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send alertmanager", "error", err, "alertmanager", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAlertmanagerNotifier(t *testing.T) {
|
||||
Convey("Alertmanager notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "alertmanager",
|
||||
Type: "alertmanager",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewAlertmanagerNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("from settings", func() {
|
||||
json := `{ "url": "http://127.0.0.1:9093/" }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "alertmanager",
|
||||
Type: "alertmanager",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewAlertmanagerNotifier(model)
|
||||
alertmanagerNotifier := not.(*AlertmanagerNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(alertmanagerNotifier.Url, ShouldEqual, "http://127.0.0.1:9093/")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package notifiers
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
@@ -14,7 +15,7 @@ type NotifierBase struct {
|
||||
}
|
||||
|
||||
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
|
||||
uploadImage := model.Get("uploadImage").MustBool(true)
|
||||
uploadImage := model.Get("uploadImage").MustBool(false)
|
||||
|
||||
return NotifierBase{
|
||||
Id: id,
|
||||
@@ -25,7 +26,13 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NotifierBase) PassesFilter(rule *alerting.Rule) bool {
|
||||
func defaultShouldNotify(context *alerting.EvalContext) bool {
|
||||
if context.PrevAlertState == context.Rule.State {
|
||||
return false
|
||||
}
|
||||
if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestBaseNotifier(t *testing.T) {
|
||||
Convey("Base notifier tests", t, func() {
|
||||
Convey("should notify", func() {
|
||||
Convey("pending -> ok", func() {
|
||||
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: m.AlertStatePending,
|
||||
})
|
||||
context.Rule.State = m.AlertStateOK
|
||||
So(defaultShouldNotify(context), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
context.Rule.State = m.AlertStateAlerting
|
||||
So(defaultShouldNotify(context), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -38,6 +38,10 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (this *DingDingNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
type DingDingNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestDingDingNotifier(t *testing.T) {
|
||||
Convey("Line notifier tests", t, func() {
|
||||
Convey("Dingding notifier tests", t, func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
@@ -25,10 +25,8 @@ func TestDingDingNotifier(t *testing.T) {
|
||||
|
||||
})
|
||||
Convey("settings should trigger incident", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "https://www.google.com"
|
||||
}`
|
||||
json := `{ "url": "https://www.google.com" }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "dingding_testing",
|
||||
|
||||
@@ -58,6 +58,10 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (this *EmailNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending alert notification to", "addresses", this.Addresses)
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ type HipChatNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *HipChatNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ type KafkaNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *KafkaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
state := evalContext.Rule.State
|
||||
|
||||
@@ -51,6 +51,10 @@ type LineNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *LineNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ type OpsGenieNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *OpsGenieNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
var err error
|
||||
|
||||
@@ -63,6 +63,10 @@ type PagerdutyNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *PagerdutyNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve {
|
||||
|
||||
@@ -123,6 +123,10 @@ type PushoverNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *PushoverNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
|
||||
@@ -71,6 +71,10 @@ type SensuNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *SensuNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending sensu result")
|
||||
|
||||
|
||||
@@ -98,6 +98,10 @@ type SlackNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *SlackNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ func TestSlackNotifier(t *testing.T) {
|
||||
So(slackNotifier.Mention, ShouldEqual, "@carl")
|
||||
So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ type TeamsNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *TeamsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
|
||||
@@ -76,6 +76,10 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending alert notification to", "bot_token", this.BotToken)
|
||||
this.log.Info("Sending alert notification to", "chat_id", this.ChatID)
|
||||
|
||||
@@ -114,6 +114,10 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (this *ThreemaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
|
||||
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)
|
||||
|
||||
@@ -68,6 +68,10 @@ type VictoropsNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *VictoropsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
// Notify sends notification to Victorops via POST to URL endpoint
|
||||
func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
@@ -65,6 +65,10 @@ type WebhookNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *WebhookNotifier) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending webhook")
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
Type: "webhook",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
Type: "webhook",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(webhookNotifier.Name, ShouldEqual, "ops")
|
||||
So(webhookNotifier.Type, ShouldEqual, "email")
|
||||
So(webhookNotifier.Type, ShouldEqual, "webhook")
|
||||
So(webhookNotifier.Url, ShouldEqual, "http://google.com")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -85,11 +85,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
if err := annotationRepo.Save(&item); err != nil {
|
||||
handler.log.Error("Failed to save annotation for new alert state", "error", err)
|
||||
}
|
||||
|
||||
if evalContext.ShouldSendNotification() {
|
||||
handler.notifier.Send(evalContext)
|
||||
}
|
||||
}
|
||||
|
||||
handler.notifier.SendIfNeeded(evalContext)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,14 +23,12 @@ func SetRepository(rep Repository) {
|
||||
}
|
||||
|
||||
type SaveDashboardItem struct {
|
||||
TitleLower string
|
||||
OrgId int64
|
||||
Folder string
|
||||
UpdatedAt time.Time
|
||||
UserId int64
|
||||
Message string
|
||||
Overwrite bool
|
||||
Dashboard *models.Dashboard
|
||||
OrgId int64
|
||||
UpdatedAt time.Time
|
||||
UserId int64
|
||||
Message string
|
||||
Overwrite bool
|
||||
Dashboard *models.Dashboard
|
||||
}
|
||||
|
||||
type DashboardRepository struct{}
|
||||
@@ -57,6 +55,8 @@ func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.D
|
||||
OrgId: json.OrgId,
|
||||
Overwrite: json.Overwrite,
|
||||
UserId: json.UserId,
|
||||
FolderId: dashboard.FolderId,
|
||||
IsFolder: dashboard.IsFolder,
|
||||
}
|
||||
|
||||
if !json.UpdatedAt.IsZero() {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package guardian
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type DashboardGuardian struct {
|
||||
user *m.SignedInUser
|
||||
dashId int64
|
||||
orgId int64
|
||||
acl []*m.DashboardAclInfoDTO
|
||||
groups []*m.Team
|
||||
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) {
|
||||
if setting.ViewersCanEdit {
|
||||
return g.HasPermission(m.PERMISSION_VIEW)
|
||||
}
|
||||
|
||||
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
|
||||
teamAclItems := []*m.DashboardAclInfoDTO{}
|
||||
|
||||
for _, p := range acl {
|
||||
// user match
|
||||
if !g.user.IsAnonymous {
|
||||
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.TeamId > 0 {
|
||||
teamAclItems = append(teamAclItems, p)
|
||||
}
|
||||
}
|
||||
|
||||
// do we have group rules?
|
||||
if len(teamAclItems) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// load groups
|
||||
teams, err := g.getTeams()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// evalute group rules
|
||||
for _, p := range acl {
|
||||
for _, ug := range teams {
|
||||
if ug.Id == p.TeamId && 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) getTeams() ([]*m.Team, error) {
|
||||
if g.groups != nil {
|
||||
return g.groups, nil
|
||||
}
|
||||
|
||||
query := m.GetTeamsByUserQuery{UserId: g.user.UserId}
|
||||
err := bus.Dispatch(&query)
|
||||
|
||||
g.groups = query.Result
|
||||
return query.Result, err
|
||||
}
|
||||
@@ -121,6 +121,9 @@ func (fr *fileReader) walkFolder() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// id = 0 indicates ID validation should be avoided before writing to the db.
|
||||
dash.Dashboard.Id = 0
|
||||
|
||||
cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug}
|
||||
err = bus.Dispatch(cmd)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -23,11 +22,9 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
|
||||
|
||||
dash := &dashboards.SaveDashboardItem{}
|
||||
dash.Dashboard = models.NewDashboardFromJson(data)
|
||||
dash.TitleLower = strings.ToLower(dash.Dashboard.Title)
|
||||
dash.UpdatedAt = lastModified
|
||||
dash.Overwrite = true
|
||||
dash.OrgId = cfg.OrgId
|
||||
dash.Folder = cfg.Folder
|
||||
dash.Dashboard.Data.Set("editable", cfg.Editable)
|
||||
|
||||
if dash.Dashboard.Title == "" {
|
||||
|
||||
@@ -12,33 +12,24 @@ func Init() {
|
||||
}
|
||||
|
||||
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,
|
||||
FolderIds: query.FolderIds,
|
||||
Tags: query.Tags,
|
||||
Limit: query.Limit,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&dashQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hits := make(HitList, 0)
|
||||
hits = append(hits, dashQuery.Result...)
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -52,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
|
||||
}
|
||||
|
||||
@@ -60,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 {
|
||||
|
||||
@@ -11,13 +11,15 @@ import (
|
||||
func TestSearch(t *testing.T) {
|
||||
|
||||
Convey("Given search query", t, func() {
|
||||
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
|
||||
})
|
||||
@@ -27,34 +29,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")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,37 +1,55 @@
|
||||
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"`
|
||||
Slug string `json:"slug"`
|
||||
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
|
||||
FolderIds []int64
|
||||
|
||||
Result HitList
|
||||
}
|
||||
@@ -39,9 +57,14 @@ type Query struct {
|
||||
type FindPersistedDashboardsQuery struct {
|
||||
Title string
|
||||
OrgId int64
|
||||
UserId int64
|
||||
SignedInUser *models.SignedInUser
|
||||
IsStarred bool
|
||||
DashboardIds []int
|
||||
DashboardIds []int64
|
||||
Type string
|
||||
FolderIds []int64
|
||||
Tags []string
|
||||
Limit int
|
||||
IsBrowse bool
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -70,6 +68,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = setHasAcl(sess, dash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parentVersion := dash.Version
|
||||
affectedRows := int64(0)
|
||||
|
||||
@@ -79,14 +82,14 @@ 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)
|
||||
|
||||
if !cmd.UpdatedAt.IsZero() {
|
||||
dash.Updated = cmd.UpdatedAt
|
||||
}
|
||||
|
||||
affectedRows, err = sess.Id(dash.Id).Update(dash)
|
||||
affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -115,7 +118,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
|
||||
@@ -130,13 +133,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)
|
||||
@@ -153,64 +180,76 @@ 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 {
|
||||
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`)
|
||||
|
||||
if query.IsStarred {
|
||||
sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
|
||||
func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
|
||||
limit := query.Limit
|
||||
if limit == 0 {
|
||||
limit = 1000
|
||||
}
|
||||
|
||||
sql.WriteString(` WHERE dashboard.org_id=?`)
|
||||
|
||||
params = append(params, query.OrgId)
|
||||
sb := NewSearchBuilder(query.SignedInUser, limit).
|
||||
WithTags(query.Tags).
|
||||
WithDashboardIdsIn(query.DashboardIds)
|
||||
|
||||
if query.IsStarred {
|
||||
sql.WriteString(` AND star.user_id=?`)
|
||||
params = append(params, query.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 = ?")
|
||||
params = append(params, dashboardId)
|
||||
}
|
||||
sql.WriteString(")")
|
||||
sb.IsStarred()
|
||||
}
|
||||
|
||||
if len(query.Title) > 0 {
|
||||
sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?")
|
||||
params = append(params, "%"+query.Title+"%")
|
||||
sb.WithTitle(query.Title)
|
||||
}
|
||||
|
||||
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
|
||||
if len(query.Type) > 0 {
|
||||
sb.WithType(query.Type)
|
||||
}
|
||||
|
||||
if len(query.FolderIds) > 0 {
|
||||
sb.WithFolderIds(query.FolderIds)
|
||||
}
|
||||
|
||||
var res []DashboardSearchProjection
|
||||
|
||||
err := x.Sql(sql.String(), params...).Find(&res)
|
||||
sql, params := sb.ToSql()
|
||||
err := x.Sql(sql, 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)
|
||||
|
||||
@@ -218,11 +257,15 @@ 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,
|
||||
Slug: 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
|
||||
@@ -231,8 +274,6 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
|
||||
hit.Tags = append(hit.Tags, item.Term)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
|
||||
@@ -252,7 +293,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
|
||||
@@ -266,6 +307,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 = ?",
|
||||
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||
}
|
||||
|
||||
@@ -304,7 +346,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
|
||||
func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
|
||||
var dashboards = make([]*m.Dashboard, 0)
|
||||
|
||||
err := x.Where("org_id=? AND plugin_id=?", query.OrgId, query.PluginId).Find(&dashboards)
|
||||
err := x.Where("org_id=? AND plugin_id=? AND is_folder=0", query.OrgId, query.PluginId).Find(&dashboards)
|
||||
query.Result = dashboards
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
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.TeamId == 0 && !item.Role.IsValid() {
|
||||
return m.ErrDashboardAclInfoMissing
|
||||
}
|
||||
|
||||
if item.DashboardId == 0 {
|
||||
return m.ErrDashboardPermissionDashboardEmpty
|
||||
}
|
||||
|
||||
sess.Nullable("user_id", "team_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.TeamId == 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 (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, 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 (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId).Update(&entity); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
entity := m.DashboardAcl{
|
||||
OrgId: cmd.OrgId,
|
||||
TeamId: cmd.TeamId,
|
||||
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.TeamId != 0 {
|
||||
cols = append(cols, "team_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.team_id,
|
||||
da.permission,
|
||||
da.role,
|
||||
da.created,
|
||||
da.updated,
|
||||
u.login AS user_login,
|
||||
u.email AS user_email,
|
||||
ug.name AS team
|
||||
FROM` + dialect.Quote("dashboard_acl") + ` as da
|
||||
LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
|
||||
LEFT OUTER JOIN team ug on ug.id = da.team_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.team_id,
|
||||
da.permission,
|
||||
da.role,
|
||||
da.created,
|
||||
da.updated,
|
||||
'' as user_login,
|
||||
'' as user_email,
|
||||
'' as team
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
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 teamId 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 team", func() {
|
||||
group1 := m.CreateTeamCommand{Name: "group1 name", OrgId: 1}
|
||||
err := CreateTeam(&group1)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should be able to add a user permission for a team", func() {
|
||||
setDashAclCmd := m.SetDashboardAclCommand{
|
||||
OrgId: 1,
|
||||
TeamId: 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].TeamId, ShouldEqual, group1.Result.Id)
|
||||
|
||||
Convey("Should be able to delete an existing permission for a team", 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 team", func() {
|
||||
err := SetDashboardAcl(&m.SetDashboardAclCommand{
|
||||
OrgId: 1,
|
||||
TeamId: 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].TeamId, ShouldEqual, group1.Result.Id)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -3,44 +3,39 @@ package sqlstore
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: orgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": title,
|
||||
"tags": tags,
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return cmd.Result
|
||||
}
|
||||
|
||||
func TestDashboardDataAccess(t *testing.T) {
|
||||
var x *xorm.Engine
|
||||
|
||||
Convey("Testing DB", t, func() {
|
||||
InitTestDB(t)
|
||||
x = InitTestDB(t)
|
||||
|
||||
Convey("Given saved dashboard", func() {
|
||||
savedDash := insertTestDashboard("test dash 23", 1, "prod", "webapp")
|
||||
insertTestDashboard("test dash 45", 1, "prod")
|
||||
insertTestDashboard("test dash 67", 1, "prod", "webapp")
|
||||
savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
|
||||
savedDash := insertTestDashboard("test dash 23", 1, savedFolder.Id, false, "prod", "webapp")
|
||||
insertTestDashboard("test dash 45", 1, savedFolder.Id, false, "prod")
|
||||
insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp")
|
||||
|
||||
Convey("Should return dashboard model", func() {
|
||||
So(savedDash.Title, ShouldEqual, "test dash 23")
|
||||
So(savedDash.Slug, ShouldEqual, "test-dash-23")
|
||||
So(savedDash.Id, ShouldNotEqual, 0)
|
||||
So(savedDash.IsFolder, ShouldBeFalse)
|
||||
So(savedDash.FolderId, ShouldBeGreaterThan, 0)
|
||||
|
||||
So(savedFolder.Title, ShouldEqual, "1 test dash folder")
|
||||
So(savedFolder.Slug, ShouldEqual, "1-test-dash-folder")
|
||||
So(savedFolder.Id, ShouldNotEqual, 0)
|
||||
So(savedFolder.IsFolder, ShouldBeTrue)
|
||||
So(savedFolder.FolderId, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard", func() {
|
||||
@@ -54,15 +49,14 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
|
||||
So(query.Result.Title, ShouldEqual, "test dash 23")
|
||||
So(query.Result.Slug, ShouldEqual, "test-dash-23")
|
||||
So(query.Result.IsFolder, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Should be able to delete dashboard", func() {
|
||||
insertTestDashboard("delete me", 1, "delete this")
|
||||
|
||||
dashboardSlug := slug.Make("delete me")
|
||||
dash := insertTestDashboard("delete me", 1, 0, false, "delete this")
|
||||
|
||||
err := DeleteDashboard(&m.DeleteDashboardCommand{
|
||||
Slug: dashboardSlug,
|
||||
Id: dash.Id,
|
||||
OrgId: 1,
|
||||
})
|
||||
|
||||
@@ -102,10 +96,11 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Should be able to search for dashboard", func() {
|
||||
Convey("Should be able to search for dashboard folder", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
Title: "test dash 23",
|
||||
OrgId: 1,
|
||||
Title: "1 test dash folder",
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
@@ -113,14 +108,29 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
hit := query.Result[0]
|
||||
So(len(hit.Tags), ShouldEqual, 2)
|
||||
So(hit.Type, ShouldEqual, search.DashHitFolder)
|
||||
})
|
||||
|
||||
Convey("Should be able to search for a dashboard folder's children", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
OrgId: 1,
|
||||
FolderIds: []int64{savedFolder.Id},
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
hit := query.Result[0]
|
||||
So(hit.Id, ShouldEqual, savedDash.Id)
|
||||
})
|
||||
|
||||
Convey("Should be able to search for dashboard by dashboard ids", func() {
|
||||
Convey("should be able to find two dashboards by id", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
DashboardIds: []int{1, 2},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{2, 3},
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
@@ -137,8 +147,8 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
|
||||
Convey("DashboardIds that does not exists should not cause errors", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
DashboardIds: []int{1000},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{1000},
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
@@ -161,6 +171,63 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Should be able to update dashboard and remove folderId", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": 1,
|
||||
"title": "folderId",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
Overwrite: true,
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.FolderId, ShouldEqual, 2)
|
||||
|
||||
cmd = m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": 1,
|
||||
"title": "folderId",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 0,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := m.GetDashboardQuery{
|
||||
Slug: cmd.Result.Slug,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
err = GetDashboard(&query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.FolderId, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Should be able to delete a dashboard folder and its children", func() {
|
||||
deleteCmd := &m.DeleteDashboardCommand{Id: savedFolder.Id}
|
||||
err := DeleteDashboard(deleteCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
OrgId: 1,
|
||||
FolderIds: []int64{savedFolder.Id},
|
||||
SignedInUser: &m.SignedInUser{},
|
||||
}
|
||||
|
||||
err = SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard tags", func() {
|
||||
query := m.GetDashboardTagsQuery{OrgId: 1}
|
||||
|
||||
@@ -171,7 +238,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() {
|
||||
starredDash := insertTestDashboard("starred dash", 1)
|
||||
starredDash := insertTestDashboard("starred dash", 1, 0, false)
|
||||
StarDashboard(&m.StarDashboardCommand{
|
||||
DashboardId: starredDash.Id,
|
||||
UserId: 10,
|
||||
@@ -183,7 +250,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should be able to search for starred dashboards", func() {
|
||||
query := search.FindPersistedDashboardsQuery{OrgId: 1, UserId: 10, IsStarred: true}
|
||||
query := search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: 10, OrgId: 1}, IsStarred: true}
|
||||
err := SearchDashboards(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
@@ -192,5 +259,307 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() {
|
||||
folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
|
||||
dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp")
|
||||
childDash := insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp")
|
||||
insertTestDashboard("test dash 45", 1, folder.Id, false, "prod")
|
||||
|
||||
currentUser := createUser("viewer", "Viewer", false)
|
||||
|
||||
Convey("and no acls are set", func() {
|
||||
Convey("should return all dashboards", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for dashboard folder", func() {
|
||||
var otherUser int64 = 999
|
||||
updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should not return folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
|
||||
Convey("when the user is given permission", func() {
|
||||
updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should be able to access folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when the user is an admin", func() {
|
||||
Convey("should be able to access folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{
|
||||
UserId: currentUser.Id,
|
||||
OrgId: 1,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for dashboard child and folder has all permissions removed", func() {
|
||||
var otherUser int64 = 999
|
||||
aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
|
||||
removeAcl(aclId)
|
||||
updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should not return folder or child", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
|
||||
Convey("when the user is given permission to child", func() {
|
||||
updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should be able to search for child dashboard but not folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, childDash.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when the user is an admin", func() {
|
||||
Convey("should be able to search for child dash and folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{
|
||||
UserId: currentUser.Id,
|
||||
OrgId: 1,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 3)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func() {
|
||||
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
|
||||
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
|
||||
dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod")
|
||||
childDash1 := insertTestDashboard("child dash 1", 1, folder1.Id, false, "prod")
|
||||
childDash2 := insertTestDashboard("child dash 2", 1, folder2.Id, false, "prod")
|
||||
|
||||
currentUser := createUser("viewer", "Viewer", false)
|
||||
var rootFolderId int64 = 0
|
||||
|
||||
Convey("and one folder is expanded, the other collapsed", func() {
|
||||
Convey("should return dashboards in root and expanded folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 4)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, childDash1.Id)
|
||||
So(query.Result[3].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for one dashboard folder", func() {
|
||||
var otherUser int64 = 999
|
||||
updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() {
|
||||
movedDash := moveDashboard(1, childDash2.Data, folder1.Id)
|
||||
So(movedDash.HasAcl, ShouldBeTrue)
|
||||
|
||||
Convey("should not return folder with acl or its children", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() {
|
||||
movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
|
||||
So(movedDash.HasAcl, ShouldBeFalse)
|
||||
|
||||
Convey("should return folder without acl and its children", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 4)
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash1.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, childDash2.Id)
|
||||
So(query.Result[3].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and a dashboard with an acl is moved to the folder without an acl", func() {
|
||||
updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT)
|
||||
movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
|
||||
So(movedDash.HasAcl, ShouldBeTrue)
|
||||
|
||||
Convey("should return folder without acl but not the dashboard with acl", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 3)
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash2.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a plugin with imported dashboards", func() {
|
||||
pluginId := "test-app"
|
||||
|
||||
appFolder := insertTestDashboardForPlugin("app-test", 1, 0, true, pluginId)
|
||||
insertTestDashboardForPlugin("app-dash1", 1, appFolder.Id, false, pluginId)
|
||||
insertTestDashboardForPlugin("app-dash2", 1, appFolder.Id, false, pluginId)
|
||||
|
||||
Convey("Should return imported dashboard", func() {
|
||||
query := m.GetDashboardsByPluginIdQuery{
|
||||
PluginId: pluginId,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
err := GetDashboardsByPluginId(&query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func insertTestDashboard(title string, orgId int64, folderId int64, isFolder bool, tags ...interface{}) *m.Dashboard {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: orgId,
|
||||
FolderId: folderId,
|
||||
IsFolder: isFolder,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": title,
|
||||
"tags": tags,
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return cmd.Result
|
||||
}
|
||||
|
||||
func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isFolder bool, pluginId string) *m.Dashboard {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: orgId,
|
||||
FolderId: folderId,
|
||||
IsFolder: isFolder,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": title,
|
||||
}),
|
||||
PluginId: pluginId,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return cmd.Result
|
||||
}
|
||||
|
||||
func createUser(name string, role string, isAdmin bool) m.User {
|
||||
setting.AutoAssignOrg = true
|
||||
setting.AutoAssignOrgRole = role
|
||||
|
||||
currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
|
||||
err := CreateUser(¤tUserCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
q1 := m.GetUserOrgListQuery{UserId: currentUserCmd.Result.Id}
|
||||
GetUserOrgList(&q1)
|
||||
So(q1.Result[0].Role, ShouldEqual, role)
|
||||
|
||||
return currentUserCmd.Result
|
||||
}
|
||||
|
||||
func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.PermissionType) int64 {
|
||||
cmd := &m.SetDashboardAclCommand{
|
||||
OrgId: 1,
|
||||
UserId: userId,
|
||||
DashboardId: dashId,
|
||||
Permission: permissions,
|
||||
}
|
||||
|
||||
err := SetDashboardAcl(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return cmd.Result.Id
|
||||
}
|
||||
|
||||
func removeAcl(aclId int64) {
|
||||
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{AclId: aclId, OrgId: 1})
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
func moveDashboard(orgId int64, dashboard *simplejson.Json, newFolderId int64) *m.Dashboard {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: orgId,
|
||||
FolderId: newFolderId,
|
||||
Dashboard: dashboard,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return cmd.Result
|
||||
}
|
||||
|
||||
@@ -36,6 +36,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,
|
||||
|
||||
@@ -29,7 +29,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,
|
||||
@@ -70,7 +70,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}
|
||||
@@ -110,7 +110,7 @@ func TestDeleteExpiredVersions(t *testing.T) {
|
||||
versionsToWrite := 10
|
||||
setting.DashboardVersionsToKeep = versionsToKeep
|
||||
|
||||
savedDash := insertTestDashboard("test dash 53", 1, "diff-all")
|
||||
savedDash := insertTestDashboard("test dash 53", 1, 0, false, "diff-all")
|
||||
for i := 0; i < versionsToWrite-1; i++ {
|
||||
updateTestDashboard(savedDash, map[string]interface{}{
|
||||
"tags": "different-tag",
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
)
|
||||
|
||||
func InitTestDB(t *testing.T) {
|
||||
func InitTestDB(t *testing.T) *xorm.Engine {
|
||||
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)
|
||||
@@ -27,6 +27,8 @@ func InitTestDB(t *testing.T) {
|
||||
if err := SetEngine(x); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
type Test struct {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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: "team_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", "team_id"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create dashboard acl table", NewAddTableMigration(dashboardAclV1))
|
||||
|
||||
//------- indexes ------------------
|
||||
mg.AddMigration("add 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_team_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: false, Default: "0",
|
||||
}))
|
||||
|
||||
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)
|
||||
addTeamMigrations(mg)
|
||||
addDashboardAclMigrations(mg)
|
||||
addTagMigration(mg)
|
||||
}
|
||||
|
||||
|
||||
@@ -83,4 +83,10 @@ func addOrgMigrations(mg *Migrator) {
|
||||
mg.AddMigration("Update org_user table charset", NewTableCharsetMigration("org_user", []*Column{
|
||||
{Name: "role", Type: DB_NVarchar, Length: 20},
|
||||
}))
|
||||
|
||||
const migrateReadOnlyViewersToViewers = `UPDATE org_user SET role = 'Viewer' WHERE role = 'Read Only Editor'`
|
||||
mg.AddMigration("Migrate all Read Only Viewers to Viewers", new(RawSqlMigration).
|
||||
Sqlite(migrateReadOnlyViewersToViewers).
|
||||
Postgres(migrateReadOnlyViewersToViewers).
|
||||
Mysql(migrateReadOnlyViewersToViewers))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addTeamMigrations(mg *Migrator) {
|
||||
teamV1 := Table{
|
||||
Name: "team",
|
||||
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 team table", NewAddTableMigration(teamV1))
|
||||
|
||||
//------- indexes ------------------
|
||||
mg.AddMigration("add index team.org_id", NewAddIndexMigration(teamV1, teamV1.Indices[0]))
|
||||
mg.AddMigration("add unique index team_org_id_name", NewAddIndexMigration(teamV1, teamV1.Indices[1]))
|
||||
|
||||
teamMemberV1 := Table{
|
||||
Name: "team_member",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt},
|
||||
{Name: "team_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", "team_id", "user_id"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create team member table", NewAddTableMigration(teamMemberV1))
|
||||
|
||||
//------- indexes ------------------
|
||||
mg.AddMigration("add index team_member.org_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[0]))
|
||||
mg.AddMigration("add unique index team_member_org_id_team_id_user_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.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)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -88,10 +88,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 team_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)
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// SearchBuilder is a builder/object mother that builds a dashboard search query
|
||||
type SearchBuilder struct {
|
||||
tags []string
|
||||
isStarred bool
|
||||
limit int
|
||||
signedInUser *m.SignedInUser
|
||||
whereDashboardIdsIn []int64
|
||||
whereTitle string
|
||||
whereTypeFolder bool
|
||||
whereTypeDash bool
|
||||
whereFolderIds []int64
|
||||
sql bytes.Buffer
|
||||
params []interface{}
|
||||
}
|
||||
|
||||
func NewSearchBuilder(signedInUser *m.SignedInUser, limit int) *SearchBuilder {
|
||||
searchBuilder := &SearchBuilder{
|
||||
signedInUser: signedInUser,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
return searchBuilder
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) WithTags(tags []string) *SearchBuilder {
|
||||
if len(tags) > 0 {
|
||||
sb.tags = tags
|
||||
}
|
||||
|
||||
return sb
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) IsStarred() *SearchBuilder {
|
||||
sb.isStarred = true
|
||||
|
||||
return sb
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) WithDashboardIdsIn(ids []int64) *SearchBuilder {
|
||||
if len(ids) > 0 {
|
||||
sb.whereDashboardIdsIn = ids
|
||||
}
|
||||
|
||||
return sb
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) WithTitle(title string) *SearchBuilder {
|
||||
sb.whereTitle = title
|
||||
|
||||
return sb
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) WithType(queryType string) *SearchBuilder {
|
||||
if len(queryType) > 0 && queryType == "dash-folder" {
|
||||
sb.whereTypeFolder = true
|
||||
}
|
||||
|
||||
if len(queryType) > 0 && queryType == "dash-db" {
|
||||
sb.whereTypeDash = true
|
||||
}
|
||||
|
||||
return sb
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) WithFolderIds(folderIds []int64) *SearchBuilder {
|
||||
sb.whereFolderIds = folderIds
|
||||
return sb
|
||||
}
|
||||
|
||||
// ToSql builds the sql and returns it as a string, together with the params.
|
||||
func (sb *SearchBuilder) ToSql() (string, []interface{}) {
|
||||
sb.params = make([]interface{}, 0)
|
||||
|
||||
sb.buildSelect()
|
||||
|
||||
if len(sb.tags) > 0 {
|
||||
sb.buildTagQuery()
|
||||
} else {
|
||||
sb.buildMainQuery()
|
||||
}
|
||||
|
||||
sb.sql.WriteString(`
|
||||
LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id
|
||||
LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`)
|
||||
|
||||
sb.sql.WriteString(" ORDER BY dashboard.title ASC LIMIT 5000")
|
||||
|
||||
return sb.sql.String(), sb.params
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) buildSelect() {
|
||||
sb.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 `)
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) buildTagQuery() {
|
||||
sb.sql.WriteString(
|
||||
`(
|
||||
SELECT
|
||||
dashboard.id FROM dashboard
|
||||
LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id
|
||||
`)
|
||||
|
||||
if sb.isStarred {
|
||||
sb.sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
|
||||
}
|
||||
|
||||
sb.sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(sb.tags)-1) + `) AND `)
|
||||
for _, tag := range sb.tags {
|
||||
sb.params = append(sb.params, tag)
|
||||
}
|
||||
|
||||
sb.buildSearchWhereClause()
|
||||
|
||||
// this ends the inner select (tag filtered part)
|
||||
sb.sql.WriteString(`
|
||||
GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
|
||||
LIMIT ?) as ids
|
||||
INNER JOIN dashboard on ids.id = dashboard.id
|
||||
`)
|
||||
|
||||
sb.params = append(sb.params, len(sb.tags))
|
||||
sb.params = append(sb.params, sb.limit)
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) buildMainQuery() {
|
||||
sb.sql.WriteString(`( SELECT dashboard.id FROM dashboard `)
|
||||
|
||||
if sb.isStarred {
|
||||
sb.sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
|
||||
}
|
||||
|
||||
sb.sql.WriteString(` WHERE `)
|
||||
sb.buildSearchWhereClause()
|
||||
|
||||
sb.sql.WriteString(`
|
||||
LIMIT ?) as ids
|
||||
INNER JOIN dashboard on ids.id = dashboard.id
|
||||
`)
|
||||
sb.params = append(sb.params, sb.limit)
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) buildSearchWhereClause() {
|
||||
sb.sql.WriteString(` dashboard.org_id=?`)
|
||||
sb.params = append(sb.params, sb.signedInUser.OrgId)
|
||||
|
||||
if sb.isStarred {
|
||||
sb.sql.WriteString(` AND star.user_id=?`)
|
||||
sb.params = append(sb.params, sb.signedInUser.UserId)
|
||||
}
|
||||
|
||||
if len(sb.whereDashboardIdsIn) > 0 {
|
||||
sb.sql.WriteString(` AND dashboard.id IN (?` + strings.Repeat(",?", len(sb.whereDashboardIdsIn)-1) + `)`)
|
||||
for _, dashboardId := range sb.whereDashboardIdsIn {
|
||||
sb.params = append(sb.params, dashboardId)
|
||||
}
|
||||
}
|
||||
|
||||
if sb.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 team_member as ugm on ugm.team_id = da.team_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 = ?
|
||||
)
|
||||
)`
|
||||
|
||||
sb.sql.WriteString(allowedDashboardsSubQuery)
|
||||
sb.params = append(sb.params, sb.signedInUser.UserId, sb.signedInUser.UserId, sb.signedInUser.OrgId)
|
||||
}
|
||||
|
||||
if len(sb.whereTitle) > 0 {
|
||||
sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?")
|
||||
sb.params = append(sb.params, "%"+sb.whereTitle+"%")
|
||||
}
|
||||
|
||||
if sb.whereTypeFolder {
|
||||
sb.sql.WriteString(" AND dashboard.is_folder = 1")
|
||||
}
|
||||
|
||||
if sb.whereTypeDash {
|
||||
sb.sql.WriteString(" AND dashboard.is_folder = 0")
|
||||
}
|
||||
|
||||
if len(sb.whereFolderIds) > 0 {
|
||||
sb.sql.WriteString(` AND dashboard.folder_id IN (?` + strings.Repeat(",?", len(sb.whereFolderIds)-1) + `) `)
|
||||
for _, id := range sb.whereFolderIds {
|
||||
sb.params = append(sb.params, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSearchBuilder(t *testing.T) {
|
||||
dialect = migrator.NewDialect("sqlite3")
|
||||
|
||||
Convey("Testing building a search", t, func() {
|
||||
signedInUser := &m.SignedInUser{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
}
|
||||
sb := NewSearchBuilder(signedInUser, 1000)
|
||||
|
||||
Convey("When building a normal search", func() {
|
||||
sql, params := sb.IsStarred().WithTitle("test").ToSql()
|
||||
So(sql, ShouldStartWith, "SELECT")
|
||||
So(sql, ShouldContainSubstring, "INNER JOIN dashboard on ids.id = dashboard.id")
|
||||
So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000")
|
||||
So(len(params), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
Convey("When building a search with tag filter", func() {
|
||||
sql, params := sb.WithTags([]string{"tag1", "tag2"}).ToSql()
|
||||
So(sql, ShouldStartWith, "SELECT")
|
||||
So(sql, ShouldContainSubstring, "LEFT OUTER JOIN dashboard_tag")
|
||||
So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000")
|
||||
So(len(params), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", CreateTeam)
|
||||
bus.AddHandler("sql", UpdateTeam)
|
||||
bus.AddHandler("sql", DeleteTeam)
|
||||
bus.AddHandler("sql", SearchTeams)
|
||||
bus.AddHandler("sql", GetTeamById)
|
||||
bus.AddHandler("sql", GetTeamsByUser)
|
||||
|
||||
bus.AddHandler("sql", AddTeamMember)
|
||||
bus.AddHandler("sql", RemoveTeamMember)
|
||||
bus.AddHandler("sql", GetTeamMembers)
|
||||
}
|
||||
|
||||
func CreateTeam(cmd *m.CreateTeamCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
|
||||
if isNameTaken, err := isTeamNameTaken(cmd.Name, 0, sess); err != nil {
|
||||
return err
|
||||
} else if isNameTaken {
|
||||
return m.ErrTeamNameTaken
|
||||
}
|
||||
|
||||
team := m.Team{
|
||||
Name: cmd.Name,
|
||||
OrgId: cmd.OrgId,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
_, err := sess.Insert(&team)
|
||||
|
||||
cmd.Result = team
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateTeam(cmd *m.UpdateTeamCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
|
||||
if isNameTaken, err := isTeamNameTaken(cmd.Name, cmd.Id, sess); err != nil {
|
||||
return err
|
||||
} else if isNameTaken {
|
||||
return m.ErrTeamNameTaken
|
||||
}
|
||||
|
||||
team := m.Team{
|
||||
Name: cmd.Name,
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
affectedRows, err := sess.Id(cmd.Id).Update(&team)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if affectedRows == 0 {
|
||||
return m.ErrTeamNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteTeam(cmd *m.DeleteTeamCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.Id); err != nil {
|
||||
return err
|
||||
} else if len(res) != 1 {
|
||||
return m.ErrTeamNotFound
|
||||
}
|
||||
|
||||
deletes := []string{
|
||||
"DELETE FROM team_member WHERE team_id = ?",
|
||||
"DELETE FROM team WHERE id = ?",
|
||||
"DELETE FROM dashboard_acl WHERE team_id = ?",
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
_, err := sess.Exec(sql, cmd.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func isTeamNameTaken(name string, existingId int64, sess *DBSession) (bool, error) {
|
||||
var team m.Team
|
||||
exists, err := sess.Where("name=?", name).Get(&team)
|
||||
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if exists && existingId != team.Id {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func SearchTeams(query *m.SearchTeamsQuery) error {
|
||||
query.Result = m.SearchTeamQueryResult{
|
||||
Teams: make([]*m.SearchTeamDto, 0),
|
||||
}
|
||||
queryWithWildcards := "%" + query.Query + "%"
|
||||
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
sql.WriteString(`select
|
||||
team.id as id,
|
||||
team.name as name,
|
||||
(select count(*) from team_member where team_member.team_id = team.id) as member_count
|
||||
from team as team
|
||||
where team.org_id = ?`)
|
||||
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if query.Query != "" {
|
||||
sql.WriteString(` and team.name ` + dialect.LikeStr() + ` ?`)
|
||||
params = append(params, queryWithWildcards)
|
||||
}
|
||||
|
||||
if query.Name != "" {
|
||||
sql.WriteString(` and team.name = ?`)
|
||||
params = append(params, query.Name)
|
||||
}
|
||||
|
||||
sql.WriteString(` order by team.name asc`)
|
||||
|
||||
if query.Limit != 0 {
|
||||
sql.WriteString(` limit ? offset ?`)
|
||||
offset := query.Limit * (query.Page - 1)
|
||||
params = append(params, query.Limit, offset)
|
||||
}
|
||||
|
||||
if err := x.Sql(sql.String(), params...).Find(&query.Result.Teams); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
team := m.Team{}
|
||||
countSess := x.Table("team")
|
||||
if query.Query != "" {
|
||||
countSess.Where(`name `+dialect.LikeStr()+` ?`, queryWithWildcards)
|
||||
}
|
||||
|
||||
if query.Name != "" {
|
||||
countSess.Where("name=?", query.Name)
|
||||
}
|
||||
|
||||
count, err := countSess.Count(&team)
|
||||
query.Result.TotalCount = count
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetTeamById(query *m.GetTeamByIdQuery) error {
|
||||
var team m.Team
|
||||
exists, err := x.Id(query.Id).Get(&team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return m.ErrTeamNotFound
|
||||
}
|
||||
|
||||
query.Result = &team
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = make([]*m.Team, 0)
|
||||
|
||||
sess := x.Table("team")
|
||||
sess.Join("INNER", "team_member", "team.id=team_member.team_id")
|
||||
sess.Where("team_member.user_id=?", query.UserId)
|
||||
|
||||
err := sess.Find(&query.Result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
if res, err := sess.Query("SELECT 1 from team_member WHERE team_id=? and user_id=?", cmd.TeamId, cmd.UserId); err != nil {
|
||||
return err
|
||||
} else if len(res) == 1 {
|
||||
return m.ErrTeamMemberAlreadyAdded
|
||||
}
|
||||
|
||||
if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.TeamId); err != nil {
|
||||
return err
|
||||
} else if len(res) != 1 {
|
||||
return m.ErrTeamNotFound
|
||||
}
|
||||
|
||||
entity := m.TeamMember{
|
||||
OrgId: cmd.OrgId,
|
||||
TeamId: cmd.TeamId,
|
||||
UserId: cmd.UserId,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
_, err := sess.Insert(&entity)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
var rawSql = "DELETE FROM team_member WHERE team_id=? and user_id=?"
|
||||
_, err := sess.Exec(rawSql, cmd.TeamId, cmd.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func GetTeamMembers(query *m.GetTeamMembersQuery) error {
|
||||
query.Result = make([]*m.TeamMemberDTO, 0)
|
||||
sess := x.Table("team_member")
|
||||
sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
sess.Where("team_member.team_id=?", query.TeamId)
|
||||
sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login")
|
||||
sess.Asc("user.login", "user.email")
|
||||
|
||||
err := sess.Find(&query.Result)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
|
||||
Convey("Testing Team commands & queries", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
Convey("Given saved users and two teams", 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.CreateTeamCommand{Name: "group1 name"}
|
||||
group2 := m.CreateTeamCommand{Name: "group2 name"}
|
||||
|
||||
err := CreateTeam(&group1)
|
||||
So(err, ShouldBeNil)
|
||||
err = CreateTeam(&group2)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should be able to create teams and add users", func() {
|
||||
query := &m.SearchTeamsQuery{Name: "group1 name", Page: 1, Limit: 10}
|
||||
err = SearchTeams(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Page, ShouldEqual, 1)
|
||||
|
||||
team1 := query.Result.Teams[0]
|
||||
So(team1.Name, ShouldEqual, "group1 name")
|
||||
|
||||
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: team1.Id, UserId: userIds[0]})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
q1 := &m.GetTeamMembersQuery{TeamId: team1.Id}
|
||||
err = GetTeamMembers(q1)
|
||||
So(err, ShouldBeNil)
|
||||
So(q1.Result[0].TeamId, ShouldEqual, team1.Id)
|
||||
So(q1.Result[0].Login, ShouldEqual, "loginuser0")
|
||||
})
|
||||
|
||||
Convey("Should be able to search for teams", func() {
|
||||
query := &m.SearchTeamsQuery{Query: "group", Page: 1}
|
||||
err = SearchTeams(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Teams), ShouldEqual, 2)
|
||||
So(query.Result.TotalCount, ShouldEqual, 2)
|
||||
|
||||
query2 := &m.SearchTeamsQuery{Query: ""}
|
||||
err = SearchTeams(query2)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query2.Result.Teams), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Should be able to return all teams a user is member of", func() {
|
||||
groupId := group2.Result.Id
|
||||
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[0]})
|
||||
|
||||
query := &m.GetTeamsByUserQuery{UserId: userIds[0]}
|
||||
err = GetTeamsByUser(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 = RemoveTeamMember(&m.RemoveTeamMemberCommand{TeamId: group1.Result.Id, UserId: userIds[0]})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id}
|
||||
err = GetTeamMembers(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 := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[1]})
|
||||
So(err, ShouldBeNil)
|
||||
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[2]})
|
||||
So(err, ShouldBeNil)
|
||||
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permission: m.PERMISSION_EDIT, TeamId: groupId})
|
||||
|
||||
err = DeleteTeam(&m.DeleteTeamCommand{Id: groupId})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := &m.GetTeamByIdQuery{Id: groupId}
|
||||
err = GetTeamById(query)
|
||||
So(err, ShouldEqual, m.ErrTeamNotFound)
|
||||
|
||||
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1}
|
||||
err = GetDashboardAclInfoList(permQuery)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(permQuery.Result), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -350,6 +350,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
||||
u.name as name,
|
||||
u.help_flags1 as help_flags1,
|
||||
u.last_seen_at as last_seen_at,
|
||||
(SELECT COUNT(*) FROM org_user where org_user.user_id = u.id) as org_count,
|
||||
org.name as org_name,
|
||||
org_user.role as org_role,
|
||||
org.id as org_id
|
||||
@@ -438,6 +439,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 team_member WHERE user_id = ?",
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
|
||||
@@ -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("Can return list of users matching query on login name", func() {
|
||||
query := models.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
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)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 1)
|
||||
So(query.Result.TotalCount, ShouldEqual, 1)
|
||||
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user