Commit f08932b7 by Marcus Efraimsson

WIP: folder api. #10630

parent ee57ed10
...@@ -240,6 +240,15 @@ func (hs *HttpServer) registerRoutes() { ...@@ -240,6 +240,15 @@ func (hs *HttpServer) registerRoutes() {
apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest) apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest) apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
// Folders
apiRoute.Group("/folders", func(folderRoute RouteRegister) {
folderRoute.Get("/", wrap(GetFolders))
folderRoute.Get("/:id", wrap(GetFolderById))
folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
folderRoute.Put("/:id", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
folderRoute.Delete("/:id", wrap(DeleteFolder))
})
// Dashboard // Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
...@@ -252,8 +261,6 @@ func (hs *HttpServer) registerRoutes() { ...@@ -252,8 +261,6 @@ func (hs *HttpServer) registerRoutes() {
dashboardRoute.Get("/tags", GetDashboardTags) dashboardRoute.Get("/tags", GetDashboardTags)
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
dashboardRoute.Get("/folders", wrap(GetFoldersForSignedInUser))
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) { dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
dashIdRoute.Get("/versions", wrap(GetDashboardVersions)) dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion)) dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
......
...@@ -130,6 +130,11 @@ func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Respo ...@@ -130,6 +130,11 @@ func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Respo
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return nil, ApiError(404, "Dashboard not found", err) return nil, ApiError(404, "Dashboard not found", err)
} }
if query.Result.IsFolder {
return nil, ApiError(404, "Dashboard not found", m.ErrDashboardNotFound)
}
return query.Result, nil return query.Result, nil
} }
...@@ -164,6 +169,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { ...@@ -164,6 +169,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
// if new dashboard, use parent folder permissions instead // if new dashboard, use parent folder permissions instead
if dashId == 0 { if dashId == 0 {
dashId = cmd.FolderId dashId = cmd.FolderId
} else {
_, rsp := getDashboardHelper(c.OrgId, "", dashId)
if rsp != nil {
return rsp
}
} }
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
...@@ -439,19 +449,3 @@ func GetDashboardTags(c *middleware.Context) { ...@@ -439,19 +449,3 @@ func GetDashboardTags(c *middleware.Context) {
c.JSON(200, query.Result) c.JSON(200, query.Result)
} }
func GetFoldersForSignedInUser(c *middleware.Context) Response {
title := c.Query("query")
query := m.GetFoldersForSignedInUserQuery{
OrgId: c.OrgId,
SignedInUser: c.SignedInUser,
Title: title,
}
err := bus.Dispatch(&query)
if err != nil {
return ApiError(500, "Failed to get folders from database", err)
}
return Json(200, query.Result)
}
...@@ -33,6 +33,44 @@ func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) ...@@ -33,6 +33,44 @@ func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem)
var fakeRepo *fakeDashboardRepo var fakeRepo *fakeDashboardRepo
func TestDashboardApiEndpoint(t *testing.T) { func TestDashboardApiEndpoint(t *testing.T) {
Convey("Given a folder", t, func() {
fakeFolder := m.NewDashboardFolder("Folder")
fakeFolder.Id = 1
fakeFolder.HasAcl = false
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeFolder
return nil
})
cmd := m.SaveDashboardCommand{
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": fakeFolder.Title,
"id": fakeFolder.Id,
}),
IsFolder: true,
}
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/1", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallGetDashboard(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/1", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
})
})
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() { Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
fakeDash := m.NewDashboard("Child dash") fakeDash := m.NewDashboard("Child dash")
fakeDash.Id = 1 fakeDash.Id = 1
...@@ -426,8 +464,7 @@ func TestDashboardApiEndpoint(t *testing.T) { ...@@ -426,8 +464,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
} }
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
sc.handlerFunc = GetDashboard CallGetDashboard(sc)
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200) So(sc.resp.Code, ShouldEqual, 200)
...@@ -438,6 +475,11 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta ...@@ -438,6 +475,11 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta
return dash return dash
} }
func CallGetDashboard(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func CallGetDashboardVersion(sc *scenarioContext) { func CallGetDashboardVersion(sc *scenarioContext) {
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error { bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{} query.Result = &m.DashboardVersion{}
......
package dtos
import "time"
type Folder struct {
Id int64 `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
HasAcl bool `json:"hasAcl"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CreatedBy string `json:"createdBy"`
Created time.Time `json:"created"`
UpdatedBy string `json:"updatedBy"`
Updated time.Time `json:"updated"`
Version int `json:"version"`
}
package api
import (
"fmt"
"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/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
)
func getFolderHelper(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 {
if err == m.ErrDashboardNotFound {
err = m.ErrFolderNotFound
}
return nil, ApiError(404, "Folder not found", err)
}
if !query.Result.IsFolder {
return nil, ApiError(404, "Folder not found", m.ErrFolderNotFound)
}
return query.Result, nil
}
func folderGuardianResponse(err error) Response {
if err != nil {
return ApiError(500, "Error while checking folder permissions", err)
}
return ApiError(403, "Access denied to this folder", nil)
}
func GetFolders(c *middleware.Context) Response {
title := c.Query("query")
query := m.GetFoldersQuery{
OrgId: c.OrgId,
SignedInUser: c.SignedInUser,
Title: title,
}
err := bus.Dispatch(&query)
if err != nil {
return ApiError(500, "Failed to retrieve folders", err)
}
return Json(200, query.Result)
}
func GetFolderById(c *middleware.Context) Response {
folder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id"))
if rsp != nil {
return rsp
}
guardian := guardian.NewDashboardGuardian(folder.Id, c.OrgId, c.SignedInUser)
if canView, err := guardian.CanView(); err != nil || !canView {
fmt.Printf("%v", err)
return folderGuardianResponse(err)
}
return Json(200, toDto(guardian, folder))
}
func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response {
cmd.OrgId = c.OrgId
cmd.UserId = c.UserId
dashFolder := m.NewDashboardFolder(cmd.Title)
guardian := guardian.NewDashboardGuardian(0, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return folderGuardianResponse(err)
}
// Check if Title is empty
if dashFolder.Title == "" {
return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil)
}
limitReached, err := middleware.QuotaReached(c, "folder")
if err != nil {
return ApiError(500, "failed to get quota", err)
}
if limitReached {
return ApiError(403, "Quota reached", nil)
}
dashFolder.CreatedBy = c.UserId
dashFolder.UpdatedBy = c.UserId
dashItem := &dashboards.SaveDashboardItem{
Dashboard: dashFolder,
OrgId: c.OrgId,
UserId: c.UserId,
}
folder, err := dashboards.GetRepository().SaveDashboard(dashItem)
if err != nil {
return toFolderError(err)
}
return Json(200, toDto(guardian, folder))
}
func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response {
cmd.OrgId = c.OrgId
cmd.UserId = c.UserId
dashFolder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id"))
if rsp != nil {
return rsp
}
guardian := guardian.NewDashboardGuardian(dashFolder.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return folderGuardianResponse(err)
}
dashFolder.Data.Set("title", cmd.Title)
dashFolder.Title = cmd.Title
dashFolder.Data.Set("version", cmd.Version)
dashFolder.Version = cmd.Version
dashFolder.UpdatedBy = c.UserId
// Check if Title is empty
if dashFolder.Title == "" {
return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil)
}
dashItem := &dashboards.SaveDashboardItem{
Dashboard: dashFolder,
OrgId: c.OrgId,
UserId: c.UserId,
}
folder, err := dashboards.GetRepository().SaveDashboard(dashItem)
if err != nil {
return toFolderError(err)
}
return Json(200, toDto(guardian, folder))
}
func DeleteFolder(c *middleware.Context) Response {
dashFolder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id"))
if rsp != nil {
return rsp
}
guardian := guardian.NewDashboardGuardian(dashFolder.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return folderGuardianResponse(err)
}
deleteCmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dashFolder.Id}
if err := bus.Dispatch(&deleteCmd); err != nil {
return ApiError(500, "Failed to delete folder", err)
}
var resp = map[string]interface{}{"title": dashFolder.Title}
return Json(200, resp)
}
func toDto(guardian *guardian.DashboardGuardian, folder *m.Dashboard) dtos.Folder {
canEdit, _ := guardian.CanEdit()
canSave, _ := guardian.CanSave()
canAdmin, _ := guardian.CanAdmin()
// Finding creator and last updater of the folder
updater, creator := "Anonymous", "Anonymous"
if folder.UpdatedBy > 0 {
updater = getUserLogin(folder.UpdatedBy)
}
if folder.CreatedBy > 0 {
creator = getUserLogin(folder.CreatedBy)
}
return dtos.Folder{
Id: folder.Id,
Title: folder.Title,
Slug: folder.Slug,
HasAcl: folder.HasAcl,
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CreatedBy: creator,
Created: folder.Created,
UpdatedBy: updater,
Updated: folder.Updated,
Version: folder.Version,
}
}
func toFolderError(err error) Response {
if err == m.ErrDashboardTitleEmpty {
return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil)
}
if err == m.ErrDashboardWithSameNameExists {
return Json(412, util.DynMap{"status": "name-exists", "message": m.ErrFolderWithSameNameExists.Error()})
}
if err == m.ErrDashboardVersionMismatch {
return Json(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()})
}
if err == m.ErrDashboardNotFound {
return Json(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()})
}
return ApiError(500, "Failed to create folder", err)
}
package api
import (
"encoding/json"
"path/filepath"
"testing"
"github.com/go-macaron/session"
"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/dashboards"
macaron "gopkg.in/macaron.v1"
. "github.com/smartystreets/goconvey/convey"
)
func TestFoldersApiEndpoint(t *testing.T) {
Convey("Given a dashboard", 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
})
updateFolderCmd := m.UpdateFolderCommand{}
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
callGetFolder(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
updateFolderScenario("When calling PUT on", "/api/folders/1", "/api/folders/:id", role, updateFolderCmd, func(sc *scenarioContext) {
callUpdateFolder(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
callDeleteFolder(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
})
})
Convey("Given a folder which does not have an acl", t, func() {
fakeFolder := m.NewDashboardFolder("Folder")
fakeFolder.Id = 1
fakeFolder.HasAcl = false
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeFolder
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.CreateFolderCommand{
Title: fakeFolder.Title,
}
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
folder := getFolderShouldReturn200(sc)
Convey("Should not be able to edit or save folder", func() {
So(folder.CanEdit, ShouldBeFalse)
So(folder.CanSave, ShouldBeFalse)
So(folder.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
callDeleteFolder(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) {
callCreateFolder(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/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
folder := getFolderShouldReturn200(sc)
Convey("Should be able to edit or save folder", func() {
So(folder.CanEdit, ShouldBeTrue)
So(folder.CanSave, ShouldBeTrue)
So(folder.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
callDeleteFolder(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
Convey("Given a folder which have an acl", t, func() {
fakeFolder := m.NewDashboardFolder("Folder")
fakeFolder.Id = 1
fakeFolder.HasAcl = true
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeFolder
return nil
})
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.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
return nil
})
cmd := m.CreateFolderCommand{
Title: fakeFolder.Title,
}
Convey("When user is an Org Viewer and has no permissions for this folder", func() {
role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
sc.handlerFunc = GetFolderById
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/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
callDeleteFolder(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor and has no permissions for this folder", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
sc.handlerFunc = GetFolderById
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/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) {
callDeleteFolder(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
}
func getFolderShouldReturn200(sc *scenarioContext) dtos.Folder {
callGetFolder(sc)
So(sc.resp.Code, ShouldEqual, 200)
folder := dtos.Folder{}
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
So(err, ShouldBeNil)
return folder
}
func callGetFolder(sc *scenarioContext) {
sc.handlerFunc = GetFolderById
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func callDeleteFolder(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
return nil
})
sc.handlerFunc = DeleteFolder
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
func callCreateFolder(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
cmd.Result = &m.Dashboard{Id: 1, Slug: "folder", Version: 2}
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func callUpdateFolder(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
cmd.Result = &m.Dashboard{Id: 1, Slug: "folder", Version: 2}
return nil
})
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
}
func createFolderScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.CreateFolderCommand, 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 CreateFolder(c, cmd)
})
fakeRepo = &fakeDashboardRepo{}
dashboards.SetRepository(fakeRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func updateFolderScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.UpdateFolderCommand, 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 UpdateFolder(c, cmd)
})
fakeRepo = &fakeDashboardRepo{}
dashboards.SetRepository(fakeRepo)
sc.m.Put(routePattern, sc.defaultHandler)
fn(sc)
})
}
...@@ -73,9 +73,9 @@ func NewDashboard(title string) *Dashboard { ...@@ -73,9 +73,9 @@ func NewDashboard(title string) *Dashboard {
// NewDashboardFolder creates a new dashboard folder // NewDashboardFolder creates a new dashboard folder
func NewDashboardFolder(title string) *Dashboard { func NewDashboardFolder(title string) *Dashboard {
folder := NewDashboard(title) folder := NewDashboard(title)
folder.IsFolder = true
folder.Data.Set("schemaVersion", 16) folder.Data.Set("schemaVersion", 16)
folder.Data.Set("editable", true) folder.Data.Set("version", 0)
folder.Data.Set("hideControls", true)
return folder return folder
} }
...@@ -209,15 +209,3 @@ type GetDashboardSlugByIdQuery struct { ...@@ -209,15 +209,3 @@ type GetDashboardSlugByIdQuery struct {
Id int64 Id int64
Result string Result string
} }
type GetFoldersForSignedInUserQuery struct {
OrgId int64
SignedInUser *SignedInUser
Title string
Result []*DashboardFolder
}
type DashboardFolder struct {
Id int64 `json:"id"`
Title string `json:"title"`
}
package models
import (
"errors"
"time"
)
// Typed errors
var (
ErrFolderNotFound = errors.New("Folder not found")
ErrFolderVersionMismatch = errors.New("The folder has been changed by someone else")
ErrFolderTitleEmpty = errors.New("Folder title cannot be empty")
ErrFolderWithSameNameExists = errors.New("A folder/dashboard with the same title already exists")
)
type Folder struct {
Id int64
Title string
Slug string
OrgId int64
Version int
Created time.Time
Updated time.Time
UpdatedBy int64
CreatedBy int64
HasAcl bool
}
type GetFoldersQueryHitResult struct {
Id int64 `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
}
//
// COMMANDS
//
type CreateFolderCommand struct {
OrgId int64 `json:"-"`
UserId int64 `json:"userId"`
Title string `json:"title"`
Result *Folder
}
type UpdateFolderCommand struct {
OrgId int64 `json:"-"`
UserId int64 `json:"userId"`
Title string `json:"title"`
Version int `json:"version"`
Result *Folder
}
type GetFoldersQuery struct {
OrgId int64
SignedInUser *SignedInUser
Title string
Result []*GetFoldersQueryHitResult
}
...@@ -50,6 +50,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -50,6 +50,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
if existing.PluginId != "" && cmd.Overwrite == false { if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId} return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
} }
dash.Created = existing.Created
dash.CreatedBy = existing.CreatedBy
} }
sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle) sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle)
...@@ -66,6 +69,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -66,6 +69,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
} else { } else {
return m.ErrDashboardWithSameNameExists return m.ErrDashboardWithSameNameExists
} }
} else {
dash.Created = sameTitle.Created
dash.CreatedBy = sameTitle.CreatedBy
} }
} }
...@@ -134,6 +140,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -134,6 +140,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
} }
} }
} }
cmd.Result = dash cmd.Result = dash
return err return err
...@@ -292,19 +299,26 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { ...@@ -292,19 +299,26 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
return err return err
} }
func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { func GetFoldersForSignedInUser(query *m.GetFoldersQuery) error {
query.Result = make([]*m.DashboardFolder, 0) query.Result = make([]*m.GetFoldersQueryHitResult, 0)
var err error var err error
params := make([]interface{}, 0)
if query.SignedInUser.OrgRole == m.ROLE_ADMIN { if query.SignedInUser.OrgRole == m.ROLE_ADMIN {
sql := `SELECT distinct d.id, d.title sql := `SELECT distinct d.id, d.title, d.slug
FROM dashboard AS d WHERE d.is_folder = ? FROM dashboard AS d WHERE d.is_folder = ?`
ORDER BY d.title ASC` params = append(params, dialect.BooleanStr(true))
if len(query.Title) > 0 {
sql += " AND d.title " + dialect.LikeStr() + " ?"
params = append(params, "%"+query.Title+"%")
}
err = x.Sql(sql, dialect.BooleanStr(true)).Find(&query.Result) sql += ` ORDER BY d.title ASC`
err = x.Sql(sql, params...).Find(&query.Result)
} else { } else {
params := make([]interface{}, 0) sql := `SELECT distinct d.id, d.title, d.slug
sql := `SELECT distinct d.id, d.title
FROM dashboard AS d FROM dashboard AS d
LEFT JOIN dashboard_acl AS da ON d.id = da.dashboard_id LEFT JOIN dashboard_acl AS da ON d.id = da.dashboard_id
LEFT JOIN team_member AS ugm ON ugm.team_id = da.team_id LEFT JOIN team_member AS ugm ON ugm.team_id = da.team_id
...@@ -315,14 +329,17 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { ...@@ -315,14 +329,17 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error {
sql += `WHERE sql += `WHERE
d.org_id = ? AND d.org_id = ? AND
d.is_folder = 1 AND d.is_folder = ? AND
( (
(d.has_acl = 1 AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL)) (d.has_acl = ? AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL))
OR (d.has_acl = 0 AND ouRole.id IS NOT NULL) OR (d.has_acl = 0 AND ouRole.id IS NOT NULL)
)` )`
params = append(params, query.OrgId) params = append(params, query.OrgId)
params = append(params, dialect.BooleanStr(true))
params = append(params, dialect.BooleanStr(true))
params = append(params, query.SignedInUser.UserId) params = append(params, query.SignedInUser.UserId)
params = append(params, query.SignedInUser.UserId) params = append(params, query.SignedInUser.UserId)
params = append(params, dialect.BooleanStr(false))
if len(query.Title) > 0 { if len(query.Title) > 0 {
sql += " AND d.title " + dialect.LikeStr() + " ?" sql += " AND d.title " + dialect.LikeStr() + " ?"
......
...@@ -470,7 +470,7 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -470,7 +470,7 @@ func TestDashboardDataAccess(t *testing.T) {
Convey("Admin users", func() { Convey("Admin users", func() {
Convey("Should have write access to all dashboard folders", func() { Convey("Should have write access to all dashboard folders", func() {
query := m.GetFoldersForSignedInUserQuery{ query := m.GetFoldersQuery{
OrgId: 1, OrgId: 1,
SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN}, SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN},
} }
...@@ -485,7 +485,7 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -485,7 +485,7 @@ func TestDashboardDataAccess(t *testing.T) {
}) })
Convey("Editor users", func() { Convey("Editor users", func() {
query := m.GetFoldersForSignedInUserQuery{ query := m.GetFoldersQuery{
OrgId: 1, OrgId: 1,
SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR}, SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR},
} }
...@@ -511,7 +511,7 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -511,7 +511,7 @@ func TestDashboardDataAccess(t *testing.T) {
}) })
Convey("Viewer users", func() { Convey("Viewer users", func() {
query := m.GetFoldersForSignedInUserQuery{ query := m.GetFoldersQuery{
OrgId: 1, OrgId: 1,
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER}, SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER},
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment