From f08932b78acf21103982f5270def7c1d2de80169 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 13:51:01 +0100 Subject: [PATCH 01/26] WIP: folder api. #10630 --- pkg/api/api.go | 11 +- pkg/api/dashboard.go | 26 +- pkg/api/dashboard_test.go | 46 +++- pkg/api/dtos/folder.go | 18 ++ pkg/api/folders.go | 221 ++++++++++++++++ pkg/api/folders_test.go | 328 ++++++++++++++++++++++++ pkg/models/dashboards.go | 16 +- pkg/models/folders.go | 63 +++++ pkg/services/sqlstore/dashboard.go | 37 ++- pkg/services/sqlstore/dashboard_test.go | 6 +- 10 files changed, 725 insertions(+), 47 deletions(-) create mode 100644 pkg/api/dtos/folder.go create mode 100644 pkg/api/folders.go create mode 100644 pkg/api/folders_test.go create mode 100644 pkg/models/folders.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 086d7345483..9a043b958b9 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -240,6 +240,15 @@ func (hs *HttpServer) registerRoutes() { 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 apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) @@ -252,8 +261,6 @@ func (hs *HttpServer) registerRoutes() { dashboardRoute.Get("/tags", GetDashboardTags) dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) - dashboardRoute.Get("/folders", wrap(GetFoldersForSignedInUser)) - dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) { dashIdRoute.Get("/versions", wrap(GetDashboardVersions)) dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index cf9f6b6bb8b..a8637dfc7e6 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -130,6 +130,11 @@ func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Respo if err := bus.Dispatch(&query); err != nil { 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 } @@ -164,6 +169,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { // if new dashboard, use parent folder permissions instead if dashId == 0 { dashId = cmd.FolderId + } else { + _, rsp := getDashboardHelper(c.OrgId, "", dashId) + if rsp != nil { + return rsp + } } guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) @@ -439,19 +449,3 @@ func GetDashboardTags(c *middleware.Context) { 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) -} diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e6228878625..af64767c713 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -33,6 +33,44 @@ func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) var fakeRepo *fakeDashboardRepo 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() { fakeDash := m.NewDashboard("Child dash") fakeDash.Id = 1 @@ -426,8 +464,7 @@ func TestDashboardApiEndpoint(t *testing.T) { } func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { - sc.handlerFunc = GetDashboard - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + CallGetDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -438,6 +475,11 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta return dash } +func CallGetDashboard(sc *scenarioContext) { + sc.handlerFunc = GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() +} + func CallGetDashboardVersion(sc *scenarioContext) { bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error { query.Result = &m.DashboardVersion{} diff --git a/pkg/api/dtos/folder.go b/pkg/api/dtos/folder.go new file mode 100644 index 00000000000..7ce7825fb6a --- /dev/null +++ b/pkg/api/dtos/folder.go @@ -0,0 +1,18 @@ +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"` +} diff --git a/pkg/api/folders.go b/pkg/api/folders.go new file mode 100644 index 00000000000..22fffc15b91 --- /dev/null +++ b/pkg/api/folders.go @@ -0,0 +1,221 @@ +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) +} diff --git a/pkg/api/folders_test.go b/pkg/api/folders_test.go new file mode 100644 index 00000000000..cbe7eb7a280 --- /dev/null +++ b/pkg/api/folders_test.go @@ -0,0 +1,328 @@ +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) + }) +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index dccd93707a5..e7b0c3a5f23 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -73,9 +73,9 @@ func NewDashboard(title string) *Dashboard { // NewDashboardFolder creates a new dashboard folder func NewDashboardFolder(title string) *Dashboard { folder := NewDashboard(title) + folder.IsFolder = true folder.Data.Set("schemaVersion", 16) - folder.Data.Set("editable", true) - folder.Data.Set("hideControls", true) + folder.Data.Set("version", 0) return folder } @@ -209,15 +209,3 @@ type GetDashboardSlugByIdQuery struct { Id int64 Result string } - -type GetFoldersForSignedInUserQuery struct { - OrgId int64 - SignedInUser *SignedInUser - Title string - Result []*DashboardFolder -} - -type DashboardFolder struct { - Id int64 `json:"id"` - Title string `json:"title"` -} diff --git a/pkg/models/folders.go b/pkg/models/folders.go new file mode 100644 index 00000000000..4b3526b00d8 --- /dev/null +++ b/pkg/models/folders.go @@ -0,0 +1,63 @@ +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 +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index f4cdab22e89..afa522036c4 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -50,6 +50,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { if existing.PluginId != "" && cmd.Overwrite == false { 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) @@ -66,6 +69,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } else { return m.ErrDashboardWithSameNameExists } + } else { + dash.Created = sameTitle.Created + dash.CreatedBy = sameTitle.CreatedBy } } @@ -134,6 +140,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } } } + cmd.Result = dash return err @@ -292,19 +299,26 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { return err } -func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { - query.Result = make([]*m.DashboardFolder, 0) +func GetFoldersForSignedInUser(query *m.GetFoldersQuery) error { + query.Result = make([]*m.GetFoldersQueryHitResult, 0) var err error + params := make([]interface{}, 0) if query.SignedInUser.OrgRole == m.ROLE_ADMIN { - sql := `SELECT distinct d.id, d.title - FROM dashboard AS d WHERE d.is_folder = ? - ORDER BY d.title ASC` + sql := `SELECT distinct d.id, d.title, d.slug + FROM dashboard AS d WHERE d.is_folder = ?` + params = append(params, dialect.BooleanStr(true)) - err = x.Sql(sql, dialect.BooleanStr(true)).Find(&query.Result) + if len(query.Title) > 0 { + sql += " AND d.title " + dialect.LikeStr() + " ?" + params = append(params, "%"+query.Title+"%") + } + + sql += ` ORDER BY d.title ASC` + + err = x.Sql(sql, params...).Find(&query.Result) } else { - params := make([]interface{}, 0) - sql := `SELECT distinct d.id, d.title + sql := `SELECT distinct d.id, d.title, d.slug FROM dashboard AS d 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 @@ -315,14 +329,17 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { sql += `WHERE 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) )` 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, dialect.BooleanStr(false)) if len(query.Title) > 0 { sql += " AND d.title " + dialect.LikeStr() + " ?" diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 3b1e05d3772..b9c5451477e 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -470,7 +470,7 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Admin users", func() { Convey("Should have write access to all dashboard folders", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := m.GetFoldersQuery{ OrgId: 1, SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN}, } @@ -485,7 +485,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Editor users", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := m.GetFoldersQuery{ OrgId: 1, SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR}, } @@ -511,7 +511,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Viewer users", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := m.GetFoldersQuery{ OrgId: 1, SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER}, } From 4e0542d740ca89a75342ffe2d8daf90ab012081d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 18:41:04 +0100 Subject: [PATCH 02/26] dashboards: revert logic of returning 404 in dashboard api if it's a folder for now --- pkg/api/dashboard.go | 9 ----- pkg/api/dashboard_test.go | 73 --------------------------------------- 2 files changed, 82 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 016253cd22a..8fcc6eb5902 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -138,10 +138,6 @@ func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dash 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 } @@ -206,11 +202,6 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { // if new dashboard, use parent folder permissions instead if dashId == 0 { dashId = cmd.FolderId - } else { - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") - if rsp != nil { - return rsp - } } guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 129a891d06e..87e1eac4113 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -33,79 +33,6 @@ var fakeRepo *fakeDashboardRepo // 2. and the dashboard is in a folder which does have an acl 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.GetDashboardsBySlugQuery) error { - dashboards := []*m.Dashboard{fakeFolder} - query.Result = dashboards - return nil - }) - - var getDashboardQueries []*m.GetDashboardQuery - - bus.AddHandler("test", func(query *m.GetDashboardQuery) error { - query.Result = fakeFolder - getDashboardQueries = append(getDashboardQueries, query) - 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/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - CallGetDashboard(sc) - So(sc.resp.Code, ShouldEqual, 404) - - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") - }) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallGetDashboard(sc) - So(sc.resp.Code, ShouldEqual, 404) - - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) - }) - - 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/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - CallDeleteDashboard(sc) - So(sc.resp.Code, ShouldEqual, 404) - - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) - So(sc.resp.Code, ShouldEqual, 404) - - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) - }) - }) - }) - Convey("Given a dashboard with a parent folder which does not have an acl", t, func() { fakeDash := m.NewDashboard("Child dash") fakeDash.Id = 1 From 5912cf4dc2bf6cd545ab16c141022df87c243999 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 18:41:50 +0100 Subject: [PATCH 03/26] folders: rename api files --- pkg/api/{folders.go => folder.go} | 0 pkg/api/{folders_test.go => folder_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename pkg/api/{folders.go => folder.go} (100%) rename pkg/api/{folders_test.go => folder_test.go} (100%) diff --git a/pkg/api/folders.go b/pkg/api/folder.go similarity index 100% rename from pkg/api/folders.go rename to pkg/api/folder.go diff --git a/pkg/api/folders_test.go b/pkg/api/folder_test.go similarity index 100% rename from pkg/api/folders_test.go rename to pkg/api/folder_test.go From 773fff9f1a5cf48c953f5223817772b42da2b858 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 21:00:37 +0100 Subject: [PATCH 04/26] folders: changes and updated tests after merging permissions and new url structure --- pkg/api/api.go | 7 +- pkg/api/dtos/folder.go | 3 +- pkg/api/folder.go | 39 ++++----- pkg/api/folder_test.go | 186 +++++++++++++++++++++++++++++++++++++---- pkg/models/folders.go | 47 ++++++++++- 5 files changed, 240 insertions(+), 42 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9b207a37293..0969191d551 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -248,10 +248,11 @@ func (hs *HttpServer) registerRoutes() { // Folders apiRoute.Group("/folders", func(folderRoute RouteRegister) { - folderRoute.Get("/:id", wrap(GetFolderById)) + folderRoute.Get("/:uid", wrap(GetFolder)) + folderRoute.Get("/id/:id", wrap(GetFolder)) folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder)) - folderRoute.Put("/:id", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) - folderRoute.Delete("/:id", wrap(DeleteFolder)) + folderRoute.Put("/:uid", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) + folderRoute.Delete("/:uid", wrap(DeleteFolder)) }) // Dashboard diff --git a/pkg/api/dtos/folder.go b/pkg/api/dtos/folder.go index 7ce7825fb6a..159d41fee1c 100644 --- a/pkg/api/dtos/folder.go +++ b/pkg/api/dtos/folder.go @@ -4,8 +4,9 @@ import "time" type Folder struct { Id int64 `json:"id"` + Uid string `json:"uid"` Title string `json:"title"` - Slug string `json:"slug"` + Url string `json:"url"` HasAcl bool `json:"hasAcl"` CanSave bool `json:"canSave"` CanEdit bool `json:"canEdit"` diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 9823f93982e..05d8a80aae9 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -12,8 +12,8 @@ import ( "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} +func getFolderHelper(orgId int64, id int64, uid string) (*m.Dashboard, Response) { + query := m.GetDashboardQuery{OrgId: orgId, Id: id, Uid: uid} if err := bus.Dispatch(&query); err != nil { if err == m.ErrDashboardNotFound { err = m.ErrFolderNotFound @@ -53,8 +53,8 @@ func GetFoldersForSignedInUser(c *middleware.Context) Response { return Json(200, query.Result) } -func GetFolderById(c *middleware.Context) Response { - folder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id")) +func GetFolder(c *middleware.Context) Response { + folder, rsp := getFolderHelper(c.OrgId, c.ParamsInt64(":id"), c.Params(":uid")) if rsp != nil { return rsp } @@ -72,14 +72,13 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { cmd.OrgId = c.OrgId cmd.UserId = c.UserId - dashFolder := m.NewDashboardFolder(cmd.Title) + dashFolder := cmd.GetDashboardModel() 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) } @@ -92,9 +91,6 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { return ApiError(403, "Quota reached", nil) } - dashFolder.CreatedBy = c.UserId - dashFolder.UpdatedBy = c.UserId - dashItem := &dashboards.SaveDashboardItem{ Dashboard: dashFolder, OrgId: c.OrgId, @@ -113,8 +109,9 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { cmd.OrgId = c.OrgId cmd.UserId = c.UserId + uid := c.Params(":uid") - dashFolder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id")) + dashFolder, rsp := getFolderHelper(c.OrgId, 0, uid) if rsp != nil { return rsp } @@ -124,13 +121,8 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { 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 + cmd.UpdateDashboardModel(dashFolder) - // Check if Title is empty if dashFolder.Title == "" { return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil) } @@ -151,7 +143,7 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { } func DeleteFolder(c *middleware.Context) Response { - dashFolder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id")) + dashFolder, rsp := getFolderHelper(c.OrgId, 0, c.Params(":uid")) if rsp != nil { return rsp } @@ -177,17 +169,18 @@ func toDto(guardian *guardian.DashboardGuardian, folder *m.Dashboard) dtos.Folde // 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) } + if folder.UpdatedBy > 0 { + updater = getUserLogin(folder.UpdatedBy) + } return dtos.Folder{ Id: folder.Id, + Uid: folder.Uid, Title: folder.Title, - Slug: folder.Slug, + Url: folder.GetUrl(), HasAcl: folder.HasAcl, CanSave: canSave, CanEdit: canEdit, @@ -205,6 +198,10 @@ func toFolderError(err error) Response { return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil) } + if err == m.ErrDashboardWithSameNameInFolderExists { + return Json(412, util.DynMap{"status": "name-exists", "message": m.ErrFolderSameNameExists.Error()}) + } + if err == m.ErrDashboardWithSameUIDExists { return Json(412, util.DynMap{"status": "uid-exists", "message": m.ErrFolderWithSameUIDExists.Error()}) } diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index cbe7eb7a280..6ef18e9a749 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -23,8 +23,11 @@ func TestFoldersApiEndpoint(t *testing.T) { fakeDash.FolderId = 1 fakeDash.HasAcl = false + var getDashboardQueries []*m.GetDashboardQuery + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeDash + getDashboardQueries = append(getDashboardQueries, query) return nil }) @@ -33,19 +36,40 @@ func TestFoldersApiEndpoint(t *testing.T) { Convey("When user is an Org Editor", func() { role := m.ROLE_EDITOR + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { + callGetFolder(sc) + So(sc.resp.Code, ShouldEqual, 404) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { callGetFolder(sc) So(sc.resp.Code, ShouldEqual, 404) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) }) - updateFolderScenario("When calling PUT on", "/api/folders/1", "/api/folders/:id", role, updateFolderCmd, func(sc *scenarioContext) { + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { callUpdateFolder(sc) So(sc.resp.Code, ShouldEqual, 404) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 404) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) }) }) @@ -55,8 +79,11 @@ func TestFoldersApiEndpoint(t *testing.T) { fakeFolder.Id = 1 fakeFolder.HasAcl = false + var getDashboardQueries []*m.GetDashboardQuery + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeFolder + getDashboardQueries = append(getDashboardQueries, query) return nil }) @@ -82,12 +109,20 @@ func TestFoldersApiEndpoint(t *testing.T) { Title: fakeFolder.Title, } + updateFolderCmd := m.UpdateFolderCommand{ + 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { folder := getFolderShouldReturn200(sc) + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + Convey("Should not be able to edit or save folder", func() { So(folder.CanEdit, ShouldBeFalse) So(folder.CanSave, ShouldBeFalse) @@ -95,23 +130,54 @@ func TestFoldersApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + folder := getFolderShouldReturn200(sc) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) + + 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { callCreateFolder(sc) So(sc.resp.Code, ShouldEqual, 403) }) + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) }) 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { folder := getFolderShouldReturn200(sc) + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + Convey("Should be able to edit or save folder", func() { So(folder.CanEdit, ShouldBeTrue) So(folder.CanSave, ShouldBeTrue) @@ -119,15 +185,42 @@ func TestFoldersApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + folder := getFolderShouldReturn200(sc) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) + + 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { callCreateFolder(sc) So(sc.resp.Code, ShouldEqual, 200) }) + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) }) }) @@ -136,8 +229,11 @@ func TestFoldersApiEndpoint(t *testing.T) { fakeFolder.Id = 1 fakeFolder.HasAcl = true + var getDashboardQueries []*m.GetDashboardQuery + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeFolder + getDashboardQueries = append(getDashboardQueries, query) return nil }) @@ -163,50 +259,110 @@ func TestFoldersApiEndpoint(t *testing.T) { Title: fakeFolder.Title, } + updateFolderCmd := m.UpdateFolderCommand{ + 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() + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { + callGetFolder(sc) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + callGetFolder(sc) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { callCreateFolder(sc) So(sc.resp.Code, ShouldEqual, 403) }) + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) }) 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() + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { + callGetFolder(sc) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + callGetFolder(sc) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { callCreateFolder(sc) So(sc.resp.Code, ShouldEqual, 403) }) + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) }) }) } @@ -224,7 +380,7 @@ func getFolderShouldReturn200(sc *scenarioContext) dtos.Folder { } func callGetFolder(sc *scenarioContext) { - sc.handlerFunc = GetFolderById + sc.handlerFunc = GetFolder sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } diff --git a/pkg/models/folders.go b/pkg/models/folders.go index f07b5daa04e..f29e106d362 100644 --- a/pkg/models/folders.go +++ b/pkg/models/folders.go @@ -2,6 +2,7 @@ package models import ( "errors" + "strings" "time" ) @@ -10,14 +11,16 @@ 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") - ErrFolderWithSameUIDExists = errors.New("A folder with the same uid already exists") + ErrFolderWithSameUIDExists = errors.New("A folder/dashboard with the same uid already exists") + ErrFolderSameNameExists = errors.New("A folder or dashboard in the general folder with the same name already exists") ErrFolderFailedGenerateUniqueUid = errors.New("Failed to generate unique folder id") ) type Folder struct { Id int64 + Uid string Title string - Slug string + Url string OrgId int64 Version int @@ -29,6 +32,45 @@ type Folder struct { HasAcl bool } +// GetDashboardModel turns the command into the savable model +func (cmd *CreateFolderCommand) GetDashboardModel() *Dashboard { + dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title)) + dashFolder.OrgId = cmd.OrgId + dashFolder.Uid = strings.TrimSpace(cmd.Uid) + dashFolder.Data.Set("uid", cmd.Uid) + + userId := cmd.UserId + + if userId == 0 { + userId = -1 + } + + dashFolder.CreatedBy = userId + dashFolder.UpdatedBy = userId + dashFolder.UpdateSlug() + + return dashFolder +} + +// UpdateDashboardModel updates an existing model from command into model for update +func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard) { + dashFolder.Title = strings.TrimSpace(cmd.Title) + dashFolder.Data.Set("title", cmd.Title) + dashFolder.Uid = dashFolder.Data.MustString("uid") + dashFolder.Data.Set("version", cmd.Version) + dashFolder.Version = cmd.Version + dashFolder.IsFolder = true + + userId := cmd.UserId + + if userId == 0 { + userId = -1 + } + + dashFolder.UpdatedBy = userId + dashFolder.UpdateSlug() +} + // // COMMANDS // @@ -36,6 +78,7 @@ type Folder struct { type CreateFolderCommand struct { OrgId int64 `json:"-"` UserId int64 `json:"userId"` + Uid string `json:"uid"` Title string `json:"title"` Result *Folder From 04a9a650e7d6effc38cbb13c8f4b5518b9bf9e04 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 21:00:37 +0100 Subject: [PATCH 05/26] folders: changes and updated tests after merging permissions and new url structure --- pkg/api/api.go | 7 +- pkg/api/dtos/folder.go | 3 +- pkg/api/folder.go | 40 +++++---- pkg/api/folder_test.go | 186 +++++++++++++++++++++++++++++++++++++---- pkg/models/folders.go | 56 +++++++++++-- 5 files changed, 246 insertions(+), 46 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9b207a37293..0969191d551 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -248,10 +248,11 @@ func (hs *HttpServer) registerRoutes() { // Folders apiRoute.Group("/folders", func(folderRoute RouteRegister) { - folderRoute.Get("/:id", wrap(GetFolderById)) + folderRoute.Get("/:uid", wrap(GetFolder)) + folderRoute.Get("/id/:id", wrap(GetFolder)) folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder)) - folderRoute.Put("/:id", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) - folderRoute.Delete("/:id", wrap(DeleteFolder)) + folderRoute.Put("/:uid", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) + folderRoute.Delete("/:uid", wrap(DeleteFolder)) }) // Dashboard diff --git a/pkg/api/dtos/folder.go b/pkg/api/dtos/folder.go index 7ce7825fb6a..159d41fee1c 100644 --- a/pkg/api/dtos/folder.go +++ b/pkg/api/dtos/folder.go @@ -4,8 +4,9 @@ import "time" type Folder struct { Id int64 `json:"id"` + Uid string `json:"uid"` Title string `json:"title"` - Slug string `json:"slug"` + Url string `json:"url"` HasAcl bool `json:"hasAcl"` CanSave bool `json:"canSave"` CanEdit bool `json:"canEdit"` diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 9823f93982e..5358caa4b61 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -12,8 +12,8 @@ import ( "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} +func getFolderHelper(orgId int64, id int64, uid string) (*m.Dashboard, Response) { + query := m.GetDashboardQuery{OrgId: orgId, Id: id, Uid: uid} if err := bus.Dispatch(&query); err != nil { if err == m.ErrDashboardNotFound { err = m.ErrFolderNotFound @@ -53,8 +53,8 @@ func GetFoldersForSignedInUser(c *middleware.Context) Response { return Json(200, query.Result) } -func GetFolderById(c *middleware.Context) Response { - folder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id")) +func GetFolder(c *middleware.Context) Response { + folder, rsp := getFolderHelper(c.OrgId, c.ParamsInt64(":id"), c.Params(":uid")) if rsp != nil { return rsp } @@ -72,14 +72,13 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { cmd.OrgId = c.OrgId cmd.UserId = c.UserId - dashFolder := m.NewDashboardFolder(cmd.Title) + dashFolder := cmd.GetDashboardModel() 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) } @@ -92,9 +91,6 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { return ApiError(403, "Quota reached", nil) } - dashFolder.CreatedBy = c.UserId - dashFolder.UpdatedBy = c.UserId - dashItem := &dashboards.SaveDashboardItem{ Dashboard: dashFolder, OrgId: c.OrgId, @@ -113,8 +109,9 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { cmd.OrgId = c.OrgId cmd.UserId = c.UserId + uid := c.Params(":uid") - dashFolder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id")) + dashFolder, rsp := getFolderHelper(c.OrgId, 0, uid) if rsp != nil { return rsp } @@ -124,13 +121,8 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { 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 + cmd.UpdateDashboardModel(dashFolder) - // Check if Title is empty if dashFolder.Title == "" { return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil) } @@ -139,6 +131,7 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { Dashboard: dashFolder, OrgId: c.OrgId, UserId: c.UserId, + Overwrite: cmd.Overwrite, } folder, err := dashboards.GetRepository().SaveDashboard(dashItem) @@ -151,7 +144,7 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { } func DeleteFolder(c *middleware.Context) Response { - dashFolder, rsp := getFolderHelper(c.OrgId, "", c.ParamsInt64(":id")) + dashFolder, rsp := getFolderHelper(c.OrgId, 0, c.Params(":uid")) if rsp != nil { return rsp } @@ -177,17 +170,18 @@ func toDto(guardian *guardian.DashboardGuardian, folder *m.Dashboard) dtos.Folde // 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) } + if folder.UpdatedBy > 0 { + updater = getUserLogin(folder.UpdatedBy) + } return dtos.Folder{ Id: folder.Id, + Uid: folder.Uid, Title: folder.Title, - Slug: folder.Slug, + Url: folder.GetUrl(), HasAcl: folder.HasAcl, CanSave: canSave, CanEdit: canEdit, @@ -205,6 +199,10 @@ func toFolderError(err error) Response { return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil) } + if err == m.ErrDashboardWithSameNameInFolderExists { + return Json(412, util.DynMap{"status": "name-exists", "message": m.ErrFolderSameNameExists.Error()}) + } + if err == m.ErrDashboardWithSameUIDExists { return Json(412, util.DynMap{"status": "uid-exists", "message": m.ErrFolderWithSameUIDExists.Error()}) } diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index cbe7eb7a280..6ef18e9a749 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -23,8 +23,11 @@ func TestFoldersApiEndpoint(t *testing.T) { fakeDash.FolderId = 1 fakeDash.HasAcl = false + var getDashboardQueries []*m.GetDashboardQuery + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeDash + getDashboardQueries = append(getDashboardQueries, query) return nil }) @@ -33,19 +36,40 @@ func TestFoldersApiEndpoint(t *testing.T) { Convey("When user is an Org Editor", func() { role := m.ROLE_EDITOR + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { + callGetFolder(sc) + So(sc.resp.Code, ShouldEqual, 404) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { callGetFolder(sc) So(sc.resp.Code, ShouldEqual, 404) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) }) - updateFolderScenario("When calling PUT on", "/api/folders/1", "/api/folders/:id", role, updateFolderCmd, func(sc *scenarioContext) { + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { callUpdateFolder(sc) So(sc.resp.Code, ShouldEqual, 404) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 404) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) }) }) @@ -55,8 +79,11 @@ func TestFoldersApiEndpoint(t *testing.T) { fakeFolder.Id = 1 fakeFolder.HasAcl = false + var getDashboardQueries []*m.GetDashboardQuery + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeFolder + getDashboardQueries = append(getDashboardQueries, query) return nil }) @@ -82,12 +109,20 @@ func TestFoldersApiEndpoint(t *testing.T) { Title: fakeFolder.Title, } + updateFolderCmd := m.UpdateFolderCommand{ + 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { folder := getFolderShouldReturn200(sc) + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + Convey("Should not be able to edit or save folder", func() { So(folder.CanEdit, ShouldBeFalse) So(folder.CanSave, ShouldBeFalse) @@ -95,23 +130,54 @@ func TestFoldersApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + folder := getFolderShouldReturn200(sc) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) + + 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { callCreateFolder(sc) So(sc.resp.Code, ShouldEqual, 403) }) + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) }) 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { folder := getFolderShouldReturn200(sc) + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + Convey("Should be able to edit or save folder", func() { So(folder.CanEdit, ShouldBeTrue) So(folder.CanSave, ShouldBeTrue) @@ -119,15 +185,42 @@ func TestFoldersApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + folder := getFolderShouldReturn200(sc) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) + + 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { callCreateFolder(sc) So(sc.resp.Code, ShouldEqual, 200) }) + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) }) }) @@ -136,8 +229,11 @@ func TestFoldersApiEndpoint(t *testing.T) { fakeFolder.Id = 1 fakeFolder.HasAcl = true + var getDashboardQueries []*m.GetDashboardQuery + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeFolder + getDashboardQueries = append(getDashboardQueries, query) return nil }) @@ -163,50 +259,110 @@ func TestFoldersApiEndpoint(t *testing.T) { Title: fakeFolder.Title, } + updateFolderCmd := m.UpdateFolderCommand{ + 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() + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { + callGetFolder(sc) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + callGetFolder(sc) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { callCreateFolder(sc) So(sc.resp.Code, ShouldEqual, 403) }) + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) }) 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() + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { + callGetFolder(sc) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { + callGetFolder(sc) + + Convey("Should lookup folder by id", func() { + So(getDashboardQueries[0].Id, ShouldEqual, 1) + }) + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { callDeleteFolder(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) }) createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { callCreateFolder(sc) So(sc.resp.Code, ShouldEqual, 403) }) + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup folder by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "uid") + }) + }) }) }) } @@ -224,7 +380,7 @@ func getFolderShouldReturn200(sc *scenarioContext) dtos.Folder { } func callGetFolder(sc *scenarioContext) { - sc.handlerFunc = GetFolderById + sc.handlerFunc = GetFolder sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } diff --git a/pkg/models/folders.go b/pkg/models/folders.go index f07b5daa04e..6ea0ac16759 100644 --- a/pkg/models/folders.go +++ b/pkg/models/folders.go @@ -2,6 +2,7 @@ package models import ( "errors" + "strings" "time" ) @@ -10,14 +11,16 @@ 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") - ErrFolderWithSameUIDExists = errors.New("A folder with the same uid already exists") + ErrFolderWithSameUIDExists = errors.New("A folder/dashboard with the same uid already exists") + ErrFolderSameNameExists = errors.New("A folder or dashboard in the general folder with the same name already exists") ErrFolderFailedGenerateUniqueUid = errors.New("Failed to generate unique folder id") ) type Folder struct { Id int64 + Uid string Title string - Slug string + Url string OrgId int64 Version int @@ -29,6 +32,45 @@ type Folder struct { HasAcl bool } +// GetDashboardModel turns the command into the savable model +func (cmd *CreateFolderCommand) GetDashboardModel() *Dashboard { + dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title)) + dashFolder.OrgId = cmd.OrgId + dashFolder.Uid = strings.TrimSpace(cmd.Uid) + dashFolder.Data.Set("uid", cmd.Uid) + + userId := cmd.UserId + + if userId == 0 { + userId = -1 + } + + dashFolder.CreatedBy = userId + dashFolder.UpdatedBy = userId + dashFolder.UpdateSlug() + + return dashFolder +} + +// UpdateDashboardModel updates an existing model from command into model for update +func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard) { + dashFolder.Title = strings.TrimSpace(cmd.Title) + dashFolder.Data.Set("title", cmd.Title) + dashFolder.Uid = dashFolder.Data.MustString("uid") + dashFolder.Data.Set("version", cmd.Version) + dashFolder.Version = cmd.Version + dashFolder.IsFolder = true + + userId := cmd.UserId + + if userId == 0 { + userId = -1 + } + + dashFolder.UpdatedBy = userId + dashFolder.UpdateSlug() +} + // // COMMANDS // @@ -36,16 +78,18 @@ type Folder struct { type CreateFolderCommand struct { OrgId int64 `json:"-"` UserId int64 `json:"userId"` + Uid string `json:"uid"` Title string `json:"title"` Result *Folder } type UpdateFolderCommand struct { - OrgId int64 `json:"-"` - UserId int64 `json:"userId"` - Title string `json:"title"` - Version int `json:"version"` + OrgId int64 `json:"-"` + UserId int64 `json:"userId"` + Title string `json:"title"` + Version int `json:"version"` + Overwrite bool `json:"overwrite"` Result *Folder } From d217688d5ebace5a17935a08861daa556469d20b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 22:32:26 +0100 Subject: [PATCH 06/26] folders: use new folder api in frontend --- .../ManageDashboards/FolderPermissions.tsx | 2 +- .../ManageDashboards/FolderSettings.jest.tsx | 16 +++--- .../ManageDashboards/FolderSettings.tsx | 31 +++++----- .../manage_dashboards/manage_dashboards.ts | 13 ++--- public/app/core/services/backend_srv.ts | 56 +++++++++++-------- .../features/dashboard/create_folder_ctrl.ts | 4 +- .../dashboard/folder_dashboards_ctrl.ts | 2 +- .../features/dashboard/folder_page_loader.ts | 12 ++-- .../dashboard/folder_settings_ctrl.ts | 28 ++++------ public/app/stores/FolderStore/FolderStore.ts | 23 +++++--- public/test/mocks/common.ts | 1 + 11 files changed, 98 insertions(+), 90 deletions(-) diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 93b9520739e..1af58b44bcc 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -20,7 +20,7 @@ export class FolderPermissions extends Component { loadStore() { const { nav, folder, view } = this.props; return folder.load(view.routeParams.get('uid') as string).then(res => { - view.updatePathAndQuery(`${res.meta.url}/permissions`, {}, {}); + view.updatePathAndQuery(`${res.url}/permissions`, {}, {}); return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); }); } diff --git a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx b/public/app/containers/ManageDashboards/FolderSettings.jest.tsx index bf7b35ed05d..72ba9ff6a7b 100644 --- a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx +++ b/public/app/containers/ManageDashboards/FolderSettings.jest.tsx @@ -9,16 +9,14 @@ describe('FolderSettings', () => { let page; beforeAll(() => { - backendSrv.getDashboardByUid.mockReturnValue( + backendSrv.getFolderByUid.mockReturnValue( Promise.resolve({ - dashboard: { - id: 1, - title: 'Folder Name', - }, - meta: { - url: '/dashboards/f/uid/folder-name', - canSave: true, - }, + id: 1, + uid: 'uid', + title: 'Folder Name', + url: '/dashboards/f/uid/folder-name', + canSave: true, + version: 1, }) ); diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx index a6349764a14..586a8f05b4c 100644 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ b/public/app/containers/ManageDashboards/FolderSettings.tsx @@ -10,7 +10,6 @@ import appEvents from 'app/core/app_events'; @observer export class FolderSettings extends React.Component { formSnapshot: any; - dashboard: any; constructor(props) { super(props); @@ -22,9 +21,7 @@ export class FolderSettings extends React.Component { return folder.load(view.routeParams.get('uid') as string).then(res => { this.formSnapshot = getSnapshot(folder); - this.dashboard = res.dashboard; - - view.updatePathAndQuery(`${res.meta.url}/settings`, {}, {}); + view.updatePathAndQuery(`${res.url}/settings`, {}, {}); return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); }); @@ -51,7 +48,7 @@ export class FolderSettings extends React.Component { const { nav, folder, view } = this.props; folder - .saveFolder(this.dashboard, { overwrite: false }) + .saveFolder({ overwrite: false }) .then(newUrl => { view.updatePathAndQuery(newUrl, {}, {}); @@ -61,7 +58,7 @@ export class FolderSettings extends React.Component { .then(() => { return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); }) - .catch(this.handleSaveFolderError); + .catch(this.handleSaveFolderError.bind(this)); } delete(evt) { @@ -79,7 +76,7 @@ export class FolderSettings extends React.Component { icon: 'fa-trash', yesText: 'Delete', onConfirm: () => { - return this.props.folder.deleteFolder().then(() => { + return folder.deleteFolder().then(() => { appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); view.updatePathAndQuery('dashboards', '', ''); }); @@ -91,6 +88,8 @@ export class FolderSettings extends React.Component { if (err.data && err.data.status === 'version-mismatch') { err.isHandled = true; + const { nav, folder, view } = this.props; + appEvents.emit('confirm-modal', { title: 'Conflict', text: 'Someone else has updated this folder.', @@ -98,16 +97,20 @@ export class FolderSettings extends React.Component { yesText: 'Save & Overwrite', icon: 'fa-warning', onConfirm: () => { - this.props.folder.saveFolder(this.dashboard, { overwrite: true }); + folder + .saveFolder({ overwrite: true }) + .then(newUrl => { + view.updatePathAndQuery(newUrl, {}, {}); + + appEvents.emit('dashboard-saved'); + appEvents.emit('alert-success', ['Folder saved']); + }) + .then(() => { + return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + }); }, }); } - - if (err.data && err.data.status === 'name-exists') { - err.isHandled = true; - - appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']); - } } render() { diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index d0f905f5f16..448b82cce54 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -173,17 +173,16 @@ export class ManageDashboardsCtrl { icon: 'fa-trash', yesText: 'Delete', onConfirm: () => { - const foldersAndDashboards = data.folders.concat(data.dashboards); - this.deleteFoldersAndDashboards(foldersAndDashboards); + this.deleteFoldersAndDashboards(data.folders, data.dashboards); }, }); } - private deleteFoldersAndDashboards(uids) { - this.backendSrv.deleteDashboards(uids).then(result => { - const folders = _.filter(result, dash => dash.meta.isFolder); + private deleteFoldersAndDashboards(folderUids, dashboardUids) { + this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(result => { + const folders = _.filter(result, dash => !dash.dashboard); const folderCount = folders.length; - const dashboards = _.filter(result, dash => !dash.meta.isFolder); + const dashboards = _.filter(result, dash => dash.dashboard); const dashCount = dashboards.length; if (result.length > 0) { @@ -198,7 +197,7 @@ export class ManageDashboardsCtrl { header = `Folder${folderCount === 1 ? '' : 's'} Deleted`; if (folderCount === 1) { - msg = `${folders[0].dashboard.title} has been deleted`; + msg = `${folders[0].title} has been deleted`; } else { msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} has been deleted`; } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 0a8b305ea53..b9712d330d9 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -229,6 +229,10 @@ export class BackendSrv { return this.get(`/api/dashboards/uid/${uid}`); } + getFolderByUid(uid: string) { + return this.get(`/api/folders/${uid}`); + } + saveDashboard(dash, options) { options = options || {}; @@ -240,32 +244,34 @@ export class BackendSrv { }); } - createDashboardFolder(name) { - const dash = { - schemaVersion: 16, - title: name.trim(), - editable: true, - panels: [], - }; + createFolder(payload: any) { + return this.post('/api/folders', payload); + } - return this.post('/api/dashboards/db/', { - dashboard: dash, - isFolder: true, - overwrite: false, - }).then(res => { - return this.getDashboard('db', res.slug); + updateFolder(folder, options) { + options = options || {}; + + return this.put(`/api/folders/${folder.uid}`, { + title: folder.title, + version: folder.version, + overwrite: options.overwrite === true, }); } - saveFolder(dash, options) { - options = options || {}; + deleteFolder(uid: string) { + let deferred = this.$q.defer(); - return this.post('/api/dashboards/db/', { - dashboard: dash, - isFolder: true, - overwrite: options.overwrite === true, - message: options.message || '', + this.getFolderByUid(uid).then(folder => { + this.delete(`/api/folders/${uid}`) + .then(() => { + deferred.resolve(folder); + }) + .catch(err => { + deferred.reject(err); + }); }); + + return deferred.promise; } deleteDashboard(uid) { @@ -284,11 +290,15 @@ export class BackendSrv { return deferred.promise; } - deleteDashboards(dashboardUids) { + deleteFoldersAndDashboards(folderUids, dashboardUids) { const tasks = []; - for (let uid of dashboardUids) { - tasks.push(this.createTask(this.deleteDashboard.bind(this), true, uid)); + for (let folderUid of folderUids) { + tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid)); + } + + for (let dashboardUid of dashboardUids) { + tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid)); } return this.executeInOrder(tasks, []); diff --git a/public/app/features/dashboard/create_folder_ctrl.ts b/public/app/features/dashboard/create_folder_ctrl.ts index f3d9167278b..0d6a92d24e3 100644 --- a/public/app/features/dashboard/create_folder_ctrl.ts +++ b/public/app/features/dashboard/create_folder_ctrl.ts @@ -17,9 +17,9 @@ export class CreateFolderCtrl { return; } - return this.backendSrv.createDashboardFolder(this.title).then(result => { + return this.backendSrv.createFolder({ title: this.title }).then(result => { appEvents.emit('alert-success', ['Folder Created', 'OK']); - this.$location.url(result.meta.url); + this.$location.url(result.url); }); } diff --git a/public/app/features/dashboard/folder_dashboards_ctrl.ts b/public/app/features/dashboard/folder_dashboards_ctrl.ts index 8ee942445ae..05cc420c489 100644 --- a/public/app/features/dashboard/folder_dashboards_ctrl.ts +++ b/public/app/features/dashboard/folder_dashboards_ctrl.ts @@ -14,7 +14,7 @@ export class FolderDashboardsCtrl { const loader = new FolderPageLoader(this.backendSrv); loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => { - const url = locationUtil.stripBaseFromUrl(folder.meta.url); + const url = locationUtil.stripBaseFromUrl(folder.url); if (url !== $location.path()) { $location.path(url).replace(); diff --git a/public/app/features/dashboard/folder_page_loader.ts b/public/app/features/dashboard/folder_page_loader.ts index 81d10068361..6842c61847e 100755 --- a/public/app/features/dashboard/folder_page_loader.ts +++ b/public/app/features/dashboard/folder_page_loader.ts @@ -36,16 +36,16 @@ export class FolderPageLoader { }, }; - return this.backendSrv.getDashboardByUid(uid).then(result => { - ctrl.folderId = result.dashboard.id; - const folderTitle = result.dashboard.title; - const folderUrl = result.meta.url; + return this.backendSrv.getFolderByUid(uid).then(folder => { + ctrl.folderId = folder.id; + const folderTitle = folder.title; + const folderUrl = folder.url; ctrl.navModel.main.text = folderTitle; const dashTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-dashboards'); dashTab.url = folderUrl; - if (result.meta.canAdmin) { + if (folder.canAdmin) { const permTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-permissions'); permTab.url = folderUrl + '/permissions'; @@ -55,7 +55,7 @@ export class FolderPageLoader { ctrl.navModel.main.children = [dashTab]; } - return result; + return folder; }); } } diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts index 004ba2efa9f..a847c29ac56 100644 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ b/public/app/features/dashboard/folder_settings_ctrl.ts @@ -7,8 +7,7 @@ export class FolderSettingsCtrl { folderId: number; uid: string; canSave = false; - dashboard: any; - meta: any; + folder: any; title: string; hasChanged: boolean; @@ -23,10 +22,9 @@ export class FolderSettingsCtrl { $location.path(`${folder.meta.url}/settings`).replace(); } - this.dashboard = folder.dashboard; - this.meta = folder.meta; - this.canSave = folder.meta.canSave; - this.title = this.dashboard.title; + this.folder = folder; + this.canSave = this.folder.canSave; + this.title = this.folder.title; }); } } @@ -38,10 +36,10 @@ export class FolderSettingsCtrl { return; } - this.dashboard.title = this.title.trim(); + this.folder.title = this.title.trim(); return this.backendSrv - .updateDashboardFolder(this.dashboard, { overwrite: false }) + .updateFolder(this.folder) .then(result => { if (result.url !== this.$location.path()) { this.$location.url(result.url + '/settings'); @@ -54,7 +52,7 @@ export class FolderSettingsCtrl { } titleChanged() { - this.hasChanged = this.dashboard.title.toLowerCase() !== this.title.trim().toLowerCase(); + this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase(); } delete(evt) { @@ -69,8 +67,8 @@ export class FolderSettingsCtrl { icon: 'fa-trash', yesText: 'Delete', onConfirm: () => { - return this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]); + return this.backendSrv.deleteFolder(this.uid).then(() => { + appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]); this.$location.url('dashboards'); }); }, @@ -88,15 +86,9 @@ export class FolderSettingsCtrl { yesText: 'Save & Overwrite', icon: 'fa-warning', onConfirm: () => { - this.backendSrv.updateDashboardFolder(this.dashboard, { overwrite: true }); + this.backendSrv.updateFolder(this.folder, { overwrite: true }); }, }); } - - if (err.data && err.data.status === 'name-exists') { - err.isHandled = true; - - appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']); - } } } diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts index 6f14e7221f8..4e2066f16e5 100644 --- a/public/app/stores/FolderStore/FolderStore.ts +++ b/public/app/stores/FolderStore/FolderStore.ts @@ -2,10 +2,12 @@ import { types, getEnv, flow } from 'mobx-state-tree'; export const Folder = types.model('Folder', { id: types.identifier(types.number), + uid: types.string, title: types.string, url: types.string, canSave: types.boolean, hasChanged: types.boolean, + version: types.number, }); export const FolderStore = types @@ -15,13 +17,15 @@ export const FolderStore = types .actions(self => ({ load: flow(function* load(uid: string) { const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getDashboardByUid(uid); + const res = yield backendSrv.getFolderByUid(uid); self.folder = Folder.create({ - id: res.dashboard.id, - title: res.dashboard.title, - url: res.meta.url, - canSave: res.meta.canSave, + id: res.id, + uid: res.uid, + title: res.title, + url: res.url, + canSave: res.canSave, hasChanged: false, + version: res.version, }); return res; }), @@ -31,12 +35,13 @@ export const FolderStore = types self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; }, - saveFolder: flow(function* saveFolder(dashboard: any, options: any) { + saveFolder: flow(function* saveFolder(options: any) { const backendSrv = getEnv(self).backendSrv; - dashboard.title = self.folder.title.trim(); + self.folder.title = self.folder.title.trim(); - const res = yield backendSrv.saveFolder(dashboard, options); + const res = yield backendSrv.updateFolder(self.folder, options); self.folder.url = res.url; + self.folder.version = res.version; return `${self.folder.url}/settings`; }), @@ -44,6 +49,6 @@ export const FolderStore = types deleteFolder: flow(function* deleteFolder() { const backendSrv = getEnv(self).backendSrv; - return backendSrv.deleteDashboard(self.folder.url); + return backendSrv.deleteFolder(self.folder.uid); }), })); diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 3f80227435d..1531f2ed176 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -2,6 +2,7 @@ export const backendSrv = { get: jest.fn(), getDashboard: jest.fn(), getDashboardByUid: jest.fn(), + getFolderByUid: jest.fn(), post: jest.fn(), }; From 39aba0348389bed60c44fcaf7cd0a512ab7d3cc2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Feb 2018 13:54:31 +0100 Subject: [PATCH 07/26] dashboards: created date should be set when creating a folder/dashboard --- pkg/services/sqlstore/dashboard.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 9da93a08450..5ae1b626554 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -60,6 +60,8 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { if existing.PluginId != "" && cmd.Overwrite == false { return m.UpdatePluginDashboardError{PluginId: existing.PluginId} } + } else { + dash.Created = time.Now() } if dash.Uid == "" { From 268fb4dc6c1c573bcddc44bae7a9e2413fb37544 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Feb 2018 13:55:43 +0100 Subject: [PATCH 08/26] folders: new folder service for managing folders --- pkg/models/folders.go | 38 ++- pkg/services/dashboards/dashboard_service.go | 27 +- .../dashboards/dashboard_service_test.go | 2 +- pkg/services/dashboards/folder_service.go | 246 ++++++++++++++++++ .../dashboards/folder_service_test.go | 168 ++++++++++++ 5 files changed, 449 insertions(+), 32 deletions(-) create mode 100644 pkg/services/dashboards/folder_service.go create mode 100644 pkg/services/dashboards/folder_service_test.go diff --git a/pkg/models/folders.go b/pkg/models/folders.go index 54085e13d4b..43c7f1a9165 100644 --- a/pkg/models/folders.go +++ b/pkg/models/folders.go @@ -14,6 +14,7 @@ var ( ErrFolderWithSameUIDExists = errors.New("A folder/dashboard with the same uid already exists") ErrFolderSameNameExists = errors.New("A folder or dashboard in the general folder with the same name already exists") ErrFolderFailedGenerateUniqueUid = errors.New("Failed to generate unique folder id") + ErrFolderAccessDenied = errors.New("Access denied to folder") ) type Folder struct { @@ -21,7 +22,6 @@ type Folder struct { Uid string Title string Url string - OrgId int64 Version int Created time.Time @@ -33,13 +33,10 @@ type Folder struct { } // GetDashboardModel turns the command into the savable model -func (cmd *CreateFolderCommand) GetDashboardModel() *Dashboard { +func (cmd *CreateFolderCommand) GetDashboardModel(orgId int64, userId int64) *Dashboard { dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title)) - dashFolder.OrgId = cmd.OrgId - dashFolder.Uid = strings.TrimSpace(cmd.Uid) - dashFolder.Data.Set("uid", cmd.Uid) - - userId := cmd.UserId + dashFolder.OrgId = orgId + dashFolder.SetUid(strings.TrimSpace(cmd.Uid)) if userId == 0 { userId = -1 @@ -53,15 +50,17 @@ func (cmd *CreateFolderCommand) GetDashboardModel() *Dashboard { } // UpdateDashboardModel updates an existing model from command into model for update -func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard) { +func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard, orgId int64, userId int64) { + dashFolder.OrgId = orgId dashFolder.Title = strings.TrimSpace(cmd.Title) - dashFolder.Data.Set("title", cmd.Title) - dashFolder.Uid = dashFolder.Data.MustString("uid") - dashFolder.Data.Set("version", cmd.Version) - dashFolder.Version = cmd.Version - dashFolder.IsFolder = true + dashFolder.Data.Set("title", dashFolder.Title) - userId := cmd.UserId + if cmd.Uid != "" { + dashFolder.SetUid(cmd.Uid) + } + + dashFolder.SetVersion(cmd.Version) + dashFolder.IsFolder = true if userId == 0 { userId = -1 @@ -76,17 +75,16 @@ func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard) { // type CreateFolderCommand struct { - OrgId int64 `json:"-"` - UserId int64 `json:"userId"` - Uid string `json:"uid"` - Title string `json:"title"` + Uid string `json:"uid"` + Title string `json:"title"` + Version int `json:"version"` + Overwrite bool `json:"overwrite"` Result *Folder } type UpdateFolderCommand struct { - OrgId int64 `json:"-"` - UserId int64 `json:"userId"` + Uid string `json:"uid"` Title string `json:"title"` Version int `json:"version"` Overwrite bool `json:"overwrite"` diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index ffae62860a6..d10a44ac6a6 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -41,7 +41,10 @@ type SaveDashboardDTO struct { Dashboard *models.Dashboard } -type dashboardServiceImpl struct{} +type dashboardServiceImpl struct { + orgId int64 + user *models.SignedInUser +} func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { cmd := &models.GetProvisionedDashboardDataQuery{Name: name} @@ -53,7 +56,7 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod return cmd.Result, nil } -func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) { +func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) { dash := dto.Dashboard dash.Title = strings.TrimSpace(dash.Title) @@ -78,13 +81,15 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO) return nil, models.ErrDashboardUidToLong } - validateAlertsCmd := models.ValidateDashboardAlertsCommand{ - OrgId: dto.OrgId, - Dashboard: dash, - } + if validateAlerts { + validateAlertsCmd := models.ValidateDashboardAlertsCommand{ + OrgId: dto.OrgId, + Dashboard: dash, + } - if err := bus.Dispatch(&validateAlertsCmd); err != nil { - return nil, models.ErrDashboardContainsInvalidAlertData + if err := bus.Dispatch(&validateAlertsCmd); err != nil { + return nil, models.ErrDashboardContainsInvalidAlertData + } } validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{ @@ -141,7 +146,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, UserId: 0, OrgRole: models.ROLE_ADMIN, } - cmd, err := dr.buildSaveDashboardCommand(dto) + cmd, err := dr.buildSaveDashboardCommand(dto, true) if err != nil { return nil, err } @@ -171,7 +176,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash UserId: 0, OrgRole: models.ROLE_ADMIN, } - cmd, err := dr.buildSaveDashboardCommand(dto) + cmd, err := dr.buildSaveDashboardCommand(dto, false) if err != nil { return nil, err } @@ -190,7 +195,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash } func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) { - cmd, err := dr.buildSaveDashboardCommand(dto) + cmd, err := dr.buildSaveDashboardCommand(dto, true) if err != nil { return nil, err } diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go index 4a7dba762f6..e9d9af661f7 100644 --- a/pkg/services/dashboards/dashboard_service_test.go +++ b/pkg/services/dashboards/dashboard_service_test.go @@ -72,7 +72,7 @@ func TestDashboardService(t *testing.T) { dto.Dashboard.SetUid(tc.Uid) dto.User = &models.SignedInUser{} - _, err := service.buildSaveDashboardCommand(dto) + _, err := service.buildSaveDashboardCommand(dto, true) So(err, ShouldEqual, tc.Error) } }) diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go new file mode 100644 index 00000000000..8fc0eb38e71 --- /dev/null +++ b/pkg/services/dashboards/folder_service.go @@ -0,0 +1,246 @@ +package dashboards + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/search" +) + +// FolderService service for operating on folders +type FolderService interface { + GetFolders(limit int) ([]*models.Folder, error) + GetFolderById(id int64) (*models.Folder, error) + GetFolderByUid(uid string) (*models.Folder, error) + CreateFolder(cmd *models.CreateFolderCommand) error + UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error + DeleteFolder(uid string) (*models.Folder, error) +} + +// NewFolderService factory for creating a new folder service +var NewFolderService = func(orgId int64, user *models.SignedInUser) FolderService { + return &dashboardServiceImpl{ + orgId: orgId, + user: user, + } +} + +func (dr *dashboardServiceImpl) GetFolders(limit int) ([]*models.Folder, error) { + if limit == 0 { + limit = 1000 + } + + searchQuery := search.Query{ + SignedInUser: dr.user, + DashboardIds: make([]int64, 0), + FolderIds: make([]int64, 0), + Limit: limit, + OrgId: dr.orgId, + Type: "dash-folder", + Permission: models.PERMISSION_VIEW, + } + + if err := bus.Dispatch(&searchQuery); err != nil { + return nil, err + } + + folders := make([]*models.Folder, 0) + + for _, hit := range searchQuery.Result { + folders = append(folders, &models.Folder{ + Id: hit.Id, + Uid: hit.Uid, + Title: hit.Title, + }) + } + + return folders, nil +} + +func (dr *dashboardServiceImpl) GetFolderById(id int64) (*models.Folder, error) { + query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id} + dashFolder, err := getFolder(query) + + if err != nil { + return nil, toFolderError(err) + } + + g := guardian.New(dashFolder.Id, dr.orgId, dr.user) + if canView, err := g.CanView(); err != nil || !canView { + if err != nil { + return nil, toFolderError(err) + } + return nil, models.ErrFolderAccessDenied + } + + return dashToFolder(dashFolder), nil +} + +func (dr *dashboardServiceImpl) GetFolderByUid(uid string) (*models.Folder, error) { + query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid} + dashFolder, err := getFolder(query) + + if err != nil { + return nil, toFolderError(err) + } + + g := guardian.New(dashFolder.Id, dr.orgId, dr.user) + if canView, err := g.CanView(); err != nil || !canView { + if err != nil { + return nil, toFolderError(err) + } + return nil, models.ErrFolderAccessDenied + } + + return dashToFolder(dashFolder), nil +} + +func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) error { + dashFolder := cmd.GetDashboardModel(dr.orgId, dr.user.UserId) + + dto := &SaveDashboardDTO{ + Dashboard: dashFolder, + OrgId: dr.orgId, + User: dr.user, + Overwrite: cmd.Overwrite, + } + + saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false) + if err != nil { + return toFolderError(err) + } + + err = bus.Dispatch(saveDashboardCmd) + if err != nil { + return toFolderError(err) + } + + query := models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id} + dashFolder, err = getFolder(query) + if err != nil { + return toFolderError(err) + } + + cmd.Result = dashToFolder(dashFolder) + + return nil +} + +func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error { + query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: existingUid} + dashFolder, err := getFolder(query) + if err != nil { + return toFolderError(err) + } + + cmd.UpdateDashboardModel(dashFolder, dr.orgId, dr.user.UserId) + + dto := &SaveDashboardDTO{ + Dashboard: dashFolder, + OrgId: dr.orgId, + User: dr.user, + Overwrite: cmd.Overwrite, + } + + saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false) + if err != nil { + return toFolderError(err) + } + + err = bus.Dispatch(saveDashboardCmd) + if err != nil { + return toFolderError(err) + } + + query = models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id} + dashFolder, err = getFolder(query) + if err != nil { + return toFolderError(err) + } + + cmd.Result = dashToFolder(dashFolder) + + return nil +} + +func (dr *dashboardServiceImpl) DeleteFolder(uid string) (*models.Folder, error) { + query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid} + dashFolder, err := getFolder(query) + if err != nil { + return nil, toFolderError(err) + } + + guardian := guardian.New(dashFolder.Id, dr.orgId, dr.user) + if canSave, err := guardian.CanSave(); err != nil || !canSave { + if err != nil { + return nil, toFolderError(err) + } + return nil, models.ErrFolderAccessDenied + } + + deleteCmd := models.DeleteDashboardCommand{OrgId: dr.orgId, Id: dashFolder.Id} + if err := bus.Dispatch(&deleteCmd); err != nil { + return nil, toFolderError(err) + } + + return dashToFolder(dashFolder), nil +} + +func getFolder(query models.GetDashboardQuery) (*models.Dashboard, error) { + if err := bus.Dispatch(&query); err != nil { + return nil, toFolderError(err) + } + + if !query.Result.IsFolder { + return nil, models.ErrFolderNotFound + } + + return query.Result, nil +} + +func dashToFolder(dash *models.Dashboard) *models.Folder { + return &models.Folder{ + Id: dash.Id, + Uid: dash.Uid, + Title: dash.Title, + HasAcl: dash.HasAcl, + Url: dash.GetUrl(), + Version: dash.Version, + Created: dash.Created, + CreatedBy: dash.CreatedBy, + Updated: dash.Updated, + UpdatedBy: dash.UpdatedBy, + } +} + +func toFolderError(err error) error { + if err == models.ErrDashboardTitleEmpty { + return models.ErrFolderTitleEmpty + } + + if err == models.ErrDashboardUpdateAccessDenied { + return models.ErrFolderAccessDenied + } + + if err == models.ErrDashboardWithSameNameInFolderExists { + return models.ErrFolderSameNameExists + } + + if err == models.ErrDashboardWithSameUIDExists { + return models.ErrFolderWithSameUIDExists + } + + if err == models.ErrDashboardVersionMismatch { + return models.ErrFolderVersionMismatch + } + + if err == models.ErrDashboardNotFound { + return models.ErrFolderNotFound + } + + if err == models.ErrDashboardFailedGenerateUniqueUid { + err = models.ErrFolderFailedGenerateUniqueUid + } + + return err +} diff --git a/pkg/services/dashboards/folder_service_test.go b/pkg/services/dashboards/folder_service_test.go new file mode 100644 index 00000000000..89dcb6022f9 --- /dev/null +++ b/pkg/services/dashboards/folder_service_test.go @@ -0,0 +1,168 @@ +package dashboards + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + + "github.com/grafana/grafana/pkg/services/guardian" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestFolderService(t *testing.T) { + Convey("Folder service tests", t, func() { + service := dashboardServiceImpl{ + orgId: 1, + user: &models.SignedInUser{UserId: 1}, + } + + Convey("Given user has no permissions", func() { + origNewGuardian := guardian.New + mockDashboardGuardian(&fakeDashboardGuardian{}) + + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = models.NewDashboardFolder("Folder") + return nil + }) + + bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { + return nil + }) + + bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { + return models.ErrDashboardUpdateAccessDenied + }) + + Convey("When get folder by id should return access denied error", func() { + _, err := service.GetFolderById(1) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, models.ErrFolderAccessDenied) + }) + + Convey("When get folder by uid should return access denied error", func() { + _, err := service.GetFolderByUid("uid") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, models.ErrFolderAccessDenied) + }) + + Convey("When creating folder should return access denied error", func() { + err := service.CreateFolder(&models.CreateFolderCommand{ + Title: "Folder", + }) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, models.ErrFolderAccessDenied) + }) + + Convey("When updating folder should return access denied error", func() { + err := service.UpdateFolder("uid", &models.UpdateFolderCommand{ + Uid: "uid", + Title: "Folder", + }) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, models.ErrFolderAccessDenied) + }) + + Convey("When deleting folder by uid should return access denied error", func() { + _, err := service.DeleteFolder("uid") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, models.ErrFolderAccessDenied) + }) + + Reset(func() { + guardian.New = origNewGuardian + }) + }) + + Convey("Given user has permission to save", func() { + origNewGuardian := guardian.New + mockDashboardGuardian(&fakeDashboardGuardian{canSave: true}) + + dash := models.NewDashboardFolder("Folder") + dash.Id = 1 + + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = dash + return nil + }) + + bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { + return nil + }) + + bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { + return nil + }) + + bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error { + return nil + }) + + bus.AddHandler("test", func(cmd *models.SaveDashboardCommand) error { + cmd.Result = dash + return nil + }) + + bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error { + return nil + }) + + Convey("When creating folder should not return access denied error", func() { + err := service.CreateFolder(&models.CreateFolderCommand{ + Title: "Folder", + }) + So(err, ShouldBeNil) + }) + + Convey("When updating folder should not return access denied error", func() { + err := service.UpdateFolder("uid", &models.UpdateFolderCommand{ + Uid: "uid", + Title: "Folder", + }) + So(err, ShouldBeNil) + }) + + Convey("When deleting folder by uid should not return access denied error", func() { + _, err := service.DeleteFolder("uid") + So(err, ShouldBeNil) + }) + + Reset(func() { + guardian.New = origNewGuardian + }) + }) + + Convey("Given user has permission to view", func() { + origNewGuardian := guardian.New + mockDashboardGuardian(&fakeDashboardGuardian{canView: true}) + + dashFolder := models.NewDashboardFolder("Folder") + dashFolder.Id = 1 + dashFolder.Uid = "uid-abc" + + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = dashFolder + return nil + }) + + Convey("When get folder by id should return folder", func() { + f, _ := service.GetFolderById(1) + So(f.Id, ShouldEqual, dashFolder.Id) + So(f.Uid, ShouldEqual, dashFolder.Uid) + So(f.Title, ShouldEqual, dashFolder.Title) + }) + + Convey("When get folder by uid should not return access denied error", func() { + f, _ := service.GetFolderByUid("uid") + So(f.Id, ShouldEqual, dashFolder.Id) + So(f.Uid, ShouldEqual, dashFolder.Uid) + So(f.Title, ShouldEqual, dashFolder.Title) + }) + + Reset(func() { + guardian.New = origNewGuardian + }) + }) + }) +} From ea7998ca8ead90349eec820f13530f03f5608a65 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Feb 2018 13:57:32 +0100 Subject: [PATCH 09/26] folders: use new folder service in folder api routes --- pkg/api/api.go | 5 +- pkg/api/dtos/folder.go | 6 + pkg/api/folder.go | 159 +++---- pkg/api/folder_test.go | 962 ++++++++++++++++++++--------------------- 4 files changed, 541 insertions(+), 591 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 1b147249b7d..77143aed942 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -248,8 +248,9 @@ func (hs *HttpServer) registerRoutes() { // Folders apiRoute.Group("/folders", func(folderRoute RouteRegister) { - folderRoute.Get("/:uid", wrap(GetFolder)) - folderRoute.Get("/id/:id", wrap(GetFolder)) + folderRoute.Get("/", wrap(GetFolders)) + folderRoute.Get("/:uid", wrap(GetFolderByUid)) + folderRoute.Get("/id/:id", wrap(GetFolderById)) folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder)) folderRoute.Put("/:uid", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) folderRoute.Delete("/:uid", wrap(DeleteFolder)) diff --git a/pkg/api/dtos/folder.go b/pkg/api/dtos/folder.go index 159d41fee1c..469656c6f8f 100644 --- a/pkg/api/dtos/folder.go +++ b/pkg/api/dtos/folder.go @@ -17,3 +17,9 @@ type Folder struct { Updated time.Time `json:"updated"` Version int `json:"version"` } + +type FolderSearchHit struct { + Id int64 `json:"id"` + Uid string `json:"uid"` + Title string `json:"title"` +} diff --git a/pkg/api/folder.go b/pkg/api/folder.go index d9bc0f5eb49..41df36eb6dc 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -1,10 +1,7 @@ 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" @@ -12,142 +9,84 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func getFolderHelper(orgId int64, id int64, uid string) (*m.Dashboard, Response) { - query := m.GetDashboardQuery{OrgId: orgId, Id: id, Uid: uid} - if err := bus.Dispatch(&query); err != nil { - if err == m.ErrDashboardNotFound { - err = m.ErrFolderNotFound - } +func GetFolders(c *middleware.Context) Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + folders, err := s.GetFolders(c.QueryInt("limit")) - 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 toFolderError(err) } - return ApiError(403, "Access denied to this folder", nil) + result := make([]dtos.FolderSearchHit, 0) + + for _, f := range folders { + result = append(result, dtos.FolderSearchHit{ + Id: f.Id, + Uid: f.Uid, + Title: f.Title, + }) + } + + return Json(200, result) } -func GetFolder(c *middleware.Context) Response { - folder, rsp := getFolderHelper(c.OrgId, c.ParamsInt64(":id"), c.Params(":uid")) - if rsp != nil { - return rsp +func GetFolderByUid(c *middleware.Context) Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + folder, err := s.GetFolderByUid(c.Params(":uid")) + + if err != nil { + return toFolderError(err) } - guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser) - if canView, err := guardian.CanView(); err != nil || !canView { - fmt.Printf("%v", err) - return folderGuardianResponse(err) + g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) + return Json(200, toFolderDto(g, folder)) +} + +func GetFolderById(c *middleware.Context) Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + folder, err := s.GetFolderById(c.ParamsInt64(":id")) + if err != nil { + return toFolderError(err) } - return Json(200, toFolderDto(&guardian, folder)) + g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) + return Json(200, toFolderDto(g, folder)) } func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { - cmd.OrgId = c.OrgId - cmd.UserId = c.UserId - - dashFolder := cmd.GetDashboardModel() - - guardian := guardian.New(0, c.OrgId, c.SignedInUser) - if canSave, err := guardian.CanSave(); err != nil || !canSave { - return folderGuardianResponse(err) - } - - 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) - } - - saveDashboardDto := &dashboards.SaveDashboardDTO{ - Dashboard: dashFolder, - OrgId: c.OrgId, - UserId: c.UserId, - } - - folder, err := dashboards.GetRepository().SaveDashboard(saveDashboardDto) - + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + err := s.CreateFolder(&cmd) if err != nil { return toFolderError(err) } - return Json(200, toFolderDto(&guardian, folder)) + g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser) + return Json(200, toFolderDto(g, cmd.Result)) } func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { - cmd.OrgId = c.OrgId - cmd.UserId = c.UserId - uid := c.Params(":uid") - - dashFolder, rsp := getFolderHelper(c.OrgId, 0, uid) - if rsp != nil { - return rsp - } - - guardian := guardian.New(dashFolder.Id, c.OrgId, c.SignedInUser) - if canSave, err := guardian.CanSave(); err != nil || !canSave { - return folderGuardianResponse(err) - } - - cmd.UpdateDashboardModel(dashFolder) - - if dashFolder.Title == "" { - return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil) - } - - saveDashboardDto := &dashboards.SaveDashboardDTO{ - Dashboard: dashFolder, - OrgId: c.OrgId, - UserId: c.UserId, - Overwrite: cmd.Overwrite, - } - - folder, err := dashboards.GetRepository().SaveDashboard(saveDashboardDto) - + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + err := s.UpdateFolder(c.Params(":uid"), &cmd) if err != nil { return toFolderError(err) } - return Json(200, toFolderDto(&guardian, folder)) + g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser) + return Json(200, toFolderDto(g, cmd.Result)) } func DeleteFolder(c *middleware.Context) Response { - dashFolder, rsp := getFolderHelper(c.OrgId, 0, c.Params(":uid")) - if rsp != nil { - return rsp + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + f, err := s.DeleteFolder(c.Params(":uid")) + if err != nil { + return toFolderError(err) } - guardian := guardian.New(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} + var resp = map[string]interface{}{"title": f.Title} return Json(200, resp) } -func toFolderDto(g *guardian.DashboardGuardian, folder *m.Dashboard) dtos.Folder { +func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder { canEdit, _ := g.CanEdit() canSave, _ := g.CanSave() canAdmin, _ := g.CanAdmin() @@ -165,7 +104,7 @@ func toFolderDto(g *guardian.DashboardGuardian, folder *m.Dashboard) dtos.Folder Id: folder.Id, Uid: folder.Uid, Title: folder.Title, - Url: folder.GetUrl(), + Url: folder.Url, HasAcl: folder.HasAcl, CanSave: canSave, CanEdit: canEdit, @@ -183,6 +122,10 @@ func toFolderError(err error) Response { return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil) } + if err == m.ErrFolderAccessDenied { + return ApiError(403, "Access denied", err) + } + if err == m.ErrDashboardWithSameNameInFolderExists { return Json(412, util.DynMap{"status": "name-exists", "message": m.ErrFolderSameNameExists.Error()}) } diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 6ef18e9a749..bdf8e89a198 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -1,484 +1,484 @@ 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 - - var getDashboardQueries []*m.GetDashboardQuery - - bus.AddHandler("test", func(query *m.GetDashboardQuery) error { - query.Result = fakeDash - getDashboardQueries = append(getDashboardQueries, query) - 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - callGetFolder(sc) - So(sc.resp.Code, ShouldEqual, 404) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { - callGetFolder(sc) - So(sc.resp.Code, ShouldEqual, 404) - - Convey("Should lookup folder by id", func() { - So(getDashboardQueries[0].Id, ShouldEqual, 1) - }) - }) - - updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { - callUpdateFolder(sc) - So(sc.resp.Code, ShouldEqual, 404) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - callDeleteFolder(sc) - So(sc.resp.Code, ShouldEqual, 404) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - }) - }) - - Convey("Given a folder which does not have an acl", t, func() { - fakeFolder := m.NewDashboardFolder("Folder") - fakeFolder.Id = 1 - fakeFolder.HasAcl = false - - var getDashboardQueries []*m.GetDashboardQuery - - bus.AddHandler("test", func(query *m.GetDashboardQuery) error { - query.Result = fakeFolder - getDashboardQueries = append(getDashboardQueries, query) - 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, - } - - updateFolderCmd := m.UpdateFolderCommand{ - Title: fakeFolder.Title, - } - - Convey("When user is an Org Viewer", func() { - role := m.ROLE_VIEWER - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - folder := getFolderShouldReturn200(sc) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - - 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 GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { - folder := getFolderShouldReturn200(sc) - - Convey("Should lookup folder by id", func() { - So(getDashboardQueries[0].Id, ShouldEqual, 1) - }) - - 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - callDeleteFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - - createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { - callCreateFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) - - updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { - callUpdateFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - }) - - Convey("When user is an Org Editor", func() { - role := m.ROLE_EDITOR - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - folder := getFolderShouldReturn200(sc) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - - 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 GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { - folder := getFolderShouldReturn200(sc) - - Convey("Should lookup folder by id", func() { - So(getDashboardQueries[0].Id, ShouldEqual, 1) - }) - - 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - callDeleteFolder(sc) - So(sc.resp.Code, ShouldEqual, 200) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - - createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { - callCreateFolder(sc) - So(sc.resp.Code, ShouldEqual, 200) - }) - - updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { - callUpdateFolder(sc) - So(sc.resp.Code, ShouldEqual, 200) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - }) - }) - - Convey("Given a folder which have an acl", t, func() { - fakeFolder := m.NewDashboardFolder("Folder") - fakeFolder.Id = 1 - fakeFolder.HasAcl = true - - var getDashboardQueries []*m.GetDashboardQuery - - bus.AddHandler("test", func(query *m.GetDashboardQuery) error { - query.Result = fakeFolder - getDashboardQueries = append(getDashboardQueries, query) - 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, - } - - updateFolderCmd := m.UpdateFolderCommand{ - 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - callGetFolder(sc) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) - }) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { - callGetFolder(sc) - - Convey("Should lookup folder by id", func() { - So(getDashboardQueries[0].Id, ShouldEqual, 1) - }) - - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - callDeleteFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - - createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { - callCreateFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) - - updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { - callUpdateFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - }) - - 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - callGetFolder(sc) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) - }) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { - callGetFolder(sc) - - Convey("Should lookup folder by id", func() { - So(getDashboardQueries[0].Id, ShouldEqual, 1) - }) - - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { - callDeleteFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - - createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { - callCreateFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) - - updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { - callUpdateFolder(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup folder by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "uid") - }) - }) - }) - }) -} - -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 = GetFolder - 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{})) +// 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 + +// var getDashboardQueries []*m.GetDashboardQuery + +// bus.AddHandler("test", func(query *m.GetDashboardQuery) error { +// query.Result = fakeDash +// getDashboardQueries = append(getDashboardQueries, query) +// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// callGetFolder(sc) +// So(sc.resp.Code, ShouldEqual, 404) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) + +// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { +// callGetFolder(sc) +// So(sc.resp.Code, ShouldEqual, 404) + +// Convey("Should lookup folder by id", func() { +// So(getDashboardQueries[0].Id, ShouldEqual, 1) +// }) +// }) + +// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { +// callUpdateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 404) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) + +// loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// callDeleteFolder(sc) +// So(sc.resp.Code, ShouldEqual, 404) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) +// }) +// }) + +// Convey("Given a folder which does not have an acl", t, func() { +// fakeFolder := m.NewDashboardFolder("Folder") +// fakeFolder.Id = 1 +// fakeFolder.HasAcl = false + +// var getDashboardQueries []*m.GetDashboardQuery + +// bus.AddHandler("test", func(query *m.GetDashboardQuery) error { +// query.Result = fakeFolder +// getDashboardQueries = append(getDashboardQueries, query) +// 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, +// } + +// updateFolderCmd := m.UpdateFolderCommand{ +// Title: fakeFolder.Title, +// } + +// Convey("When user is an Org Viewer", func() { +// role := m.ROLE_VIEWER + +// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// folder := getFolderShouldReturn200(sc) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) + +// 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 GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { +// folder := getFolderShouldReturn200(sc) + +// Convey("Should lookup folder by id", func() { +// So(getDashboardQueries[0].Id, ShouldEqual, 1) +// }) + +// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// callDeleteFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) + +// createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { +// callCreateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) +// }) + +// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { +// callUpdateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) +// }) + +// Convey("When user is an Org Editor", func() { +// role := m.ROLE_EDITOR + +// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// folder := getFolderShouldReturn200(sc) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) + +// 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 GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { +// folder := getFolderShouldReturn200(sc) + +// Convey("Should lookup folder by id", func() { +// So(getDashboardQueries[0].Id, ShouldEqual, 1) +// }) + +// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// callDeleteFolder(sc) +// So(sc.resp.Code, ShouldEqual, 200) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) + +// createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { +// callCreateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 200) +// }) + +// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { +// callUpdateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 200) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) +// }) +// }) + +// Convey("Given a folder which have an acl", t, func() { +// fakeFolder := m.NewDashboardFolder("Folder") +// fakeFolder.Id = 1 +// fakeFolder.HasAcl = true + +// var getDashboardQueries []*m.GetDashboardQuery + +// bus.AddHandler("test", func(query *m.GetDashboardQuery) error { +// query.Result = fakeFolder +// getDashboardQueries = append(getDashboardQueries, query) +// 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, +// } + +// updateFolderCmd := m.UpdateFolderCommand{ +// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// callGetFolder(sc) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) + +// Convey("Should be denied access", func() { +// So(sc.resp.Code, ShouldEqual, 403) +// }) +// }) + +// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { +// callGetFolder(sc) + +// Convey("Should lookup folder by id", func() { +// So(getDashboardQueries[0].Id, ShouldEqual, 1) +// }) + +// Convey("Should be denied access", func() { +// So(sc.resp.Code, ShouldEqual, 403) +// }) +// }) + +// loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// callDeleteFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) + +// createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { +// callCreateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) +// }) + +// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { +// callUpdateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) +// }) + +// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// callGetFolder(sc) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) + +// Convey("Should be denied access", func() { +// So(sc.resp.Code, ShouldEqual, 403) +// }) +// }) + +// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { +// callGetFolder(sc) + +// Convey("Should lookup folder by id", func() { +// So(getDashboardQueries[0].Id, ShouldEqual, 1) +// }) + +// Convey("Should be denied access", func() { +// So(sc.resp.Code, ShouldEqual, 403) +// }) +// }) + +// loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { +// callDeleteFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) + +// createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { +// callCreateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) +// }) + +// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { +// callUpdateFolder(sc) +// So(sc.resp.Code, ShouldEqual, 403) + +// Convey("Should lookup folder by uid", func() { +// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") +// }) +// }) +// }) +// }) +// } + +// 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 = GetFolder +// 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) - }) -} +// 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) +// }) +// } From b56e2a61f9fe86a61a393c1dd1e2487dfaf0b80d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Feb 2018 14:00:32 +0100 Subject: [PATCH 10/26] folders: basic integration tests for folders --- tests/api/folder.test.ts | 78 ++++++++++++++++++++++++++++++++++++++++ tests/api/setup.ts | 18 +++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/api/folder.test.ts diff --git a/tests/api/folder.test.ts b/tests/api/folder.test.ts new file mode 100644 index 00000000000..0d40972aa6d --- /dev/null +++ b/tests/api/folder.test.ts @@ -0,0 +1,78 @@ +import client from './client'; +import * as setup from './setup'; + +describe('/api/folders', () => { + let state: any = {}; + + beforeAll(async () => { + state = await setup.ensureState({ + orgName: 'api-test-org', + users: [ + { user: setup.admin, role: 'Admin' }, + { user: setup.editor, role: 'Editor' }, + { user: setup.viewer, role: 'Viewer' }, + ], + admin: setup.admin, + folders: [ + { + title: 'Folder 1', + uid: 'f-01', + }, + { + title: 'Folder 2', + uid: 'f-02', + }, + { + title: 'Folder 3', + uid: 'f-03', + }, + ], + }); + }); + + describe('With admin user', () => { + it('can delete folder', async () => { + let rsp = await client.callAs(setup.admin).delete(`/api/folders/f-01`); + expect(rsp.data.title).toBe('Folder 1'); + }); + + it('can update folder', async () => { + let rsp = await client.callAs(setup.admin).put(`/api/folders/f-02`, { + uid: 'f-02', + title: 'Folder 2 upd', + overwrite: true, + }); + expect(rsp.data.title).toBe('Folder 2 upd'); + }); + + it('can update folder uid', async () => { + let rsp = await client.callAs(setup.admin).put(`/api/folders/f-03`, { + uid: 'f-03-upd', + title: 'Folder 3 upd', + overwrite: true, + }); + expect(rsp.data.uid).toBe('f-03-upd'); + expect(rsp.data.title).toBe('Folder 3 upd'); + }); + }); + + describe('With viewer user', () => { + it('Cannot delete folder', async () => { + let rsp = await setup.expectError(() => { + return client.callAs(setup.viewer).delete(`/api/folders/f-02`); + }); + expect(rsp.response.status).toBe(403); + }); + + it('Cannot update folder', async () => { + let rsp = await setup.expectError(() => { + return client.callAs(setup.viewer).put(`/api/folders/f-02`, { + uid: 'f-02', + title: 'Folder 2 upd', + overwrite: true, + }); + }); + expect(rsp.response.status).toBe(403); + }); + }); +}); diff --git a/tests/api/setup.ts b/tests/api/setup.ts index 0566729999c..94b0b20b8b8 100644 --- a/tests/api/setup.ts +++ b/tests/api/setup.ts @@ -90,6 +90,18 @@ export async function createDashboard(user, dashboard) { return dashboard; } +export async function createFolder(user, folder) { + const rsp = await client.callAs(user).post(`/api/folders`, { + uid: folder.uid, + title: folder.title, + overwrite: true, + }); + folder.id = rsp.id; + folder.url = rsp.url; + + return folder; +} + export async function ensureState(state) { const org = await getOrg(state.orgName); @@ -99,9 +111,13 @@ export async function ensureState(state) { await setUsingOrg(user, org); } - for (let dashboard of state.dashboards) { + for (let dashboard of state.dashboards || []) { await createDashboard(state.admin, dashboard); } + for (let folder of state.folders || []) { + await createFolder(state.admin, folder); + } + return state; } From 717d8d0c45e296a6d5dd399cd095a60f3df6a150 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Feb 2018 15:01:20 +0100 Subject: [PATCH 11/26] folders: fix api error mapping --- pkg/api/folder.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 41df36eb6dc..0c4f82aa896 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -138,7 +138,7 @@ func toFolderError(err error) Response { return Json(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()}) } - if err == m.ErrDashboardNotFound { + if err == m.ErrFolderNotFound { return Json(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()}) } @@ -146,5 +146,5 @@ func toFolderError(err error) Response { err = m.ErrFolderFailedGenerateUniqueUid } - return ApiError(500, "Failed to create folder", err) + return ApiError(500, "Folder API error", err) } From 892bdecb1969aa8a86dde5ecbe1dd569c19bfe57 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Feb 2018 15:25:16 +0100 Subject: [PATCH 12/26] folders: folder permission api routes --- pkg/api/api.go | 14 ++++-- pkg/api/folder_acl.go | 98 +++++++++++++++++++++++++++++++++++++ pkg/models/dashboard_acl.go | 5 +- 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 pkg/api/folder_acl.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 77143aed942..24cf2832af0 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -249,11 +249,19 @@ func (hs *HttpServer) registerRoutes() { // Folders apiRoute.Group("/folders", func(folderRoute RouteRegister) { folderRoute.Get("/", wrap(GetFolders)) - folderRoute.Get("/:uid", wrap(GetFolderByUid)) folderRoute.Get("/id/:id", wrap(GetFolderById)) folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder)) - folderRoute.Put("/:uid", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) - folderRoute.Delete("/:uid", wrap(DeleteFolder)) + + folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) { + folderUidRoute.Get("/", wrap(GetFolderByUid)) + folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) + folderUidRoute.Delete("/", wrap(DeleteFolder)) + + folderUidRoute.Group("/permissions", func(folderAclRoute RouteRegister) { + folderAclRoute.Get("/", wrap(GetFolderPermissionList)) + folderAclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions)) + }) + }) }) // Dashboard diff --git a/pkg/api/folder_acl.go b/pkg/api/folder_acl.go new file mode 100644 index 00000000000..5d2c28fe769 --- /dev/null +++ b/pkg/api/folder_acl.go @@ -0,0 +1,98 @@ +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/dashboards" + "github.com/grafana/grafana/pkg/services/guardian" +) + +func GetFolderPermissionList(c *middleware.Context) Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + folder, err := s.GetFolderByUid(c.Params(":uid")) + + if err != nil { + return toFolderError(err) + } + + guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser) + + if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + return toFolderError(m.ErrFolderAccessDenied) + } + + acl, err := guardian.GetAcl() + if err != nil { + return ApiError(500, "Failed to get folder permissions", err) + } + + for _, perm := range acl { + perm.FolderId = folder.Id + perm.DashboardId = 0 + + if perm.Slug != "" { + perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug) + } + } + + return Json(200, acl) +} + +func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + folder, err := s.GetFolderByUid(c.Params(":uid")) + + if err != nil { + return toFolderError(err) + } + + guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser) + if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + return toFolderError(err) + } + + cmd := m.UpdateDashboardAclCommand{} + cmd.DashboardId = folder.Id + + for _, item := range apiCmd.Items { + cmd.Items = append(cmd.Items, &m.DashboardAcl{ + OrgId: c.OrgId, + DashboardId: folder.Id, + UserId: item.UserId, + TeamId: item.TeamId, + Role: item.Role, + Permission: item.Permission, + Created: time.Now(), + Updated: time.Now(), + }) + } + + if okToUpdate, err := guardian.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { + if err != nil { + return ApiError(500, "Error while checking folder permissions", err) + } + + return ApiError(403, "Cannot remove own admin permission for a folder", nil) + } + + if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrDashboardAclInfoMissing { + err = m.ErrFolderAclInfoMissing + } + if err == m.ErrDashboardPermissionDashboardEmpty { + err = m.ErrFolderPermissionFolderEmpty + } + + if err == m.ErrFolderAclInfoMissing || err == m.ErrFolderPermissionFolderEmpty { + return ApiError(409, err.Error(), err) + } + + return ApiError(500, "Failed to create permission", err) + } + + return ApiSuccess("Folder acl updated") +} diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 202b519207d..1fbd2b451b9 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -26,6 +26,8 @@ func (p PermissionType) String() string { 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.") + ErrFolderAclInfoMissing = errors.New("User id and team id cannot both be empty for a folder permission.") + ErrFolderPermissionFolderEmpty = errors.New("Folder Id must be greater than zero for a folder permission.") ) // Dashboard ACL model @@ -45,7 +47,8 @@ type DashboardAcl struct { type DashboardAclInfoDTO struct { OrgId int64 `json:"-"` - DashboardId int64 `json:"dashboardId"` + DashboardId int64 `json:"dashboardId,omitempty"` + FolderId int64 `json:"folderId,omitempty"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` From 02278f90a710900e28564ebf2791ac10abbc8d8b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Feb 2018 18:08:19 +0100 Subject: [PATCH 13/26] dashboards: make fake dashboard guardian available to other packages --- .../dashboards/dashboard_service_test.go | 51 +--------- .../dashboards/folder_service_test.go | 8 +- pkg/services/guardian/guardian.go | 49 ++++++++++ .../dashboard_service_integration_test.go | 96 +++++-------------- 4 files changed, 76 insertions(+), 128 deletions(-) diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go index e9d9af661f7..965b10655b3 100644 --- a/pkg/services/dashboards/dashboard_service_test.go +++ b/pkg/services/dashboards/dashboard_service_test.go @@ -17,7 +17,7 @@ func TestDashboardService(t *testing.T) { service := dashboardServiceImpl{} origNewDashboardGuardian := guardian.New - mockDashboardGuardian(&fakeDashboardGuardian{canSave: true}) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) Convey("Save dashboard validation", func() { dto := &SaveDashboardDTO{} @@ -93,52 +93,3 @@ func TestDashboardService(t *testing.T) { }) }) } - -func mockDashboardGuardian(mock *fakeDashboardGuardian) { - guardian.New = func(dashId int64, orgId int64, user *models.SignedInUser) guardian.DashboardGuardian { - mock.orgId = orgId - mock.dashId = dashId - mock.user = user - return mock - } -} - -type fakeDashboardGuardian struct { - dashId int64 - orgId int64 - user *models.SignedInUser - canSave bool - canEdit bool - canView bool - canAdmin bool - hasPermission bool - checkPermissionBeforeUpdate bool -} - -func (g *fakeDashboardGuardian) CanSave() (bool, error) { - return g.canSave, nil -} - -func (g *fakeDashboardGuardian) CanEdit() (bool, error) { - return g.canEdit, nil -} - -func (g *fakeDashboardGuardian) CanView() (bool, error) { - return g.canView, nil -} - -func (g *fakeDashboardGuardian) CanAdmin() (bool, error) { - return g.canAdmin, nil -} - -func (g *fakeDashboardGuardian) HasPermission(permission models.PermissionType) (bool, error) { - return g.hasPermission, nil -} - -func (g *fakeDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) { - return g.checkPermissionBeforeUpdate, nil -} - -func (g *fakeDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO, error) { - return nil, nil -} diff --git a/pkg/services/dashboards/folder_service_test.go b/pkg/services/dashboards/folder_service_test.go index 89dcb6022f9..ad91060b296 100644 --- a/pkg/services/dashboards/folder_service_test.go +++ b/pkg/services/dashboards/folder_service_test.go @@ -20,7 +20,7 @@ func TestFolderService(t *testing.T) { Convey("Given user has no permissions", func() { origNewGuardian := guardian.New - mockDashboardGuardian(&fakeDashboardGuardian{}) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{}) bus.AddHandler("test", func(query *models.GetDashboardQuery) error { query.Result = models.NewDashboardFolder("Folder") @@ -77,7 +77,7 @@ func TestFolderService(t *testing.T) { Convey("Given user has permission to save", func() { origNewGuardian := guardian.New - mockDashboardGuardian(&fakeDashboardGuardian{canSave: true}) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) dash := models.NewDashboardFolder("Folder") dash.Id = 1 @@ -135,7 +135,7 @@ func TestFolderService(t *testing.T) { Convey("Given user has permission to view", func() { origNewGuardian := guardian.New - mockDashboardGuardian(&fakeDashboardGuardian{canView: true}) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true}) dashFolder := models.NewDashboardFolder("Folder") dashFolder.Id = 1 @@ -153,7 +153,7 @@ func TestFolderService(t *testing.T) { So(f.Title, ShouldEqual, dashFolder.Title) }) - Convey("When get folder by uid should not return access denied error", func() { + Convey("When get folder by uid should return folder", func() { f, _ := service.GetFolderByUid("uid") So(f.Id, ShouldEqual, dashFolder.Id) So(f.Uid, ShouldEqual, dashFolder.Uid) diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 98db5449182..23d43a53f35 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -158,3 +158,52 @@ func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) { g.groups = query.Result return query.Result, err } + +type FakeDashboardGuardian struct { + DashId int64 + OrgId int64 + User *m.SignedInUser + CanSaveValue bool + CanEditValue bool + CanViewValue bool + CanAdminValue bool + HasPermissionValue bool + CheckPermissionBeforeUpdateValue bool +} + +func (g *FakeDashboardGuardian) CanSave() (bool, error) { + return g.CanSaveValue, nil +} + +func (g *FakeDashboardGuardian) CanEdit() (bool, error) { + return g.CanEditValue, nil +} + +func (g *FakeDashboardGuardian) CanView() (bool, error) { + return g.CanViewValue, nil +} + +func (g *FakeDashboardGuardian) CanAdmin() (bool, error) { + return g.CanAdminValue, nil +} + +func (g *FakeDashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) { + return g.HasPermissionValue, nil +} + +func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) { + return g.CheckPermissionBeforeUpdateValue, nil +} + +func (g *FakeDashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) { + return nil, nil +} + +func MockDashboardGuardian(mock *FakeDashboardGuardian) { + New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardian { + mock.OrgId = orgId + mock.DashId = dashId + mock.User = user + return mock + } +} diff --git a/pkg/services/sqlstore/dashboard_service_integration_test.go b/pkg/services/sqlstore/dashboard_service_integration_test.go index c1dd5cf3673..d005270c33c 100644 --- a/pkg/services/sqlstore/dashboard_service_integration_test.go +++ b/pkg/services/sqlstore/dashboard_service_integration_test.go @@ -142,9 +142,9 @@ func TestIntegratedDashboardService(t *testing.T) { So(err, ShouldNotBeNil) So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied) - So(sc.dashboardGuardianMock.dashId, ShouldEqual, 0) - So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId) - So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId) + So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0) + So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId) + So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId) }) }) @@ -165,9 +165,9 @@ func TestIntegratedDashboardService(t *testing.T) { So(err, ShouldNotBeNil) So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied) - So(sc.dashboardGuardianMock.dashId, ShouldEqual, otherSavedFolder.Id) - So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId) - So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId) + So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id) + So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId) + So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId) }) }) @@ -189,9 +189,9 @@ func TestIntegratedDashboardService(t *testing.T) { So(err, ShouldNotBeNil) So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied) - So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInGeneralFolder.Id) - So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId) - So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId) + So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedDashInGeneralFolder.Id) + So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId) + So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId) }) }) @@ -213,9 +213,9 @@ func TestIntegratedDashboardService(t *testing.T) { So(err, ShouldNotBeNil) So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied) - So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInFolder.Id) - So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId) - So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId) + So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedDashInFolder.Id) + So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId) + So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId) }) }) }) @@ -785,68 +785,16 @@ func TestIntegratedDashboardService(t *testing.T) { }) } -func mockDashboardGuardian(mock *mockDashboardGuarder) { - guardian.New = func(dashId int64, orgId int64, user *models.SignedInUser) guardian.DashboardGuardian { - mock.orgId = orgId - mock.dashId = dashId - mock.user = user - return mock - } -} - -type mockDashboardGuarder struct { - dashId int64 - orgId int64 - user *models.SignedInUser - canSave bool - canSaveCallCounter int - canEdit bool - canView bool - canAdmin bool - hasPermission bool - checkPermissionBeforeRemove bool - checkPermissionBeforeUpdate bool -} - -func (g *mockDashboardGuarder) CanSave() (bool, error) { - g.canSaveCallCounter++ - return g.canSave, nil -} - -func (g *mockDashboardGuarder) CanEdit() (bool, error) { - return g.canEdit, nil -} - -func (g *mockDashboardGuarder) CanView() (bool, error) { - return g.canView, nil -} - -func (g *mockDashboardGuarder) CanAdmin() (bool, error) { - return g.canAdmin, nil -} - -func (g *mockDashboardGuarder) HasPermission(permission models.PermissionType) (bool, error) { - return g.hasPermission, nil -} - -func (g *mockDashboardGuarder) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) { - return g.checkPermissionBeforeUpdate, nil -} - -func (g *mockDashboardGuarder) GetAcl() ([]*models.DashboardAclInfoDTO, error) { - return nil, nil -} - type scenarioContext struct { - dashboardGuardianMock *mockDashboardGuarder + dashboardGuardianMock *guardian.FakeDashboardGuardian } type scenarioFunc func(c *scenarioContext) -func dashboardGuardianScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) { +func dashboardGuardianScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) { Convey(desc, func() { origNewDashboardGuardian := guardian.New - mockDashboardGuardian(mock) + guardian.MockDashboardGuardian(mock) sc := &scenarioContext{ dashboardGuardianMock: mock, @@ -861,15 +809,15 @@ func dashboardGuardianScenario(desc string, mock *mockDashboardGuarder, fn scena } type dashboardPermissionScenarioContext struct { - dashboardGuardianMock *mockDashboardGuarder + dashboardGuardianMock *guardian.FakeDashboardGuardian } type dashboardPermissionScenarioFunc func(sc *dashboardPermissionScenarioContext) -func dashboardPermissionScenario(desc string, mock *mockDashboardGuarder, fn dashboardPermissionScenarioFunc) { +func dashboardPermissionScenario(desc string, mock *guardian.FakeDashboardGuardian, fn dashboardPermissionScenarioFunc) { Convey(desc, func() { origNewDashboardGuardian := guardian.New - mockDashboardGuardian(mock) + guardian.MockDashboardGuardian(mock) sc := &dashboardPermissionScenarioContext{ dashboardGuardianMock: mock, @@ -884,8 +832,8 @@ func dashboardPermissionScenario(desc string, mock *mockDashboardGuarder, fn das } func permissionScenario(desc string, canSave bool, fn dashboardPermissionScenarioFunc) { - mock := &mockDashboardGuarder{ - canSave: canSave, + mock := &guardian.FakeDashboardGuardian{ + CanSaveValue: canSave, } dashboardPermissionScenario(desc, mock, fn) } @@ -902,10 +850,10 @@ func callSaveWithError(cmd models.SaveDashboardCommand) error { return err } -func dashboardServiceScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) { +func dashboardServiceScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) { Convey(desc, func() { origNewDashboardGuardian := guardian.New - mockDashboardGuardian(mock) + guardian.MockDashboardGuardian(mock) sc := &scenarioContext{ dashboardGuardianMock: mock, From 417541c77f96e278fed34e1e11e82f3e453d7267 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Feb 2018 18:11:50 +0100 Subject: [PATCH 14/26] folders: folder permissions api tests --- pkg/api/folder_acl.go | 7 ++- pkg/api/folder_acl_test.go | 125 +++++++++++++++++++++++++++++++++++++ pkg/api/folder_test.go | 52 +++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 pkg/api/folder_acl_test.go diff --git a/pkg/api/folder_acl.go b/pkg/api/folder_acl.go index 5d2c28fe769..58702dce08e 100644 --- a/pkg/api/folder_acl.go +++ b/pkg/api/folder_acl.go @@ -51,10 +51,15 @@ func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardA } guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser) - if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + canAdmin, err := guardian.CanAdmin() + if err != nil { return toFolderError(err) } + if !canAdmin { + return toFolderError(m.ErrFolderAccessDenied) + } + cmd := m.UpdateDashboardAclCommand{} cmd.DashboardId = folder.Id diff --git a/pkg/api/folder_acl_test.go b/pkg/api/folder_acl_test.go new file mode 100644 index 00000000000..5ec8be9a2cb --- /dev/null +++ b/pkg/api/folder_acl_test.go @@ -0,0 +1,125 @@ +package api + +import ( + "testing" + + "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/smartystreets/goconvey/convey" +) + +func TestFolderAclApiEndpoint(t *testing.T) { + Convey("Folder permissions test", t, func() { + Convey("Given user has no admin permissions", func() { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) + + mock := &fakeFolderService{ + GetFolderByUidResult: &m.Folder{ + Id: 1, + Uid: "uid", + Title: "Folder", + }, + } + + origNewFolderService := dashboards.NewFolderService + mockFolderService(mock) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) { + callGetFolderPermissions(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: m.PERMISSION_ADMIN}, + }, + } + + updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { + callUpdateFolderPermissions(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + Reset(func() { + guardian.New = origNewGuardian + dashboards.NewFolderService = origNewFolderService + }) + }) + + Convey("Given user has admin permissions and permissions to update", func() { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: true, CheckPermissionBeforeUpdateValue: true}) + + mock := &fakeFolderService{ + GetFolderByUidResult: &m.Folder{ + Id: 1, + Uid: "uid", + Title: "Folder", + }, + } + + origNewFolderService := dashboards.NewFolderService + mockFolderService(mock) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) { + callGetFolderPermissions(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: m.PERMISSION_ADMIN}, + }, + } + + updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { + callUpdateFolderPermissions(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + Reset(func() { + guardian.New = origNewGuardian + dashboards.NewFolderService = origNewFolderService + }) + }) + }) +} + +func callGetFolderPermissions(sc *scenarioContext) { + sc.handlerFunc = GetFolderPermissionList + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() +} + +func callUpdateFolderPermissions(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error { + return nil + }) + + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() +} + +func updateFolderPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := setupScenarioContext(url) + + sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.context = c + sc.context.OrgId = TestOrgID + sc.context.UserId = TestUserID + + return UpdateFolderPermissions(c, cmd) + }) + + sc.m.Post(routePattern, sc.defaultHandler) + + fn(sc) + }) +} diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index bdf8e89a198..618d46c4ab5 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -1,5 +1,10 @@ package api +import ( + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" +) + // import ( // "encoding/json" // "path/filepath" @@ -482,3 +487,50 @@ package api // fn(sc) // }) // } + +type fakeFolderService struct { + GetFoldersResult []*models.Folder + GetFoldersError error + GetFolderByUidResult *models.Folder + GetFolderByUidError error + GetFolderByIdResult *models.Folder + GetFolderByIdError error + CreateFolderError error + UpdateFolderError error + DeleteFolderResult *models.Folder + DeleteFolderError error + CreatedFolders []*models.CreateFolderCommand + UpdatedFolders []*models.UpdateFolderCommand + DeletedFolderUids []string +} + +func (s *fakeFolderService) GetFolders(limit int) ([]*models.Folder, error) { + return s.GetFoldersResult, s.GetFoldersError +} + +func (s *fakeFolderService) GetFolderById(id int64) (*models.Folder, error) { + return s.GetFolderByIdResult, s.GetFolderByIdError +} + +func (s *fakeFolderService) GetFolderByUid(uid string) (*models.Folder, error) { + return s.GetFolderByUidResult, s.GetFolderByUidError +} + +func (s *fakeFolderService) CreateFolder(cmd *models.CreateFolderCommand) error { + return s.CreateFolderError +} + +func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error { + return s.UpdateFolderError +} + +func (s *fakeFolderService) DeleteFolder(uid string) (*models.Folder, error) { + s.DeletedFolderUids = append(s.DeletedFolderUids, uid) + return s.DeleteFolderResult, s.DeleteFolderError +} + +func mockFolderService(mock *fakeFolderService) { + dashboards.NewFolderService = func(orgId int64, user *models.SignedInUser) dashboards.FolderService { + return mock + } +} From a167e19b371ebe15c043b438ec2c8e0665a915c2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:24:54 +0100 Subject: [PATCH 15/26] folders: folder api tests --- pkg/api/folder.go | 25 +- pkg/api/folder_test.go | 686 +++++++--------------- pkg/models/folders.go | 6 +- pkg/services/dashboards/folder_service.go | 3 +- 4 files changed, 214 insertions(+), 506 deletions(-) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 0c4f82aa896..7ba5f3fdcc3 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -118,32 +118,25 @@ func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder { } func toFolderError(err error) Response { - if err == m.ErrDashboardTitleEmpty { - return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil) + if err == m.ErrFolderTitleEmpty || + err == m.ErrFolderSameNameExists || + err == m.ErrFolderWithSameUIDExists || + err == m.ErrDashboardTypeMismatch || + err == m.ErrDashboardInvalidUid || + err == m.ErrDashboardUidToLong { + return ApiError(400, err.Error(), nil) } if err == m.ErrFolderAccessDenied { return ApiError(403, "Access denied", err) } - if err == m.ErrDashboardWithSameNameInFolderExists { - return Json(412, util.DynMap{"status": "name-exists", "message": m.ErrFolderSameNameExists.Error()}) - } - - if err == m.ErrDashboardWithSameUIDExists { - return Json(412, util.DynMap{"status": "uid-exists", "message": m.ErrFolderWithSameUIDExists.Error()}) - } - - if err == m.ErrDashboardVersionMismatch { - return Json(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()}) - } - if err == m.ErrFolderNotFound { return Json(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()}) } - if err == m.ErrDashboardFailedGenerateUniqueUid { - err = m.ErrFolderFailedGenerateUniqueUid + if err == m.ErrFolderVersionMismatch { + return Json(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()}) } return ApiError(500, "Folder API error", err) diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 618d46c4ab5..0ab3cc7d7c9 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -1,492 +1,208 @@ package api import ( + "encoding/json" + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + + m "github.com/grafana/grafana/pkg/models" + + . "github.com/smartystreets/goconvey/convey" ) -// 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 - -// var getDashboardQueries []*m.GetDashboardQuery - -// bus.AddHandler("test", func(query *m.GetDashboardQuery) error { -// query.Result = fakeDash -// getDashboardQueries = append(getDashboardQueries, query) -// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// callGetFolder(sc) -// So(sc.resp.Code, ShouldEqual, 404) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) - -// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { -// callGetFolder(sc) -// So(sc.resp.Code, ShouldEqual, 404) - -// Convey("Should lookup folder by id", func() { -// So(getDashboardQueries[0].Id, ShouldEqual, 1) -// }) -// }) - -// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { -// callUpdateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 404) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) - -// loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// callDeleteFolder(sc) -// So(sc.resp.Code, ShouldEqual, 404) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) -// }) -// }) - -// Convey("Given a folder which does not have an acl", t, func() { -// fakeFolder := m.NewDashboardFolder("Folder") -// fakeFolder.Id = 1 -// fakeFolder.HasAcl = false - -// var getDashboardQueries []*m.GetDashboardQuery - -// bus.AddHandler("test", func(query *m.GetDashboardQuery) error { -// query.Result = fakeFolder -// getDashboardQueries = append(getDashboardQueries, query) -// 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, -// } - -// updateFolderCmd := m.UpdateFolderCommand{ -// Title: fakeFolder.Title, -// } - -// Convey("When user is an Org Viewer", func() { -// role := m.ROLE_VIEWER - -// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// folder := getFolderShouldReturn200(sc) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) - -// 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 GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { -// folder := getFolderShouldReturn200(sc) - -// Convey("Should lookup folder by id", func() { -// So(getDashboardQueries[0].Id, ShouldEqual, 1) -// }) - -// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// callDeleteFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) - -// createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { -// callCreateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) -// }) - -// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { -// callUpdateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) -// }) - -// Convey("When user is an Org Editor", func() { -// role := m.ROLE_EDITOR - -// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// folder := getFolderShouldReturn200(sc) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) - -// 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 GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { -// folder := getFolderShouldReturn200(sc) - -// Convey("Should lookup folder by id", func() { -// So(getDashboardQueries[0].Id, ShouldEqual, 1) -// }) - -// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// callDeleteFolder(sc) -// So(sc.resp.Code, ShouldEqual, 200) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) - -// createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { -// callCreateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 200) -// }) - -// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { -// callUpdateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 200) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) -// }) -// }) - -// Convey("Given a folder which have an acl", t, func() { -// fakeFolder := m.NewDashboardFolder("Folder") -// fakeFolder.Id = 1 -// fakeFolder.HasAcl = true - -// var getDashboardQueries []*m.GetDashboardQuery - -// bus.AddHandler("test", func(query *m.GetDashboardQuery) error { -// query.Result = fakeFolder -// getDashboardQueries = append(getDashboardQueries, query) -// 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, -// } - -// updateFolderCmd := m.UpdateFolderCommand{ -// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// callGetFolder(sc) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) - -// Convey("Should be denied access", func() { -// So(sc.resp.Code, ShouldEqual, 403) -// }) -// }) - -// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { -// callGetFolder(sc) - -// Convey("Should lookup folder by id", func() { -// So(getDashboardQueries[0].Id, ShouldEqual, 1) -// }) - -// Convey("Should be denied access", func() { -// So(sc.resp.Code, ShouldEqual, 403) -// }) -// }) - -// loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// callDeleteFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) - -// createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { -// callCreateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) -// }) - -// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { -// callUpdateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) -// }) - -// 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/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// callGetFolder(sc) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) - -// Convey("Should be denied access", func() { -// So(sc.resp.Code, ShouldEqual, 403) -// }) -// }) - -// loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/1", "/api/folders/:id", role, func(sc *scenarioContext) { -// callGetFolder(sc) - -// Convey("Should lookup folder by id", func() { -// So(getDashboardQueries[0].Id, ShouldEqual, 1) -// }) - -// Convey("Should be denied access", func() { -// So(sc.resp.Code, ShouldEqual, 403) -// }) -// }) - -// loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/folders/uid", "/api/folders/:uid", role, func(sc *scenarioContext) { -// callDeleteFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) - -// createFolderScenario("When calling POST on", "/api/folders", "/api/folders", role, cmd, func(sc *scenarioContext) { -// callCreateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) -// }) - -// updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", role, updateFolderCmd, func(sc *scenarioContext) { -// callUpdateFolder(sc) -// So(sc.resp.Code, ShouldEqual, 403) - -// Convey("Should lookup folder by uid", func() { -// So(getDashboardQueries[0].Uid, ShouldEqual, "uid") -// }) -// }) -// }) -// }) -// } - -// 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 = GetFolder -// 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) -// }) -// } +func TestFoldersApiEndpoint(t *testing.T) { + Convey("Create/update folder response tests", t, func() { + Convey("Given a correct request for creating a folder", func() { + cmd := m.CreateFolderCommand{ + Uid: "uid", + Title: "Folder", + } + + mock := &fakeFolderService{ + CreateFolderResult: &m.Folder{Id: 1, Uid: "uid", Title: "Folder"}, + } + + createFolderScenario("When calling POST on", "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) { + callCreateFolder(sc) + + Convey("It should return correct response data", func() { + folder := dtos.Folder{} + err := json.NewDecoder(sc.resp.Body).Decode(&folder) + So(err, ShouldBeNil) + So(folder.Id, ShouldEqual, 1) + So(folder.Uid, ShouldEqual, "uid") + So(folder.Title, ShouldEqual, "Folder") + }) + }) + }) + + Convey("Given incorrect requests for creating a folder", func() { + testCases := []struct { + Error error + ExpectedStatusCode int + }{ + {Error: m.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400}, + {Error: m.ErrFolderTitleEmpty, ExpectedStatusCode: 400}, + {Error: m.ErrFolderSameNameExists, ExpectedStatusCode: 400}, + {Error: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400}, + {Error: m.ErrDashboardUidToLong, ExpectedStatusCode: 400}, + {Error: m.ErrFolderAccessDenied, ExpectedStatusCode: 403}, + {Error: m.ErrFolderNotFound, ExpectedStatusCode: 404}, + {Error: m.ErrFolderVersionMismatch, ExpectedStatusCode: 412}, + {Error: m.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500}, + } + + cmd := m.CreateFolderCommand{ + Uid: "uid", + Title: "Folder", + } + + for _, tc := range testCases { + mock := &fakeFolderService{ + CreateFolderError: tc.Error, + } + + createFolderScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()), "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) { + callCreateFolder(sc) + if sc.resp.Code != tc.ExpectedStatusCode { + t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code) + } + }) + } + }) + + Convey("Given a correct request for updating a folder", func() { + cmd := m.UpdateFolderCommand{ + Title: "Folder upd", + } + + mock := &fakeFolderService{ + UpdateFolderResult: &m.Folder{Id: 1, Uid: "uid", Title: "Folder upd"}, + } + + updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + + Convey("It should return correct response data", func() { + folder := dtos.Folder{} + err := json.NewDecoder(sc.resp.Body).Decode(&folder) + So(err, ShouldBeNil) + So(folder.Id, ShouldEqual, 1) + So(folder.Uid, ShouldEqual, "uid") + So(folder.Title, ShouldEqual, "Folder upd") + }) + }) + }) + + Convey("Given incorrect requests for updating a folder", func() { + testCases := []struct { + Error error + ExpectedStatusCode int + }{ + {Error: m.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400}, + {Error: m.ErrFolderTitleEmpty, ExpectedStatusCode: 400}, + {Error: m.ErrFolderSameNameExists, ExpectedStatusCode: 400}, + {Error: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400}, + {Error: m.ErrDashboardUidToLong, ExpectedStatusCode: 400}, + {Error: m.ErrFolderAccessDenied, ExpectedStatusCode: 403}, + {Error: m.ErrFolderNotFound, ExpectedStatusCode: 404}, + {Error: m.ErrFolderVersionMismatch, ExpectedStatusCode: 412}, + {Error: m.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500}, + } + + cmd := m.UpdateFolderCommand{ + Title: "Folder upd", + } + + for _, tc := range testCases { + mock := &fakeFolderService{ + UpdateFolderError: tc.Error, + } + + updateFolderScenario(fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()), "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) { + callUpdateFolder(sc) + if sc.resp.Code != tc.ExpectedStatusCode { + t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code) + } + }) + } + }) + }) +} + +func callGetFolderByUid(sc *scenarioContext) { + sc.handlerFunc = GetFolderByUid + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() +} + +func callDeleteFolder(sc *scenarioContext) { + sc.handlerFunc = DeleteFolder + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() +} + +func callCreateFolder(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() +} + +func createFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd m.CreateFolderCommand, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := setupScenarioContext(url) + sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.context = c + sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID} + + return CreateFolder(c, cmd) + }) + + origNewFolderService := dashboards.NewFolderService + mockFolderService(mock) + + sc.m.Post(routePattern, sc.defaultHandler) + + defer func() { + dashboards.NewFolderService = origNewFolderService + }() + + fn(sc) + }) +} + +func callUpdateFolder(sc *scenarioContext) { + sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() +} + +func updateFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd m.UpdateFolderCommand, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := setupScenarioContext(url) + sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.context = c + sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID} + + return UpdateFolder(c, cmd) + }) + + origNewFolderService := dashboards.NewFolderService + mockFolderService(mock) + + sc.m.Put(routePattern, sc.defaultHandler) + + defer func() { + dashboards.NewFolderService = origNewFolderService + }() + + fn(sc) + }) +} type fakeFolderService struct { GetFoldersResult []*models.Folder @@ -495,12 +211,12 @@ type fakeFolderService struct { GetFolderByUidError error GetFolderByIdResult *models.Folder GetFolderByIdError error + CreateFolderResult *models.Folder CreateFolderError error + UpdateFolderResult *models.Folder UpdateFolderError error DeleteFolderResult *models.Folder DeleteFolderError error - CreatedFolders []*models.CreateFolderCommand - UpdatedFolders []*models.UpdateFolderCommand DeletedFolderUids []string } @@ -517,10 +233,12 @@ func (s *fakeFolderService) GetFolderByUid(uid string) (*models.Folder, error) { } func (s *fakeFolderService) CreateFolder(cmd *models.CreateFolderCommand) error { + cmd.Result = s.CreateFolderResult return s.CreateFolderError } func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error { + cmd.Result = s.UpdateFolderResult return s.UpdateFolderError } diff --git a/pkg/models/folders.go b/pkg/models/folders.go index 43c7f1a9165..c61620a11fc 100644 --- a/pkg/models/folders.go +++ b/pkg/models/folders.go @@ -75,10 +75,8 @@ func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard, orgI // type CreateFolderCommand struct { - Uid string `json:"uid"` - Title string `json:"title"` - Version int `json:"version"` - Overwrite bool `json:"overwrite"` + Uid string `json:"uid"` + Title string `json:"title"` Result *Folder } diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go index 8fc0eb38e71..61f86484dc6 100644 --- a/pkg/services/dashboards/folder_service.go +++ b/pkg/services/dashboards/folder_service.go @@ -102,7 +102,6 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er Dashboard: dashFolder, OrgId: dr.orgId, User: dr.user, - Overwrite: cmd.Overwrite, } saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false) @@ -222,7 +221,7 @@ func toFolderError(err error) error { return models.ErrFolderAccessDenied } - if err == models.ErrDashboardWithSameNameInFolderExists { + if err == models.ErrDashbloardWithSameNameInFolderExists { return models.ErrFolderSameNameExists } From 78283b9c7759adae0d9461c9b4a684613baee385 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:25:21 +0100 Subject: [PATCH 16/26] folders: extend folder service tests --- .../dashboards/folder_service_test.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pkg/services/dashboards/folder_service_test.go b/pkg/services/dashboards/folder_service_test.go index ad91060b296..6357e84805a 100644 --- a/pkg/services/dashboards/folder_service_test.go +++ b/pkg/services/dashboards/folder_service_test.go @@ -164,5 +164,28 @@ func TestFolderService(t *testing.T) { guardian.New = origNewGuardian }) }) + + Convey("Should map errors correct", func() { + testCases := []struct { + ActualError error + ExpectedError error + }{ + {ActualError: models.ErrDashboardTitleEmpty, ExpectedError: models.ErrFolderTitleEmpty}, + {ActualError: models.ErrDashboardUpdateAccessDenied, ExpectedError: models.ErrFolderAccessDenied}, + {ActualError: models.ErrDashboardWithSameNameInFolderExists, ExpectedError: models.ErrFolderSameNameExists}, + {ActualError: models.ErrDashboardWithSameUIDExists, ExpectedError: models.ErrFolderWithSameUIDExists}, + {ActualError: models.ErrDashboardVersionMismatch, ExpectedError: models.ErrFolderVersionMismatch}, + {ActualError: models.ErrDashboardNotFound, ExpectedError: models.ErrFolderNotFound}, + {ActualError: models.ErrDashboardFailedGenerateUniqueUid, ExpectedError: models.ErrFolderFailedGenerateUniqueUid}, + {ActualError: models.ErrDashboardInvalidUid, ExpectedError: models.ErrDashboardInvalidUid}, + } + + for _, tc := range testCases { + actualError := toFolderError(tc.ActualError) + if actualError != tc.ExpectedError { + t.Errorf("For error '%s' expected error '%s', actual '%s'", tc.ActualError, tc.ExpectedError, actualError) + } + } + }) }) } From 8a8f3bd5bbba9940016ed9baca93c2f5e4a5ff88 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:28:11 +0100 Subject: [PATCH 17/26] folders: fix typo --- pkg/services/dashboards/folder_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go index 61f86484dc6..66afa6306fb 100644 --- a/pkg/services/dashboards/folder_service.go +++ b/pkg/services/dashboards/folder_service.go @@ -221,7 +221,7 @@ func toFolderError(err error) error { return models.ErrFolderAccessDenied } - if err == models.ErrDashbloardWithSameNameInFolderExists { + if err == models.ErrDashboardWithSameNameInFolderExists { return models.ErrFolderSameNameExists } From d68099bc4afe0c88c9fc0d09d100e46438320b8c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:42:54 +0100 Subject: [PATCH 18/26] dashboards: change api route for dashboard permissions From /api/dashboards/id/:id/acl to /api/dashboards/id/:id/permissions --- pkg/api/api.go | 12 ++++++------ ...{dashboard_acl.go => dashboard_permission.go} | 4 ++-- ..._acl_test.go => dashboard_permission_test.go} | 16 ++++++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) rename pkg/api/{dashboard_acl.go => dashboard_permission.go} (92%) rename pkg/api/{dashboard_acl_test.go => dashboard_permission_test.go} (94%) diff --git a/pkg/api/api.go b/pkg/api/api.go index 24cf2832af0..84fd6f8f659 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -257,9 +257,9 @@ func (hs *HttpServer) registerRoutes() { folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) folderUidRoute.Delete("/", wrap(DeleteFolder)) - folderUidRoute.Group("/permissions", func(folderAclRoute RouteRegister) { - folderAclRoute.Get("/", wrap(GetFolderPermissionList)) - folderAclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions)) + folderUidRoute.Group("/permissions", func(folderPermissionRoute RouteRegister) { + folderPermissionRoute.Get("/", wrap(GetFolderPermissionList)) + folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions)) }) }) }) @@ -284,9 +284,9 @@ func (hs *HttpServer) registerRoutes() { dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion)) dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion)) - dashIdRoute.Group("/acl", func(aclRoute RouteRegister) { - aclRoute.Get("/", wrap(GetDashboardAclList)) - aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl)) + dashIdRoute.Group("/permissions", func(dashboardPermissionRoute RouteRegister) { + dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList)) + dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardPermissions)) }) }) }) diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_permission.go similarity index 92% rename from pkg/api/dashboard_acl.go rename to pkg/api/dashboard_permission.go index 13b29db78e6..18520fff262 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_permission.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" ) -func GetDashboardAclList(c *middleware.Context) Response { +func GetDashboardPermissionList(c *middleware.Context) Response { dashId := c.ParamsInt64(":dashboardId") _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") @@ -38,7 +38,7 @@ func GetDashboardAclList(c *middleware.Context) Response { return Json(200, acl) } -func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response { +func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response { dashId := c.ParamsInt64(":dashboardId") _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_permission_test.go similarity index 94% rename from pkg/api/dashboard_acl_test.go rename to pkg/api/dashboard_permission_test.go index d6b7e305daf..c29cb7688bf 100644 --- a/pkg/api/dashboard_acl_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -12,8 +12,8 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -func TestDashboardAclApiEndpoint(t *testing.T) { - Convey("Given a dashboard acl", t, func() { +func TestDashboardPermissionApiEndpoint(t *testing.T) { + Convey("Given a dashboard with permissions", t, func() { mockResult := []*m.DashboardAclInfoDTO{ {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW}, {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, @@ -56,7 +56,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { 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.handlerFunc = GetDashboardPermissionList sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 200) @@ -71,7 +71,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) { getDashboardNotFoundError = m.ErrDashboardNotFound - sc.handlerFunc = GetDashboardAclList + sc.handlerFunc = GetDashboardPermissionList sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should not be able to access ACL", func() { @@ -99,7 +99,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) Convey("Should be able to access ACL", func() { - sc.handlerFunc = GetDashboardAclList + sc.handlerFunc = GetDashboardPermissionList sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 200) @@ -145,7 +145,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { // Getting the permissions is an Admin permission Convey("Should not be able to get list of permissions from ACL", func() { - sc.handlerFunc = GetDashboardAclList + sc.handlerFunc = GetDashboardPermissionList sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 403) @@ -157,7 +157,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { 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.handlerFunc = GetDashboardPermissionList sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 403) @@ -204,7 +204,7 @@ func postAclScenario(desc string, url string, routePattern string, role m.RoleTy sc.context.OrgId = TestOrgID sc.context.OrgRole = role - return UpdateDashboardAcl(c, cmd) + return UpdateDashboardPermissions(c, cmd) }) sc.m.Post(routePattern, sc.defaultHandler) From 3df8ae2306a084ff7d40dd84fe8f3d56e3def256 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:45:19 +0100 Subject: [PATCH 19/26] folders: rename folder_acl in api to folder_permission --- pkg/api/{folder_acl.go => folder_permission.go} | 0 pkg/api/{folder_acl_test.go => folder_permission_test.go} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/api/{folder_acl.go => folder_permission.go} (100%) rename pkg/api/{folder_acl_test.go => folder_permission_test.go} (98%) diff --git a/pkg/api/folder_acl.go b/pkg/api/folder_permission.go similarity index 100% rename from pkg/api/folder_acl.go rename to pkg/api/folder_permission.go diff --git a/pkg/api/folder_acl_test.go b/pkg/api/folder_permission_test.go similarity index 98% rename from pkg/api/folder_acl_test.go rename to pkg/api/folder_permission_test.go index 5ec8be9a2cb..bbae5390b80 100644 --- a/pkg/api/folder_acl_test.go +++ b/pkg/api/folder_permission_test.go @@ -13,7 +13,7 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -func TestFolderAclApiEndpoint(t *testing.T) { +func TestFolderPermissionApiEndpoint(t *testing.T) { Convey("Folder permissions test", t, func() { Convey("Given user has no admin permissions", func() { origNewGuardian := guardian.New From 9889c920833568ea4d3603eb00280bf9e093ff1c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:48:22 +0100 Subject: [PATCH 20/26] fix typos in api, acl to permissions --- pkg/api/dashboard_permission.go | 4 ++-- pkg/api/dashboard_permission_test.go | 16 ++++++++-------- pkg/api/folder_permission.go | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 18520fff262..351b81885d6 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -26,7 +26,7 @@ func GetDashboardPermissionList(c *middleware.Context) Response { acl, err := guardian.GetAcl() if err != nil { - return ApiError(500, "Failed to get dashboard acl", err) + return ApiError(500, "Failed to get dashboard permissions", err) } for _, perm := range acl { @@ -82,5 +82,5 @@ func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboa return ApiError(500, "Failed to create permission", err) } - return ApiSuccess("Dashboard acl updated") + return ApiSuccess("Dashboard permissions updated") } diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index c29cb7688bf..fd399cfb096 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -54,7 +54,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { // 4. user is an org editor AND has no permissions for the dashboard 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardsId/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) { Convey("Should be able to access ACL", func() { sc.handlerFunc = GetDashboardPermissionList sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() @@ -69,7 +69,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) { getDashboardNotFoundError = m.ErrDashboardNotFound sc.handlerFunc = GetDashboardPermissionList sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() @@ -86,7 +86,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, } - postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, cmd, func(sc *scenarioContext) { + postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_ADMIN, cmd, func(sc *scenarioContext) { getDashboardNotFoundError = m.ErrDashboardNotFound CallPostAcl(sc) So(sc.resp.Code, ShouldEqual, 404) @@ -95,7 +95,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }) Convey("When user is org 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) { mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) Convey("Should be able to access ACL", func() { @@ -113,7 +113,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, } - postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) { + postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) { mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) CallPostAcl(sc) @@ -129,7 +129,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, } - postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) { + postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) { mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) CallPostAcl(sc) @@ -140,7 +140,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }) Convey("When user is org viewer 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_VIEWER, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_VIEWER, func(sc *scenarioContext) { mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) // Getting the permissions is an Admin permission @@ -154,7 +154,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }) Convey("When user is org 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) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardsId/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) { Convey("Should not be able to access ACL", func() { sc.handlerFunc = GetDashboardPermissionList diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 58702dce08e..7453552d092 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -99,5 +99,5 @@ func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardA return ApiError(500, "Failed to create permission", err) } - return ApiSuccess("Folder acl updated") + return ApiSuccess("Folder permissions updated") } From 71c97417642ef528b6609540a148e417ae6885e4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:53:02 +0100 Subject: [PATCH 21/26] permissions: use updated api endpoint for dashboard permissions --- .../app/core/components/Permissions/AddPermissions.jest.tsx | 4 ++-- public/app/stores/PermissionsStore/PermissionsStore.jest.ts | 4 ++-- public/app/stores/PermissionsStore/PermissionsStore.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.jest.tsx index 48ff20a16aa..9c01eee70b1 100644 --- a/public/app/core/components/Permissions/AddPermissions.jest.tsx +++ b/public/app/core/components/Permissions/AddPermissions.jest.tsx @@ -53,7 +53,7 @@ describe('AddPermissions', () => { wrapper.find('form').simulate('submit', { preventDefault() {} }); expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); + expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); }); }); @@ -80,7 +80,7 @@ describe('AddPermissions', () => { wrapper.find('form').simulate('submit', { preventDefault() {} }); expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); + expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); }); }); diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index 97a9906d0e5..f7332516bef 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -44,7 +44,7 @@ describe('PermissionsStore', () => { expect(store.items[0].permission).toBe(2); expect(store.items[0].permissionName).toBe('Edit'); expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); + expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); }); it('should save removed permissions automatically', () => { @@ -54,7 +54,7 @@ describe('PermissionsStore', () => { expect(store.items.length).toBe(2); expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); + expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); }); describe('when duplicate team permissions are added', () => { diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index a7c90d13da0..88b26a2886f 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -110,7 +110,7 @@ export const PermissionsStore = types self.dashboardId = dashboardId; self.items.clear(); - const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`); + const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/permissions`); const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot); self.items = items; self.originalItems = items; @@ -210,7 +210,7 @@ const updateItems = self => { let res; try { - res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/acl`, { + res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, { items: updated, }); } catch (error) { From 0135178645dbbfe967638f356739bacb28468179 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 15:05:33 +0100 Subject: [PATCH 22/26] folders: fix create folder in folder picker --- .../app/features/dashboard/folder_picker/folder_picker.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts index 0e5c22c4db2..cbf23e3ea4b 100644 --- a/public/app/features/dashboard/folder_picker/folder_picker.ts +++ b/public/app/features/dashboard/folder_picker/folder_picker.ts @@ -89,13 +89,13 @@ export class FolderPickerCtrl { evt.preventDefault(); } - return this.backendSrv.createDashboardFolder(this.newFolderName).then(result => { + return this.backendSrv.createFolder({ title: this.newFolderName }).then(result => { appEvents.emit('alert-success', ['Folder Created', 'OK']); this.closeCreateFolder(); this.folder = { - text: result.dashboard.title, - value: result.dashboard.id, + text: result.title, + value: result.id, }; this.onFolderChange(this.folder); }); From 3f3175972e1a1a18b6c9dcc4a7bb4ebb618b717d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 16:38:09 +0100 Subject: [PATCH 23/26] dashboards: fix batch dashboard/folder delete response Use message returned from backend and use that as alert success response to user --- pkg/api/dashboard.go | 12 ++++-- pkg/api/folder.go | 8 +++- .../manage_dashboards/manage_dashboards.ts | 36 +----------------- public/app/core/services/backend_srv.ts | 38 +++++-------------- 4 files changed, 24 insertions(+), 70 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 29097153ea3..88502804919 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -167,8 +167,10 @@ func DeleteDashboard(c *middleware.Context) Response { return ApiError(500, "Failed to delete dashboard", err) } - var resp = map[string]interface{}{"title": dash.Title} - return Json(200, resp) + return Json(200, util.DynMap{ + "title": dash.Title, + "message": fmt.Sprintf("Dashboard %s deleted", dash.Title), + }) } func DeleteDashboardByUid(c *middleware.Context) Response { @@ -187,8 +189,10 @@ func DeleteDashboardByUid(c *middleware.Context) Response { return ApiError(500, "Failed to delete dashboard", err) } - var resp = map[string]interface{}{"title": dash.Title} - return Json(200, resp) + return Json(200, util.DynMap{ + "title": dash.Title, + "message": fmt.Sprintf("Dashboard %s deleted", dash.Title), + }) } func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 7ba5f3fdcc3..e3c4f127569 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -1,6 +1,8 @@ package api import ( + "fmt" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -82,8 +84,10 @@ func DeleteFolder(c *middleware.Context) Response { return toFolderError(err) } - var resp = map[string]interface{}{"title": f.Title} - return Json(200, resp) + return Json(200, util.DynMap{ + "title": f.Title, + "message": fmt.Sprintf("Folder %s deleted", f.Title), + }) } func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder { diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 3bbedfdc127..99907350275 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -179,41 +179,7 @@ export class ManageDashboardsCtrl { } private deleteFoldersAndDashboards(folderUids, dashboardUids) { - this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(result => { - const folders = _.filter(result, dash => !dash.dashboard); - const folderCount = folders.length; - const dashboards = _.filter(result, dash => dash.dashboard); - const dashCount = dashboards.length; - - if (result.length > 0) { - let header; - let msg; - - if (folderCount > 0 && dashCount > 0) { - header = `Folder${folderCount === 1 ? '' : 's'} And Dashboard${dashCount === 1 ? '' : 's'} Deleted`; - msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} `; - msg += `and ${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`; - } else if (folderCount > 0) { - header = `Folder${folderCount === 1 ? '' : 's'} Deleted`; - - if (folderCount === 1) { - msg = `${folders[0].title} has been deleted`; - } else { - msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} has been deleted`; - } - } else if (dashCount > 0) { - header = `Dashboard${dashCount === 1 ? '' : 's'} Deleted`; - - if (dashCount === 1) { - msg = `${dashboards[0].dashboard.title} has been deleted`; - } else { - msg = `${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`; - } - } - - appEvents.emit('alert-success', [header, msg]); - } - + this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => { this.refreshList(); }); } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index b9712d330d9..837a7af6764 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -258,47 +258,27 @@ export class BackendSrv { }); } - deleteFolder(uid: string) { - let deferred = this.$q.defer(); - - this.getFolderByUid(uid).then(folder => { - this.delete(`/api/folders/${uid}`) - .then(() => { - deferred.resolve(folder); - }) - .catch(err => { - deferred.reject(err); - }); - }); - - return deferred.promise; + deleteFolder(uid: string, showSuccessAlert) { + return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true }); } - deleteDashboard(uid) { - let deferred = this.$q.defer(); - - this.getDashboardByUid(uid).then(fullDash => { - this.delete(`/api/dashboards/uid/${uid}`) - .then(() => { - deferred.resolve(fullDash); - }) - .catch(err => { - deferred.reject(err); - }); + deleteDashboard(uid, showSuccessAlert) { + return this.request({ + method: 'DELETE', + url: `/api/dashboards/uid/${uid}`, + showSuccessAlert: showSuccessAlert === true, }); - - return deferred.promise; } deleteFoldersAndDashboards(folderUids, dashboardUids) { const tasks = []; for (let folderUid of folderUids) { - tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid)); + tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true)); } for (let dashboardUid of dashboardUids) { - tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid)); + tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true)); } return this.executeInOrder(tasks, []); From b7cf6f9781e6c6b01a4d738eca8da9c59c8b3f76 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 16:55:35 +0100 Subject: [PATCH 24/26] folders: use folder api for retrieving folder --- .../core/components/manage_dashboards/manage_dashboards.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 99907350275..545119a80d7 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -78,8 +78,8 @@ export class ManageDashboardsCtrl { return; } - return this.backendSrv.getDashboardByUid(this.folderUid).then(dash => { - this.canSave = dash.meta.canSave; + return this.backendSrv.getFolderByUid(this.folderUid).then(folder => { + this.canSave = folder.canSave; }); }); } From 20ebb614f6e3de87f6a9629a22bd6dedc6cb899d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 16:56:34 +0100 Subject: [PATCH 25/26] dashboards: cleanup Move api calls to backend_srv --- public/app/core/services/backend_srv.ts | 4 ++-- public/app/features/panel/solo_panel_ctrl.ts | 2 +- public/app/routes/dashboard_loaders.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 837a7af6764..8b7ca518e8b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -221,8 +221,8 @@ export class BackendSrv { return this.get('/api/search', query); } - getDashboard(type, slug) { - return this.get('/api/dashboards/' + type + '/' + slug); + getDashboardBySlug(slug) { + return this.get(`/api/dashboards/db/${slug}`); } getDashboardByUid(uid: string) { diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index 2c7698db08e..242d2e7da3e 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -18,7 +18,7 @@ export class SoloPanelCtrl { // if no uid, redirect to new route based on slug if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { - backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { + backendSrv.getDashboardBySlug($routeParams.slug).then(res => { if (res) { const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); $location.path(url).replace(); diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index d84f03ed38f..9224ec33bcc 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -21,7 +21,7 @@ export class LoadDashboardCtrl { // if no uid, redirect to new route based on slug if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { - backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { + backendSrv.getDashboardBySlug($routeParams.slug).then(res => { if (res) { $location.path(locationUtil.stripBaseFromUrl(res.meta.url)).replace(); } From 3f588b4c4835a7f65242a2943972e4d0a398db1d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Feb 2018 11:54:28 +0100 Subject: [PATCH 26/26] dashboards: created/updated and createdby/updatedby should be set before save If creating a new folder/dashboard created/updated and createdby/updatedby should always be set. If updating an existing folder/dashboard updated and updatedby should always be set. --- pkg/models/dashboards.go | 4 ---- pkg/services/sqlstore/dashboard.go | 20 +++++++++++++++----- pkg/services/sqlstore/dashboard_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 3ac91e2bb09..4b771038df6 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -166,10 +166,6 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { userId = -1 } - if dash.Data.Get("version").MustInt(0) == 0 { - dash.CreatedBy = userId - } - dash.UpdatedBy = userId dash.OrgId = cmd.OrgId dash.PluginId = cmd.PluginId diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 5ae1b626554..5ee34183628 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -37,6 +37,12 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { dash := cmd.GetDashboardModel() + userId := cmd.UserId + + if userId == 0 { + userId = -1 + } + if dash.Id > 0 { var existing m.Dashboard dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) @@ -60,8 +66,6 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { if existing.PluginId != "" && cmd.Overwrite == false { return m.UpdatePluginDashboardError{PluginId: existing.PluginId} } - } else { - dash.Created = time.Now() } if dash.Uid == "" { @@ -78,17 +82,23 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { if dash.Id == 0 { dash.SetVersion(1) + dash.Created = time.Now() + dash.CreatedBy = userId + dash.Updated = time.Now() + dash.UpdatedBy = userId metrics.M_Api_Dashboard_Insert.Inc() affectedRows, err = sess.Insert(dash) } else { - v := dash.Version - v++ - dash.SetVersion(v) + dash.SetVersion(dash.Version + 1) if !cmd.UpdatedAt.IsZero() { dash.Updated = cmd.UpdatedAt + } else { + dash.Updated = time.Now() } + dash.UpdatedBy = userId + affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash) } diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 51a2d4eb64e..e0a73b9a49a 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -3,6 +3,7 @@ package sqlstore import ( "fmt" "testing" + "time" "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/components/simplejson" @@ -124,6 +125,24 @@ func TestDashboardDataAccess(t *testing.T) { generateNewUid = util.GenerateShortUid }) + Convey("Should be able to create dashboard", func() { + cmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "folderId", + "tags": []interface{}{}, + }), + UserId: 100, + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + So(cmd.Result.CreatedBy, ShouldEqual, 100) + So(cmd.Result.Created.IsZero(), ShouldBeFalse) + So(cmd.Result.UpdatedBy, ShouldEqual, 100) + So(cmd.Result.Updated.IsZero(), ShouldBeFalse) + }) + Convey("Should be able to update dashboard by id and remove folderId", func() { cmd := m.SaveDashboardCommand{ OrgId: 1, @@ -134,6 +153,7 @@ func TestDashboardDataAccess(t *testing.T) { }), Overwrite: true, FolderId: 2, + UserId: 100, } err := SaveDashboard(&cmd) @@ -149,6 +169,7 @@ func TestDashboardDataAccess(t *testing.T) { }), FolderId: 0, Overwrite: true, + UserId: 100, } err = SaveDashboard(&cmd) @@ -162,6 +183,10 @@ func TestDashboardDataAccess(t *testing.T) { err = GetDashboard(&query) So(err, ShouldBeNil) So(query.Result.FolderId, ShouldEqual, 0) + So(query.Result.CreatedBy, ShouldEqual, savedDash.CreatedBy) + So(query.Result.Created, ShouldEqual, savedDash.Created.Truncate(time.Second)) + So(query.Result.UpdatedBy, ShouldEqual, 100) + So(query.Result.Updated.IsZero(), ShouldBeFalse) }) Convey("Should be able to delete a dashboard folder and its children", func() {