From c226f8eeb9e3dc9849e77e935f8fce382b7cf624 Mon Sep 17 00:00:00 2001 From: Overcooked Panda Date: Sat, 27 Jan 2018 01:58:43 -0600 Subject: [PATCH 01/65] added hash rate units for monitoring mining processes --- public/app/core/utils/kbn.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index e7ae645d4e4..99b99fc536b 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -463,6 +463,15 @@ kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); +// Hash Rate +kbn.valueFormats.Hs = kbn.formatBuilders.decimalSIPrefix('H/s'); +kbn.valueFormats.KHs = kbn.formatBuilders.decimalSIPrefix('H/s', 1); +kbn.valueFormats.MHs = kbn.formatBuilders.decimalSIPrefix('H/s', 2); +kbn.valueFormats.GHs = kbn.formatBuilders.decimalSIPrefix('H/s', 3); +kbn.valueFormats.THs = kbn.formatBuilders.decimalSIPrefix('H/s', 4); +kbn.valueFormats.PHs = kbn.formatBuilders.decimalSIPrefix('H/s', 5); +kbn.valueFormats.EHs = kbn.formatBuilders.decimalSIPrefix('H/s', 6); + // Throughput kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps'); @@ -878,6 +887,18 @@ kbn.getUnitFormats = function() { { text: 'gigabits/sec', value: 'Gbits' }, ], }, + { + text: 'hash rate', + submenu: [ + {text: 'hashes/sec', value: 'Hs'}, + {text: 'kilohashes/sec', value: 'KHs'}, + {text: 'megahashes/sec', value: 'MHs'}, + {text: 'gigahashes/sec', value: 'GHs'}, + {text: 'terahashes/sec', value: 'THs'}, + {text: 'petahashes/sec', value: 'PHs'}, + {text: 'exahashes/sec', value: 'EHs'}, + ], + }, { text: 'throughput', submenu: [ From f08932b78acf21103982f5270def7c1d2de80169 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 13:51:01 +0100 Subject: [PATCH 02/65] 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 03/65] 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 04/65] 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 05/65] 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 06/65] 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 07/65] 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 08/65] 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 09/65] 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 10/65] 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 11/65] 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 12/65] 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 13/65] 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 14/65] 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 15/65] 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 98a4731454639b22eabf30ee8943726bd4c44011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 21 Feb 2018 11:01:48 +0100 Subject: [PATCH 16/65] fix: scrollbar position now to max right pos, fixes #10982 --- public/sass/components/_scrollbar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index 50d6428b41e..42818e786f6 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -75,7 +75,7 @@ border-radius: 6px; width: 6px; /* there must be 'right' for ps__thumb-y */ - right: 2px; + right: 0px; /* please don't change 'position' */ position: absolute; } From a167e19b371ebe15c043b438ec2c8e0665a915c2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:24:54 +0100 Subject: [PATCH 17/65] 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 f00a7e4dd73d9851b6319e0f1f4ed3d96869fe02 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 21 Feb 2018 11:25:14 +0100 Subject: [PATCH 18/65] dev: docker-compose setup for prom2. Copy of prometheus1 docker-compose config. --- docker/blocks/prometheus2/docker-compose.yaml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docker/blocks/prometheus2/docker-compose.yaml diff --git a/docker/blocks/prometheus2/docker-compose.yaml b/docker/blocks/prometheus2/docker-compose.yaml new file mode 100644 index 00000000000..7b133ed9000 --- /dev/null +++ b/docker/blocks/prometheus2/docker-compose.yaml @@ -0,0 +1,25 @@ + prometheus: + build: blocks/prometheus2 + network_mode: host + ports: + - "9090:9090" + + node_exporter: + image: prom/node-exporter + network_mode: host + ports: + - "9100:9100" + + fake-prometheus-data: + image: grafana/fake-data-gen + network_mode: host + ports: + - "9091:9091" + environment: + FD_DATASOURCE: prom + + alertmanager: + image: quay.io/prometheus/alertmanager + network_mode: host + ports: + - "9093:9093" From 78283b9c7759adae0d9461c9b4a684613baee385 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 11:25:21 +0100 Subject: [PATCH 19/65] 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 20/65] 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 21/65] 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 22/65] 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 23/65] 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 24/65] 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 7d9a9fa29cbe88e9744a2ff76d2f8e5b35bf66ac Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Feb 2018 23:26:08 +0100 Subject: [PATCH 25/65] snapshots: change to snapshot list query Admins can see all snapshots. Other roles can only see their own snapshots. Added permission check for deleting snapshots - admins can delete any snapshot, other roles can delete their own snapshots or snapshots that they have access to via dashboard permissions. --- pkg/api/api.go | 2 +- pkg/api/dashboard_snapshot.go | 42 ++++++-- pkg/api/dashboard_snapshot_test.go | 97 +++++++++++++++++++ pkg/models/dashboard_snapshot.go | 10 +- pkg/services/sqlstore/dashboard_snapshot.go | 15 ++- .../sqlstore/dashboard_snapshot_test.go | 73 +++++++++++++- 6 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 pkg/api/dashboard_snapshot_test.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 1320663f630..8faf2ed5dde 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -106,7 +106,7 @@ func (hs *HttpServer) registerRoutes() { r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) r.Get("/api/snapshot/shared-options/", GetSharingOptions) r.Get("/api/snapshots/:key", GetDashboardSnapshot) - r.Get("/api/snapshots-delete/:key", reqEditorRole, DeleteDashboardSnapshot) + r.Get("/api/snapshots-delete/:key", reqEditorRole, wrap(DeleteDashboardSnapshot)) // api renew session based on remember cookie r.Get("/api/login/ping", quota("session"), LoginApiPing) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index a834bd4717d..c10302faf32 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -56,6 +57,7 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho }) } +// GET /api/snapshots/:key func GetDashboardSnapshot(c *middleware.Context) { key := c.Params(":key") query := &m.GetDashboardSnapshotQuery{Key: key} @@ -90,18 +92,43 @@ func GetDashboardSnapshot(c *middleware.Context) { c.JSON(200, dto) } -func DeleteDashboardSnapshot(c *middleware.Context) { +// GET /api/snapshots-delete/:key +func DeleteDashboardSnapshot(c *middleware.Context) Response { key := c.Params(":key") + + query := &m.GetDashboardSnapshotQuery{DeleteKey: key} + + err := bus.Dispatch(query) + if err != nil { + return ApiError(500, "Failed to get dashboard snapshot", err) + } + + if query.Result == nil { + return ApiError(404, "Failed to get dashboard snapshot", nil) + } + dashboard := query.Result.Dashboard + dashboardId := dashboard.Get("id").MustInt64() + + guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser) + canEdit, err := guardian.CanEdit() + if err != nil { + return ApiError(500, "Error while checking permissions for snapshot", err) + } + + if !canEdit && query.Result.UserId != c.SignedInUser.UserId { + return ApiError(403, "Access denied to this snapshot", nil) + } + cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: key} if err := bus.Dispatch(cmd); err != nil { - c.JsonApiErr(500, "Failed to delete dashboard snapshot", err) - return + return ApiError(500, "Failed to delete dashboard snapshot", err) } - c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."}) + return Json(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."}) } +// GET /api/dashboard/snapshots func SearchDashboardSnapshots(c *middleware.Context) Response { query := c.Query("query") limit := c.QueryInt("limit") @@ -111,9 +138,10 @@ func SearchDashboardSnapshots(c *middleware.Context) Response { } searchQuery := m.GetDashboardSnapshotsQuery{ - Name: query, - Limit: limit, - OrgId: c.OrgId, + Name: query, + Limit: limit, + OrgId: c.OrgId, + SignedInUser: c.SignedInUser, } err := bus.Dispatch(&searchQuery) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go new file mode 100644 index 00000000000..87c2b9e99d4 --- /dev/null +++ b/pkg/api/dashboard_snapshot_test.go @@ -0,0 +1,97 @@ +package api + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestDashboardSnapshotApiEndpoint(t *testing.T) { + Convey("Given a single snapshot", t, func() { + jsonModel, _ := simplejson.NewJson([]byte(`{"id":100}`)) + + mockSnapshotResult := &m.DashboardSnapshot{ + Id: 1, + Dashboard: jsonModel, + Expires: time.Now().Add(time.Duration(1000) * time.Second), + UserId: 999999, + } + + bus.AddHandler("test", func(query *m.GetDashboardSnapshotQuery) error { + query.Result = mockSnapshotResult + return nil + }) + + bus.AddHandler("test", func(cmd *m.DeleteDashboardSnapshotCommand) error { + return nil + }) + + viewerRole := m.ROLE_VIEWER + editorRole := m.ROLE_EDITOR + aclMockResp := []*m.DashboardAclInfoDTO{} + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = aclMockResp + return nil + }) + + teamResp := []*m.Team{} + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { + query.Result = teamResp + return nil + }) + + Convey("When user has editor role and is not in the ACL", func() { + Convey("Should not be able to delete snapshot", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() + + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + }) + + Convey("When user is editor and dashboard has default ACL", func() { + aclMockResp = []*m.DashboardAclInfoDTO{ + {Role: &viewerRole, Permission: m.PERMISSION_VIEW}, + {Role: &editorRole, Permission: m.PERMISSION_EDIT}, + } + + Convey("Should be able to delete a snapshot", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + + So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted") + }) + }) + }) + + Convey("When user is editor and is the creator of the snapshot", func() { + aclMockResp = []*m.DashboardAclInfoDTO{} + mockSnapshotResult.UserId = TestUserID + + Convey("Should be able to delete a snapshot", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + + So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted") + }) + }) + }) + }) +} diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index 9273b88f291..ccb154f4a69 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -67,7 +67,8 @@ type DeleteExpiredSnapshotsCommand struct { } type GetDashboardSnapshotQuery struct { - Key string + Key string + DeleteKey string Result *DashboardSnapshot } @@ -76,9 +77,10 @@ type DashboardSnapshots []*DashboardSnapshot type DashboardSnapshotsList []*DashboardSnapshotDTO type GetDashboardSnapshotsQuery struct { - Name string - Limit int - OrgId int64 + Name string + Limit int + OrgId int64 + SignedInUser *SignedInUser Result DashboardSnapshotsList } diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go index 0ef7f99da67..84850b2ec48 100644 --- a/pkg/services/sqlstore/dashboard_snapshot.go +++ b/pkg/services/sqlstore/dashboard_snapshot.go @@ -72,7 +72,7 @@ func DeleteDashboardSnapshot(cmd *m.DeleteDashboardSnapshotCommand) error { } func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error { - snapshot := m.DashboardSnapshot{Key: query.Key} + snapshot := m.DashboardSnapshot{Key: query.Key, DeleteKey: query.DeleteKey} has, err := x.Get(&snapshot) if err != nil { @@ -85,6 +85,8 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error { return nil } +// SearchDashboardSnapshots returns a list of all snapshots for admins +// for other roles, it returns snapshots created by the user func SearchDashboardSnapshots(query *m.GetDashboardSnapshotsQuery) error { var snapshots = make(m.DashboardSnapshotsList, 0) @@ -95,7 +97,16 @@ func SearchDashboardSnapshots(query *m.GetDashboardSnapshotsQuery) error { sess.Where("name LIKE ?", query.Name) } - sess.Where("org_id = ?", query.OrgId) + // admins can see all snapshots, everyone else can only see their own snapshots + if query.SignedInUser.OrgRole == m.ROLE_ADMIN { + sess.Where("org_id = ?", query.OrgId) + } else if !query.SignedInUser.IsAnonymous { + sess.Where("org_id = ? AND user_id = ?", query.OrgId, query.SignedInUser.UserId) + } else { + query.Result = snapshots + return nil + } + err := sess.Find(&snapshots) query.Result = snapshots return err diff --git a/pkg/services/sqlstore/dashboard_snapshot_test.go b/pkg/services/sqlstore/dashboard_snapshot_test.go index 50375088b4b..74a26068ece 100644 --- a/pkg/services/sqlstore/dashboard_snapshot_test.go +++ b/pkg/services/sqlstore/dashboard_snapshot_test.go @@ -14,17 +14,19 @@ func TestDashboardSnapshotDBAccess(t *testing.T) { Convey("Testing DashboardSnapshot data access", t, func() { InitTestDB(t) - Convey("Given saved snaphot", func() { + Convey("Given saved snapshot", func() { cmd := m.CreateDashboardSnapshotCommand{ Key: "hej", Dashboard: simplejson.NewFromAny(map[string]interface{}{ "hello": "mupp", }), + UserId: 1000, + OrgId: 1, } err := CreateDashboardSnapshot(&cmd) So(err, ShouldBeNil) - Convey("Should be able to get snaphot by key", func() { + Convey("Should be able to get snapshot by key", func() { query := m.GetDashboardSnapshotQuery{Key: "hej"} err = GetDashboardSnapshot(&query) So(err, ShouldBeNil) @@ -33,6 +35,73 @@ func TestDashboardSnapshotDBAccess(t *testing.T) { So(query.Result.Dashboard.Get("hello").MustString(), ShouldEqual, "mupp") }) + Convey("And the user has the admin role", func() { + Convey("Should return all the snapshots", func() { + query := m.GetDashboardSnapshotsQuery{ + OrgId: 1, + SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}, + } + err := SearchDashboardSnapshots(&query) + So(err, ShouldBeNil) + + So(query.Result, ShouldNotBeNil) + So(len(query.Result), ShouldEqual, 1) + }) + }) + + Convey("And the user has the editor role and has created a snapshot", func() { + Convey("Should return all the snapshots", func() { + query := m.GetDashboardSnapshotsQuery{ + OrgId: 1, + SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR, UserId: 1000}, + } + err := SearchDashboardSnapshots(&query) + So(err, ShouldBeNil) + + So(query.Result, ShouldNotBeNil) + So(len(query.Result), ShouldEqual, 1) + }) + }) + + Convey("And the user has the editor role and has not created any snapshot", func() { + Convey("Should not return any snapshots", func() { + query := m.GetDashboardSnapshotsQuery{ + OrgId: 1, + SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR, UserId: 2}, + } + err := SearchDashboardSnapshots(&query) + So(err, ShouldBeNil) + + So(query.Result, ShouldNotBeNil) + So(len(query.Result), ShouldEqual, 0) + }) + }) + + Convey("And the user is anonymous", func() { + cmd := m.CreateDashboardSnapshotCommand{ + Key: "strangesnapshotwithuserid0", + DeleteKey: "adeletekey", + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "hello": "mupp", + }), + UserId: 0, + OrgId: 1, + } + err := CreateDashboardSnapshot(&cmd) + So(err, ShouldBeNil) + + Convey("Should not return any snapshots", func() { + query := m.GetDashboardSnapshotsQuery{ + OrgId: 1, + SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR, IsAnonymous: true, UserId: 0}, + } + err := SearchDashboardSnapshots(&query) + So(err, ShouldBeNil) + + So(query.Result, ShouldNotBeNil) + So(len(query.Result), ShouldEqual, 0) + }) + }) }) }) } From 0135178645dbbfe967638f356739bacb28468179 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 15:05:33 +0100 Subject: [PATCH 26/65] 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 aa3872a0e26d7353abb9f60c9dc714d8d4f6a126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 21 Feb 2018 15:41:55 +0100 Subject: [PATCH 27/65] fix: elasticsearch terms size now allows custom values again, fixes #10124 --- .../plugins/datasource/elasticsearch/partials/bucket_agg.html | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index a180f97c994..d0cf23a3d93 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -101,6 +101,7 @@ get-options="getSizeOptions()" on-change="onChangeInternal()" label-mode="true" + allow-custom="true" css-class="width-12"> From 3f3175972e1a1a18b6c9dcc4a7bb4ebb618b717d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 21 Feb 2018 16:38:09 +0100 Subject: [PATCH 28/65] 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 29/65] 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 30/65] 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 bb7a6718f1a5b10f79e4aed40dd565a16a7fa772 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Feb 2018 10:52:21 +0100 Subject: [PATCH 31/65] Fix save as dashboard from folder to General folder (#10988) * dashboards: fix save as dashboard from folder to general folder * dashboards: disable save button in save dashboard as dialog when creating a new folder --- public/app/features/dashboard/dashboard_srv.ts | 2 +- public/app/features/dashboard/save_as_modal.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index b41d7b0583e..9d766fdfc3f 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -92,7 +92,7 @@ export class DashboardSrv { save(clone, options) { options = options || {}; - options.folderId = options.folderId || this.dash.meta.folderId || clone.folderId; + options.folderId = options.folderId >= 0 ? options.folderId : this.dash.meta.folderId || clone.folderId; return this.backendSrv .saveDashboard(clone, options) diff --git a/public/app/features/dashboard/save_as_modal.ts b/public/app/features/dashboard/save_as_modal.ts index 15a3ce5dba7..60f6fb4c1da 100644 --- a/public/app/features/dashboard/save_as_modal.ts +++ b/public/app/features/dashboard/save_as_modal.ts @@ -22,6 +22,8 @@ const template = `
@@ -29,7 +31,7 @@ const template = `
- + Cancel
@@ -40,6 +42,7 @@ export class SaveDashboardAsModalCtrl { clone: any; folderId: any; dismiss: () => void; + isValidFolderSelection = true; /** @ngInject */ constructor(private dashboardSrv) { @@ -79,6 +82,14 @@ export class SaveDashboardAsModalCtrl { onFolderChange(folder) { this.folderId = folder.id; } + + onEnterFolderCreation() { + this.isValidFolderSelection = false; + } + + onExitFolderCreation() { + this.isValidFolderSelection = true; + } } export function saveDashboardAsDirective() { From 3f588b4c4835a7f65242a2943972e4d0a398db1d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Feb 2018 11:54:28 +0100 Subject: [PATCH 32/65] 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() { From 5b8a49ee4468f4b7b1d92fbc60f256c4e0b3145c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Feb 2018 15:14:37 +0100 Subject: [PATCH 33/65] Update ROADMAP.md --- ROADMAP.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 479c1933bc0..648fef23ad3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,25 +1,28 @@ -# Roadmap (2017-10-31) +# Roadmap (2018-02-22) This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. But it will give you an idea of our current vision and plan. -### Short term (1-4 months) +### Short term (1-2 months) - - Release Grafana v5 - - Teams - - Dashboard folders - - Dashboard & folder permissions (assigned to users or groups) - - New Dashboard layout engine - - New sidemenu & nav UX +- v5.1 + - Crossplatform builds & build speed improvements + - New template interpolation syntax + - Provisioning workflow + - Enterprise LDAP + - First login registration view + - IFQL Initial support + +### Mid term (2-4 months) + +- v5.2 + - Azure monitor backend rewrite - Elasticsearch alerting - - React migration foundation (core components) - - Graphite 1.1 Tags Support + - Backend plugins? (alert notifiers, auth) ### Long term (4 - 8 months) -- Backend plugins to support more Auth options, Alerting data sources & notifications - Alerting improvements (silence, per series tracking, etc) -- Dashboard as configuration and other automation / provisioning improvements - Progress on React migration - Change visualization (panel type) on the fly. - Multi stat panel (vertical version of singlestat with bars/graph mode with big number etc) From 2451282563e2a36df224d92b38e284248509ffed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Feb 2018 15:24:38 +0100 Subject: [PATCH 34/65] Update ROADMAP.md --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 648fef23ad3..c8dc3186c73 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,9 +7,9 @@ But it will give you an idea of our current vision and plan. - v5.1 - Crossplatform builds & build speed improvements + - Enterprise LDAP - New template interpolation syntax - Provisioning workflow - - Enterprise LDAP - First login registration view - IFQL Initial support From fe49182b9d1f03171e12618c5cd99b04978cf24e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Feb 2018 23:10:59 +0100 Subject: [PATCH 35/65] snapshots: fixes cleanup of old snapshots Snapshot cleanup did not work due to time.Now syntax error. Added test for it as well to catch any future errors. Added error and debug logging so that it is possible to see any errors in the future. Removed an unused configuration value and deprecated the remove expired snapshots setting. --- conf/defaults.ini | 3 - conf/sample.ini | 3 - docs/sources/installation/configuration.md | 5 +- pkg/models/dashboard_snapshot.go | 1 + pkg/models/dashboard_version.go | 1 + pkg/services/cleanup/cleanup.go | 14 +++- pkg/services/sqlstore/dashboard_snapshot.go | 23 ++++--- .../sqlstore/dashboard_snapshot_test.go | 65 +++++++++++++++++++ pkg/services/sqlstore/dashboard_version.go | 4 +- pkg/setting/setting.go | 2 - 10 files changed, 94 insertions(+), 27 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 86768738171..948c9c4458e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -187,9 +187,6 @@ external_snapshot_name = Publish to snapshot.raintank.io # remove expired snapshot snapshot_remove_expired = true -# remove snapshots after 90 days -snapshot_TTL_days = 90 - #################################### Dashboards ################## [dashboards] diff --git a/conf/sample.ini b/conf/sample.ini index 5f13dad4061..3e45ac44d61 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -175,9 +175,6 @@ log_queries = # remove expired snapshot ;snapshot_remove_expired = true -# remove snapshots after 90 days -;snapshot_TTL_days = 90 - #################################### Dashboards History ################## [dashboards] # Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1 diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 498ab7fc8df..5dc2ee651b2 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -795,12 +795,9 @@ Set root url to a Grafana instance where you want to publish external snapshots ### external_snapshot_name Set name for external snapshot button. Defaults to `Publish to snapshot.raintank.io` -### remove expired snapshot +### snapshot_remove_expired Enabled to automatically remove expired snapshots -### remove snapshots after 90 days -Time to live for snapshots. - ## [external_image_storage] These options control how images should be made public so they can be shared on services like slack. diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index ccb154f4a69..ec8b19f3c18 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -64,6 +64,7 @@ type DeleteDashboardSnapshotCommand struct { } type DeleteExpiredSnapshotsCommand struct { + DeletedRows int64 } type GetDashboardSnapshotQuery struct { diff --git a/pkg/models/dashboard_version.go b/pkg/models/dashboard_version.go index 4acb4282a58..9f5f18cb263 100644 --- a/pkg/models/dashboard_version.go +++ b/pkg/models/dashboard_version.go @@ -75,4 +75,5 @@ type GetDashboardVersionsQuery struct { // type DeleteExpiredVersionsCommand struct { + DeletedRows int64 } diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index f9dcfce51b7..5e9efeea3b0 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -83,11 +83,21 @@ func (service *CleanUpService) cleanUpTmpFiles() { } func (service *CleanUpService) deleteExpiredSnapshots() { - bus.Dispatch(&m.DeleteExpiredSnapshotsCommand{}) + cmd := m.DeleteExpiredSnapshotsCommand{} + if err := bus.Dispatch(&cmd); err != nil { + service.log.Error("Failed to delete expired snapshots", "error", err.Error()) + } else { + service.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows) + } } func (service *CleanUpService) deleteExpiredDashboardVersions() { - bus.Dispatch(&m.DeleteExpiredVersionsCommand{}) + cmd := m.DeleteExpiredVersionsCommand{} + if err := bus.Dispatch(&cmd); err != nil { + service.log.Error("Failed to delete expired dashboard versions", "error", err.Error()) + } else { + service.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows) + } } func (service *CleanUpService) deleteOldLoginAttempts() { diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go index 84850b2ec48..9e82bbb2c83 100644 --- a/pkg/services/sqlstore/dashboard_snapshot.go +++ b/pkg/services/sqlstore/dashboard_snapshot.go @@ -16,20 +16,23 @@ func init() { bus.AddHandler("sql", DeleteExpiredSnapshots) } +// DeleteExpiredSnapshots removes snapshots with old expiry dates. +// SnapShotRemoveExpired is deprecated and should be removed in the future. +// Snapshot expiry is decided by the user when they share the snapshot. func DeleteExpiredSnapshots(cmd *m.DeleteExpiredSnapshotsCommand) error { return inTransaction(func(sess *DBSession) error { - var expiredCount int64 = 0 - - if setting.SnapShotRemoveExpired { - deleteExpiredSql := "DELETE FROM dashboard_snapshot WHERE expires < ?" - expiredResponse, err := x.Exec(deleteExpiredSql, time.Now) - if err != nil { - return err - } - expiredCount, _ = expiredResponse.RowsAffected() + if !setting.SnapShotRemoveExpired { + sqlog.Warn("[Deprecated] The snapshot_remove_expired setting is outdated. Please remove from your config.") + return nil } - sqlog.Debug("Deleted old/expired snaphots", "expired", expiredCount) + deleteExpiredSql := "DELETE FROM dashboard_snapshot WHERE expires < ?" + expiredResponse, err := sess.Exec(deleteExpiredSql, time.Now()) + if err != nil { + return err + } + cmd.DeletedRows, _ = expiredResponse.RowsAffected() + return nil }) } diff --git a/pkg/services/sqlstore/dashboard_snapshot_test.go b/pkg/services/sqlstore/dashboard_snapshot_test.go index 74a26068ece..2081cbf6194 100644 --- a/pkg/services/sqlstore/dashboard_snapshot_test.go +++ b/pkg/services/sqlstore/dashboard_snapshot_test.go @@ -2,11 +2,14 @@ package sqlstore import ( "testing" + "time" + "github.com/go-xorm/xorm" . "github.com/smartystreets/goconvey/convey" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" ) func TestDashboardSnapshotDBAccess(t *testing.T) { @@ -105,3 +108,65 @@ func TestDashboardSnapshotDBAccess(t *testing.T) { }) }) } + +func TestDeleteExpiredSnapshots(t *testing.T) { + Convey("Testing dashboard snapshots clean up", t, func() { + x := InitTestDB(t) + + setting.SnapShotRemoveExpired = true + + notExpiredsnapshot := createTestSnapshot(x, "key1", 1000) + createTestSnapshot(x, "key2", -1000) + createTestSnapshot(x, "key3", -1000) + + Convey("Clean up old dashboard snapshots", func() { + err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{}) + So(err, ShouldBeNil) + + query := m.GetDashboardSnapshotsQuery{ + OrgId: 1, + SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}, + } + err = SearchDashboardSnapshots(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Key, ShouldEqual, notExpiredsnapshot.Key) + }) + + Convey("Don't delete anything if there are no expired snapshots", func() { + err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{}) + So(err, ShouldBeNil) + + query := m.GetDashboardSnapshotsQuery{ + OrgId: 1, + SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}, + } + SearchDashboardSnapshots(&query) + + So(len(query.Result), ShouldEqual, 1) + }) + }) +} + +func createTestSnapshot(x *xorm.Engine, key string, expires int64) *m.DashboardSnapshot { + cmd := m.CreateDashboardSnapshotCommand{ + Key: key, + DeleteKey: "delete" + key, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "hello": "mupp", + }), + UserId: 1000, + OrgId: 1, + Expires: expires, + } + err := CreateDashboardSnapshot(&cmd) + So(err, ShouldBeNil) + + // Set expiry date manually - to be able to create expired snapshots + expireDate := time.Now().Add(time.Second * time.Duration(expires)) + _, err = x.Exec("update dashboard_snapshot set expires = ? where "+dialect.Quote("key")+" = ?", expireDate, key) + So(err, ShouldBeNil) + + return cmd.Result +} diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 49c35397094..547f62628f3 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -69,7 +69,6 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error { func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return inTransaction(func(sess *DBSession) error { - expiredCount := int64(0) versions := []DashboardVersionExp{} versionsToKeep := setting.DashboardVersionsToKeep @@ -98,8 +97,7 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { if err != nil { return err } - expiredCount, _ = expiredResponse.RowsAffected() - sqlog.Debug("Deleted old/expired dashboard versions", "expired", expiredCount) + cmd.DeletedRows, _ = expiredResponse.RowsAffected() } return nil diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6ce80a69957..6099388f668 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -88,7 +88,6 @@ var ( ExternalSnapshotUrl string ExternalSnapshotName string ExternalEnabled bool - SnapShotTTLDays int SnapShotRemoveExpired bool // Dashboard history @@ -523,7 +522,6 @@ func NewConfigContext(args *CommandLineArgs) error { ExternalSnapshotName = snapshots.Key("external_snapshot_name").String() ExternalEnabled = snapshots.Key("external_enabled").MustBool(true) SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true) - SnapShotTTLDays = snapshots.Key("snapshot_TTL_days").MustInt(90) // read dashboard settings dashboards := Cfg.Section("dashboards") From 609f3c1c9d30662cd8fddfbcce2d43b23cb30080 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Feb 2018 18:02:29 +0100 Subject: [PATCH 36/65] dashboards: fix keyboard shortcut for remove panel --- public/app/core/services/keybindingSrv.ts | 5 ++- .../app/features/dashboard/dashboard_ctrl.ts | 39 +++++++++++++++++++ public/app/features/panel/panel_ctrl.ts | 29 ++------------ 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 6f7a48f61d5..d0e74698e3f 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -171,8 +171,9 @@ export class KeybindingSrv { // delete panel this.bind('p r', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { - var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId); - panelInfo.row.removePanel(panelInfo.panel); + this.$rootScope.appEvent('panel-remove', { + panelId: dashboard.meta.focusPanelId, + }); dashboard.meta.focusPanelId = 0; } }); diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 8b1c69ef7fe..94d0b18f157 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -3,6 +3,7 @@ import config from 'app/core/config'; import coreModule from 'app/core/core_module'; import { PanelContainer } from './dashgrid/PanelContainer'; import { DashboardModel } from './dashboard_model'; +import { PanelModel } from './panel_model'; export class DashboardCtrl implements PanelContainer { dashboard: DashboardModel; @@ -130,9 +131,47 @@ export class DashboardCtrl implements PanelContainer { return this; } + onRemovingPanel(evt, options) { + options = options || {}; + if (!options.panelId) { + return; + } + + var panelInfo = this.dashboard.getPanelInfoById(options.panelId); + this.removePanel(panelInfo.panel, true); + } + + removePanel(panel: PanelModel, ask: boolean) { + // confirm deletion + if (ask !== false) { + var text2, confirmText; + + if (panel.alert) { + text2 = 'Panel includes an alert rule, removing panel will also remove alert rule'; + confirmText = 'YES'; + } + + this.$scope.appEvent('confirm-modal', { + title: 'Remove Panel', + text: 'Are you sure you want to remove this panel?', + text2: text2, + icon: 'fa-trash', + confirmText: confirmText, + yesText: 'Remove', + onConfirm: () => { + this.removePanel(panel, false); + }, + }); + return; + } + + this.dashboard.removePanel(panel); + } + init(dashboard) { this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this)); this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this)); + this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this)); this.setupDashboard(dashboard); } } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index d8757f49be6..429408ed803 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -241,31 +241,10 @@ export class PanelCtrl { }); } - removePanel(ask: boolean) { - // confirm deletion - if (ask !== false) { - var text2, confirmText; - - if (this.panel.alert) { - text2 = 'Panel includes an alert rule, removing panel will also remove alert rule'; - confirmText = 'YES'; - } - - appEvents.emit('confirm-modal', { - title: 'Remove Panel', - text: 'Are you sure you want to remove this panel?', - text2: text2, - icon: 'fa-trash', - confirmText: confirmText, - yesText: 'Remove', - onConfirm: () => { - this.removePanel(false); - }, - }); - return; - } - - this.dashboard.removePanel(this.panel); + removePanel() { + this.publishAppEvent('panel-remove', { + panelId: this.panel.id, + }); } editPanelJson() { From 34f1611d183f3735971cf9f386c16a9ce1175a1a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Feb 2018 18:03:29 +0100 Subject: [PATCH 37/65] dashboards: fix keyboard shortcut for expand/collapse rows --- public/app/core/services/keybindingSrv.ts | 8 ++---- .../app/features/dashboard/dashboard_model.ts | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index d0e74698e3f..64aeeddb522 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -213,16 +213,12 @@ export class KeybindingSrv { // collapse all rows this.bind('d shift+c', () => { - for (let row of dashboard.rows) { - row.collapse = true; - } + dashboard.collapseRows(); }); // expand all rows this.bind('d shift+e', () => { - for (let row of dashboard.rows) { - row.collapse = false; - } + dashboard.expandRows(); }); this.bind('d n', e => { diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index e35f8f0d430..3fa8ed9973a 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -524,6 +524,34 @@ export class DashboardModel { this.removePanel(row); } + expandRows() { + for (let i = 0; i < this.panels.length; i++) { + var panel = this.panels[i]; + + if (panel.type !== 'row') { + continue; + } + + if (panel.collapsed) { + this.toggleRow(panel); + } + } + } + + collapseRows() { + for (let i = 0; i < this.panels.length; i++) { + var panel = this.panels[i]; + + if (panel.type !== 'row') { + continue; + } + + if (!panel.collapsed) { + this.toggleRow(panel); + } + } + } + setPanelFocus(id) { this.meta.focusPanelId = id; } From cacfdc64219ea045a0a9b8b98255fde05acd23cb Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Feb 2018 18:08:51 +0100 Subject: [PATCH 38/65] dashboards: remove non-supported keyboard shortcuts for delete/collapse row --- public/app/core/components/help/help.ts | 4 ---- public/app/core/services/keybindingSrv.ts | 18 ------------------ 2 files changed, 22 deletions(-) diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index c2f6f213dd3..a544fc89854 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -33,10 +33,6 @@ export class HelpCtrl { { keys: ['p', 's'], description: 'Open Panel Share Modal' }, { keys: ['p', 'r'], description: 'Remove Panel' }, ], - 'Focused Row': [ - { keys: ['r', 'c'], description: 'Collapse Row' }, - { keys: ['r', 'r'], description: 'Remove Row' }, - ], 'Time Range': [ { keys: ['t', 'z'], description: 'Zoom out time range' }, { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 64aeeddb522..1658d74be0a 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -193,24 +193,6 @@ export class KeybindingSrv { } }); - // delete row - this.bind('r r', () => { - if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { - var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId); - dashboard.removeRow(panelInfo.row); - dashboard.meta.focusPanelId = 0; - } - }); - - // collapse row - this.bind('r c', () => { - if (dashboard.meta.focusPanelId) { - var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId); - panelInfo.row.toggleCollapse(); - dashboard.meta.focusPanelId = 0; - } - }); - // collapse all rows this.bind('d shift+c', () => { dashboard.collapseRows(); From b316dfea988f1362f24765074302ed76287e7cc1 Mon Sep 17 00:00:00 2001 From: Brian Gann Date: Thu, 15 Feb 2018 11:44:03 -0600 Subject: [PATCH 39/65] feature for issue #9911 --- .../templating/specs/template_srv.jest.ts | 80 ++++++++++++++++++- .../app/features/templating/template_srv.ts | 23 +++--- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 577669a41dd..37e2f5e4fe5 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -31,12 +31,40 @@ describe('templateSrv', function() { expect(target).toBe('this.mupp.filters'); }); + it('should replace ${test} with scoped value', function() { + var target = _templateSrv.replace('this.${test}.filters', { + test: { value: 'mupp', text: 'asd' }, + }); + expect(target).toBe('this.mupp.filters'); + }); + + it('should replace ${test:glob} with scoped value', function() { + var target = _templateSrv.replace('this.${test:glob}.filters', { + test: { value: 'mupp', text: 'asd' }, + }); + expect(target).toBe('this.mupp.filters'); + }); + it('should replace $test with scoped text', function() { var target = _templateSrv.replaceWithText('this.$test.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); }); + + it('should replace ${test} with scoped text', function() { + var target = _templateSrv.replaceWithText('this.${test}.filters', { + test: { value: 'mupp', text: 'asd' }, + }); + expect(target).toBe('this.asd.filters'); + }); + + it('should replace ${test:glob} with scoped text', function() { + var target = _templateSrv.replaceWithText('this.${test:glob}.filters', { + test: { value: 'mupp', text: 'asd' }, + }); + expect(target).toBe('this.asd.filters'); + }); }); describe('getAdhocFilters', function() { @@ -79,18 +107,34 @@ describe('templateSrv', function() { ]); }); + it('should replace $test with globbed value', function() { var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); + it('should replace ${test} with globbed value', function() { + var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + expect(target).toBe('this.{value1,value2}.filters'); + }); + + it('should replace ${test:glob} with globbed value', function() { + var target = _templateSrv.replace('this.${test:glob}.filters', {}); + expect(target).toBe('this.{value1,value2}.filters'); + }); + it('should replace $test with piped value', function() { var target = _templateSrv.replace('this=$test', {}, 'pipe'); expect(target).toBe('this=value1|value2'); }); - it('should replace $test with piped value', function() { - var target = _templateSrv.replace('this=$test', {}, 'pipe'); + it('should replace ${test} with piped value', function() { + var target = _templateSrv.replace('this=${test}', {}, 'pipe'); + expect(target).toBe('this=value1|value2'); + }); + + it('should replace ${test:pipe} with piped value', function() { + var target = _templateSrv.replace('this=${test:pipe}', {}); expect(target).toBe('this=value1|value2'); }); }); @@ -111,6 +155,16 @@ describe('templateSrv', function() { var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); + + it('should replace ${test} with formatted all value', function() { + var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + expect(target).toBe('this.{value1,value2}.filters'); + }); + + it('should replace ${test:glob} with formatted all value', function() { + var target = _templateSrv.replace('this.${test:glob}.filters', {}); + expect(target).toBe('this.{value1,value2}.filters'); + }); }); describe('variable with all option and custom value', function() { @@ -131,6 +185,16 @@ describe('templateSrv', function() { expect(target).toBe('this.*.filters'); }); + it('should replace ${test} with formatted all value', function() { + var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + expect(target).toBe('this.*.filters'); + }); + + it('should replace ${test:glob} with formatted all value', function() { + var target = _templateSrv.replace('this.${test:glob}.filters', {}); + expect(target).toBe('this.*.filters'); + }); + it('should not escape custom all value', function() { var target = _templateSrv.replace('this.$test', {}, 'regex'); expect(target).toBe('this.*'); @@ -143,6 +207,18 @@ describe('templateSrv', function() { var target = _templateSrv.replace('this:$test', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); + + it('should properly escape ${test} with lucene escape sequences', function() { + initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); + var target = _templateSrv.replace('this:${test}', {}, 'lucene'); + expect(target).toBe('this:value\\/4'); + }); + + it('should properly escape ${test:lucene} with lucene escape sequences', function() { + initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); + var target = _templateSrv.replace('this:${test:lucene}', {}); + expect(target).toBe('this:value\\/4'); + }); }); describe('format variable to string values', function() { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index fa5cabd847a..f29e133238e 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -8,7 +8,7 @@ function luceneEscape(value) { export class TemplateSrv { variables: any[]; - private regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g; + private regex = /\$(\w+)|\[\[([\s\S]+?)\]\]|\${(\w+):?(\w+)?}/g; private index = {}; private grafanaVariables = {}; private builtIns = {}; @@ -89,6 +89,9 @@ export class TemplateSrv { } var escapedValues = _.map(value, kbn.regexEscape); + if (escapedValues.length === 1) { + return escapedValues[0]; + } return '(' + escapedValues.join('|') + ')'; } case 'lucene': { @@ -140,8 +143,8 @@ export class TemplateSrv { str = _.escape(str); this.regex.lastIndex = 0; - return str.replace(this.regex, (match, g1, g2) => { - if (this.index[g1 || g2] || this.builtIns[g1 || g2]) { + return str.replace(this.regex, (match, g1, g2, g3) => { + if (this.index[g1 || g2 || g3] || this.builtIns[g1 || g2 || g3]) { return '' + match + ''; } return match; @@ -167,11 +170,11 @@ export class TemplateSrv { var variable, systemValue, value; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2) => { - variable = this.index[g1 || g2]; - + return target.replace(this.regex, (match, g1, g2, g3, g4) => { + variable = this.index[g1 || g2 || g3]; + format = g4 || format; if (scopedVars) { - value = scopedVars[g1 || g2]; + value = scopedVars[g1 || g2 || g3]; if (value) { return this.formatValue(value.value, format, variable); } @@ -212,15 +215,15 @@ export class TemplateSrv { var variable; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2) => { + return target.replace(this.regex, (match, g1, g2, g3) => { if (scopedVars) { - var option = scopedVars[g1 || g2]; + var option = scopedVars[g1 || g2 || g3]; if (option) { return option.text; } } - variable = this.index[g1 || g2]; + variable = this.index[g1 || g2 || g3]; if (!variable) { return match; } From fa193fa88837cb9b496bf21443c1e05ba32fa8cf Mon Sep 17 00:00:00 2001 From: Brian Gann Date: Thu, 15 Feb 2018 11:49:26 -0600 Subject: [PATCH 40/65] offer template variables for tags --- public/app/plugins/datasource/graphite/query_ctrl.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index 29ff29b7ae3..0563de61705 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -348,6 +348,10 @@ export class GraphiteQueryCtrl extends QueryCtrl { let tagKey = tag.key; return this.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix).then(values => { let altValues = _.map(values, 'text'); + // Add template variables as additional values + _.eachRight(this.templateSrv.variables, variable => { + altValues.push('${' + variable.name + ':regex}'); + }); return mapToDropdownOptions(altValues); }); } From dc6f977304c509d58b47f7e990ad2a3ea79666f7 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 22 Feb 2018 18:39:54 -0500 Subject: [PATCH 41/65] support [[variable:type]] syntax --- .../app/features/templating/template_srv.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index f29e133238e..a5b5418a564 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -8,7 +8,7 @@ function luceneEscape(value) { export class TemplateSrv { variables: any[]; - private regex = /\$(\w+)|\[\[([\s\S]+?)\]\]|\${(\w+):?(\w+)?}/g; + private regex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g; private index = {}; private grafanaVariables = {}; private builtIns = {}; @@ -143,8 +143,8 @@ export class TemplateSrv { str = _.escape(str); this.regex.lastIndex = 0; - return str.replace(this.regex, (match, g1, g2, g3) => { - if (this.index[g1 || g2 || g3] || this.builtIns[g1 || g2 || g3]) { + return str.replace(this.regex, (match, g1, g2, g3, g4) => { + if (this.index[g1 || g2 || g4] || this.builtIns[g1 || g2 || g4]) { return '' + match + ''; } return match; @@ -170,11 +170,11 @@ export class TemplateSrv { var variable, systemValue, value; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2, g3, g4) => { - variable = this.index[g1 || g2 || g3]; - format = g4 || format; + return target.replace(this.regex, (match, g1, g2, g3, g4, g5) => { + variable = this.index[g1 || g2 || g4]; + format = g3 || g5 || format; if (scopedVars) { - value = scopedVars[g1 || g2 || g3]; + value = scopedVars[g1 || g2 || g4]; if (value) { return this.formatValue(value.value, format, variable); } @@ -215,15 +215,15 @@ export class TemplateSrv { var variable; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2, g3) => { + return target.replace(this.regex, (match, g1, g2, g3, g4) => { if (scopedVars) { - var option = scopedVars[g1 || g2 || g3]; + var option = scopedVars[g1 || g2 || g4]; if (option) { return option.text; } } - variable = this.index[g1 || g2 || g3]; + variable = this.index[g1 || g2 || g4]; if (!variable) { return match; } From e35065343081cda6def7a97ec651743f5391ea6d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 23 Feb 2018 14:52:45 +0100 Subject: [PATCH 42/65] docs: fix type in datasource http api --- docs/sources/http_api/data_source.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md index 468a812c709..364b55b0cfc 100644 --- a/docs/sources/http_api/data_source.md +++ b/docs/sources/http_api/data_source.md @@ -90,7 +90,7 @@ Content-Type: application/json ## Get a single data source by Name -`GET /api/datasources/:name` +`GET /api/datasources/name/:name` **Example Request**: From abf498fc7f4523c44f6aacfb1070e55829059f6d Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 23 Feb 2018 09:51:19 -0500 Subject: [PATCH 43/65] improve maintainability --- .../app/features/templating/template_srv.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index a5b5418a564..40f119ea10b 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -8,6 +8,12 @@ function luceneEscape(value) { export class TemplateSrv { variables: any[]; + /* + * This regex matches 3 types of variable reference with an optional format specifier + * \$(\w+) $var1 + * \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] + * \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3} + */ private regex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g; private index = {}; private grafanaVariables = {}; @@ -143,8 +149,8 @@ export class TemplateSrv { str = _.escape(str); this.regex.lastIndex = 0; - return str.replace(this.regex, (match, g1, g2, g3, g4) => { - if (this.index[g1 || g2 || g4] || this.builtIns[g1 || g2 || g4]) { + return str.replace(this.regex, (match, var1, var2, fmt2, var3) => { + if (this.index[var1 || var2 || var3] || this.builtIns[var1 || var2 || var3]) { return '' + match + ''; } return match; @@ -170,11 +176,11 @@ export class TemplateSrv { var variable, systemValue, value; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2, g3, g4, g5) => { - variable = this.index[g1 || g2 || g4]; - format = g3 || g5 || format; + return target.replace(this.regex, (match, var1, var2, fmt2, var3, fmt3) => { + variable = this.index[var1 || var2 || var3]; + format = fmt2 || fmt3 || format; if (scopedVars) { - value = scopedVars[g1 || g2 || g4]; + value = scopedVars[var1 || var2 || var3]; if (value) { return this.formatValue(value.value, format, variable); } @@ -215,15 +221,15 @@ export class TemplateSrv { var variable; this.regex.lastIndex = 0; - return target.replace(this.regex, (match, g1, g2, g3, g4) => { + return target.replace(this.regex, (match, var1, var2, fmt2, var3) => { if (scopedVars) { - var option = scopedVars[g1 || g2 || g4]; + var option = scopedVars[var1 || var2 || var3]; if (option) { return option.text; } } - variable = this.index[g1 || g2 || g4]; + variable = this.index[var1 || var2 || var3]; if (!variable) { return match; } From 3682225617f993e6d92aa9d6e27e6944dff8d77c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Sun, 25 Feb 2018 00:50:49 +0100 Subject: [PATCH 44/65] build: use tarball instead of git commit for tether drop It is the only js package that is installed directly from GitHub - this changes it to download the tarball instead of doing a git clone. Background: Git clones against GitHub does currently does not work with the container build which uses CentOS 6.6. It does not have support for TLS 1.2 and cannot git clone against GitHub since they removed support for TLS 1.0 and 1.1. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cae6360f01..3dde98f0f39 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", "tether": "^1.4.0", - "tether-drop": "https://github.com/torkelo/drop", + "tether-drop": "https://github.com/torkelo/drop/tarball/master", "tinycolor2": "^1.4.1" } } From 3218927eadf10f36c37df6a612ac9023886b8380 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Sun, 25 Feb 2018 01:14:53 +0100 Subject: [PATCH 45/65] yarn: update lock file with tarball change --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index a78fcf0c3d6..692eb83ea96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9773,9 +9773,9 @@ test-exclude@^4.1.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" -"tether-drop@https://github.com/torkelo/drop": +"tether-drop@https://github.com/torkelo/drop/tarball/master": version "1.5.0" - resolved "https://github.com/torkelo/drop#fc83ca88db0076fbf6359cbe1743a9ef0f1ee6e1" + resolved "https://github.com/torkelo/drop/tarball/master#6a3eb15b882b416f06e1e7ae04c7e57d08418020" dependencies: tether "^1.1.0" From 1a170133ca7be76f3a7860c037d26d30e5c0629f Mon Sep 17 00:00:00 2001 From: Marco Tulio R Braga Date: Mon, 26 Feb 2018 03:04:39 -0300 Subject: [PATCH 46/65] [doc] Fix extra alerting options in installation->configuration --- docs/sources/installation/configuration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 5dc2ee651b2..be9ac44e827 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -875,6 +875,4 @@ Defaults to true. Set to false to disable alerting engine and hide Alerting from ### execute_alerts -### execute_alerts = true - Makes it possible to turn off alert rule execution. From c21d9deccb62873dbae9fb3483e7880aa9f03ed7 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 26 Feb 2018 09:44:03 +0100 Subject: [PATCH 47/65] build: update to version 5.0.0-beta5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3dde98f0f39..5f20f8ffc73 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.0.0-beta4", + "version": "5.0.0-beta5", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From ef67d20971310439efbd8dd28cb37fd9843fc5a1 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 26 Feb 2018 10:49:43 +0100 Subject: [PATCH 48/65] changelog: update for v5.0.0-beta5 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d16e816f3d..c76346e419e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 5.0.0-beta5 (2018-02-26) + +### Fixes + +- **Orgs** Unable to switch org when too many orgs listed [#10774](https://github.com/grafana/grafana/issues/10774) +- **Folders** Make it easier/explicit to access/modify folders using the API [#10630](https://github.com/grafana/grafana/issues/10630) +- **Dashboard** Scrollbar works incorrectly in Grafana 5.0 Beta4 in some cases [#10982](https://github.com/grafana/grafana/issues/10982) +- **ElasticSearch** Custom aggregation sizes no longer allowed for Elasticsearch [#10124](https://github.com/grafana/grafana/issues/10124) +- **oauth** Github OAuth with allowed organizations fails to login [#10964](https://github.com/grafana/grafana/issues/10964) +- **heatmap** Heatmap panel has partially hidden legend [#10793](https://github.com/grafana/grafana/issues/10793) +- **snapshots** Expired snapshots not being cleaned up [#10996](https://github.com/grafana/grafana/pull/10996) + # 5.0.0-beta4 (2018-02-19) ### Fixes From 6f4c043dd672b7f4ac6f25d00b9b22ebbeb597cf Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 26 Feb 2018 11:04:08 +0100 Subject: [PATCH 49/65] docs: update to install pages for beta5 --- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 4 ++-- docs/sources/installation/windows.md | 2 +- packaging/publish/publish_testing.sh | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index a45afe35e6b..bb85a579a0c 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -16,7 +16,7 @@ weight = 1 Description | Download ------------ | ------------- Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb) -Beta for Debian-based Linux | [grafana_5.0.0-beta4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta4_amd64.deb) +Beta for Debian-based Linux | [grafana_5.0.0-beta5_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta5_amd64.deb) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -33,9 +33,9 @@ sudo dpkg -i grafana_4.6.3_amd64.deb ## Install Latest Beta ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta4_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta5_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.0.0-beta4_amd64.deb +sudo dpkg -i grafana_5.0.0-beta5_amd64.deb ``` ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index e97ab17a697..1e0645e74c1 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -16,7 +16,7 @@ weight = 2 Description | Download ------------ | ------------- Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm) -Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0-beta4 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta4.x86_64.rpm) +Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0-beta5 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.x86_64.rpm) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -32,7 +32,7 @@ $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/g ## Install Beta ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta4.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.x86_64.rpm ``` Or install manually using `rpm`. diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 07f5abde52f..90c469bfe59 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -14,7 +14,7 @@ weight = 3 Description | Download ------------ | ------------- Latest stable package for Windows | [grafana.4.6.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3.windows-x64.zip) -Latest beta package for Windows | [grafana.5.0.0-beta4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta4.windows-x64.zip) +Latest beta package for Windows | [grafana.5.0.0-beta5.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.windows-x64.zip) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. diff --git a/packaging/publish/publish_testing.sh b/packaging/publish/publish_testing.sh index 5dea3655251..08ba2a89dd9 100755 --- a/packaging/publish/publish_testing.sh +++ b/packaging/publish/publish_testing.sh @@ -1,6 +1,6 @@ #! /usr/bin/env bash -deb_ver=5.0.0-beta4 -rpm_ver=5.0.0-beta4 +deb_ver=5.0.0-beta5 +rpm_ver=5.0.0-beta5 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb From 7fb34f7918173ebed4e9d8a2992a2beb2736628a Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 26 Feb 2018 11:52:48 +0100 Subject: [PATCH 50/65] gave scroll-canvas-dashboard 100% height in kiosk-mode, fixes #11010 (#11017) --- public/sass/components/_view_states.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss index be5361ceeb4..b1fa47d0c0a 100644 --- a/public/sass/components/_view_states.scss +++ b/public/sass/components/_view_states.scss @@ -3,6 +3,9 @@ .navbar { display: none; } + .scroll-canvas--dashboard { + height: 100%; + } } .playlist-active, From dff2c14aedca25c360b30bb08f0a318050a00d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 27 Feb 2018 11:34:29 +0100 Subject: [PATCH 51/65] fix: changed react-grid-layout to use grafana fork to a commit before https://github.com/STRML/react-grid-layout/commit/15503084fb7b0af826427c8c0706901e5745a39f, this fixes all the panel movement bugs, fixes #10831 --- package.json | 2 +- .../features/dashboard/dashgrid/DashboardGrid.tsx | 10 ++++++---- public/sass/components/_dashboard_grid.scss | 2 +- yarn.lock | 15 +++++++++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 5f20f8ffc73..b715d96dcde 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "prop-types": "^15.6.0", "react": "^16.2.0", "react-dom": "^16.2.0", - "react-grid-layout": "^0.16.2", + "react-grid-layout-grafana": "0.16.0", "react-highlight-words": "^0.10.0", "react-popper": "^0.7.5", "react-select": "^1.1.0", diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 3f65c33c90d..03bf65afc6e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import ReactGridLayout from 'react-grid-layout'; +import ReactGridLayout from 'react-grid-layout-grafana'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { DashboardPanel } from './DashboardPanel'; import { DashboardModel } from '../dashboard_model'; @@ -50,7 +50,8 @@ function GridWrapper({ onResize={onResize} onResizeStop={onResizeStop} onDragStop={onDragStop} - onLayoutChange={onLayoutChange}> + onLayoutChange={onLayoutChange} + > {children} ); @@ -178,7 +179,7 @@ export class DashboardGrid extends React.Component { panelElements.push(
-
, + ); } @@ -196,7 +197,8 @@ export class DashboardGrid extends React.Component { onWidthChange={this.onWidthChange} onDragStop={this.onDragStop} onResize={this.onResize} - onResizeStop={this.onResizeStop}> + onResizeStop={this.onResizeStop} + > {this.renderPanels()} ); diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index 0a27df75164..dd81a210409 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -1,4 +1,4 @@ -@import '~react-grid-layout/css/styles.css'; +@import '~react-grid-layout-grafana/css/styles.css'; @import '~react-resizable/css/styles.css'; .panel-in-fullscreen { diff --git a/yarn.lock b/yarn.lock index 692eb83ea96..2de60f03c27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8275,16 +8275,23 @@ react-dom@^16.2.0: object-assign "^4.1.1" prop-types "^15.6.0" -"react-draggable@^2.2.6 || ^3.0.3", react-draggable@^3.0.3: +"react-draggable@^2.2.6 || ^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4" dependencies: classnames "^2.2.5" prop-types "^15.5.10" -react-grid-layout@^0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.2.tgz#ef09b0b6db4a9635799663658277ee2d26fa2994" +react-draggable@^3.0.3: + version "3.0.5" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.5.tgz#c031e0ed4313531f9409d6cd84c8ebcec0ddfe2d" + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + +react-grid-layout-grafana@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/react-grid-layout-grafana/-/react-grid-layout-grafana-0.16.0.tgz#12242153fcd0bb80a26af8e41694bc2fde788b3a" dependencies: classnames "2.x" lodash.isequal "^4.0.0" From 7d91b737749fa0293a53fbc115ad798ad26c1ab3 Mon Sep 17 00:00:00 2001 From: jganseman Date: Tue, 27 Feb 2018 13:26:53 +0100 Subject: [PATCH 52/65] grammar fix, add dir, and remove redundant info --- docs/sources/installation/windows.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 90c469bfe59..5e8a204d11d 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -31,9 +31,9 @@ on windows. Edit `custom.ini` and uncomment the `http_port` configuration option (`;` is the comment character in ini files) and change it to something like `8080` or similar. That port should not require extra Windows privileges. -Start Grafana by executing `grafana-server.exe`, preferably from the +Start Grafana by executing `grafana-server.exe`, located in the `bin` directory, preferably from the command line. If you want to run Grafana as windows service, download -[NSSM](https://nssm.cc/). It is very easy add Grafana as a Windows +[NSSM](https://nssm.cc/). It is very easy to add Grafana as a Windows service using that tool. Read more about the [configuration options]({{< relref "configuration.md" >}}). @@ -43,7 +43,3 @@ Read more about the [configuration options]({{< relref "configuration.md" >}}). The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on Windows you need to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). - -Copy `conf/sample.ini` to a file named `conf/custom.ini` and change the -web server port to something like 8080. The default Grafana port, 3000, -requires special privileges on Windows. From 69006a7764e93c4ae2192fa5346b0a7db7ce2453 Mon Sep 17 00:00:00 2001 From: Marcel Anacker Date: Tue, 27 Feb 2018 15:48:36 +0100 Subject: [PATCH 53/65] Alerting: Fix OK state doesn't show up in Microsoft Teams (#11032) * Alerting: Fix OK state doesn't show up in Microsoft Teams * Fix formatting --- pkg/services/alerting/notifiers/teams.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 605b2742325..851e5a01c75 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -82,6 +82,8 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { message := this.Mention if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. message += " " + evalContext.Rule.Message + } else { + message += " " // summary must not be empty } body := map[string]interface{}{ From 955dfcc8fe364bc7e3ff8ebbaaaab3ac12fc8cfc Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 26 Feb 2018 19:12:01 +0100 Subject: [PATCH 54/65] dashboards: don't allow override of permissions with a lower precedence If a dashboard inherits permissions from a folder, don't allow same permission to be added to the dashboard with a lower permission. Add backend validation so that you cannot add same permission to folder/dashboard, for example same user/team with different permissions --- pkg/services/guardian/guardian.go | 50 +- pkg/services/guardian/guardian_test.go | 659 +++++++++++++++++++++++++ 2 files changed, 702 insertions(+), 7 deletions(-) create mode 100644 pkg/services/guardian/guardian_test.go diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 23d43a53f35..972e87de39b 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -1,12 +1,19 @@ package guardian import ( + "errors" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) +var ( + ErrGuardianDuplicatePermission = errors.New("You cannot add multiple permissions for a user, team or role") + ErrGuardianOverrideLowerPresedence = errors.New("You cannot override a permission with a lower presedence permission") +) + // DashboardGuardian to be used for guard against operations without access on dashboard and acl type DashboardGuardian interface { CanSave() (bool, error) @@ -119,14 +126,41 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D } func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) { - if g.user.OrgRole == m.ROLE_ADMIN { - return true, nil - } - acl := []*m.DashboardAclInfoDTO{} for _, p := range updatePermissions { - acl = append(acl, &m.DashboardAclInfoDTO{UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}) + for _, a := range acl { + if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == p.UserId && a.TeamId == p.TeamId && a.Role == p.Role) || + (a.UserId > 0 && a.UserId == p.UserId) || + (a.TeamId > 0 && a.TeamId == p.TeamId) { + return false, ErrGuardianDuplicatePermission + } + } + + acl = append(acl, &m.DashboardAclInfoDTO{DashboardId: p.DashboardId, UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}) + } + + existingPermissions, err := g.GetAcl() + if err != nil { + return false, err + } + + for _, a := range acl { + for _, existingPerm := range existingPermissions { + if a.DashboardId == existingPerm.DashboardId { + continue + } + + if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == existingPerm.UserId && a.TeamId == existingPerm.TeamId && *a.Role == *existingPerm.Role && a.Permission <= existingPerm.Permission) || + (a.UserId > 0 && a.UserId == existingPerm.UserId && a.Permission <= existingPerm.Permission) || + (a.TeamId > 0 && a.TeamId == existingPerm.TeamId && a.Permission <= existingPerm.Permission) { + return false, ErrGuardianOverrideLowerPresedence + } + } + } + + if g.user.OrgRole == m.ROLE_ADMIN { + return true, nil } return g.checkAcl(permission, acl) @@ -169,6 +203,8 @@ type FakeDashboardGuardian struct { CanAdminValue bool HasPermissionValue bool CheckPermissionBeforeUpdateValue bool + CheckPermissionBeforeUpdateError error + GetAclValue []*m.DashboardAclInfoDTO } func (g *FakeDashboardGuardian) CanSave() (bool, error) { @@ -192,11 +228,11 @@ func (g *FakeDashboardGuardian) HasPermission(permission m.PermissionType) (bool } func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) { - return g.CheckPermissionBeforeUpdateValue, nil + return g.CheckPermissionBeforeUpdateValue, g.CheckPermissionBeforeUpdateError } func (g *FakeDashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) { - return nil, nil + return g.GetAclValue, nil } func MockDashboardGuardian(mock *FakeDashboardGuardian) { diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go new file mode 100644 index 00000000000..d420d1add97 --- /dev/null +++ b/pkg/services/guardian/guardian_test.go @@ -0,0 +1,659 @@ +package guardian + +import ( + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/bus" + + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestGuardian(t *testing.T) { + Convey("Guardian permission tests", t, func() { + orgRoleScenario("Given user has admin org role", m.ROLE_ADMIN, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeTrue) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + + Convey("When trying to update permissions", func() { + Convey("With duplicate user/role permissions should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianDuplicatePermission) + }) + + Convey("With duplicate team/role permissions should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianDuplicatePermission) + }) + + Convey("With duplicate everyone/role permissions should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_ADMIN}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianDuplicatePermission) + }) + }) + + Convey("Given parent folder has user admin permission", func() { + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions with admin user permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + + Convey("When trying to update dashboard permissions with edit user permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + + Convey("When trying to update dashboard permissions with view user permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + }) + + Convey("Given parent folder has user edit permission", func() { + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with edit user permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + + Convey("When trying to update dashboard permissions with view user permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + }) + + Convey("Given parent folder has user view permission", func() { + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with edit user permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with view user permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + }) + + Convey("Given parent folder has team admin permission", func() { + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_ADMIN}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions with admin team permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + + Convey("When trying to update dashboard permissions with edit team permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + + Convey("When trying to update dashboard permissions with view team permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + }) + + Convey("Given parent folder has team edit permission", func() { + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_EDIT}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with edit team permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + + Convey("When trying to update dashboard permissions with view team permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + }) + + Convey("Given parent folder has team view permission", func() { + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_VIEW}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with edit team permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with view team permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + }) + + Convey("Given parent folder has editor role with edit permission", func() { + r := m.ROLE_EDITOR + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_EDIT}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions with everyone with editor role can admin permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with everyone with editor role can edit permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + + Convey("When trying to update dashboard permissions with everyone with editor role can view permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + }) + + Convey("Given parent folder has editor role with view permission", func() { + r := m.ROLE_EDITOR + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_VIEW}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions with everyone with viewer role can admin permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with everyone with viewer role can edit permission should be allowed", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions with everyone with viewer role can view permission should return error", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + }) + }) + }) + + orgRoleScenario("Given user has editor org role", m.ROLE_EDITOR, func(sc *scenarioContext) { + everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeTrue) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeTrue) + }) + + everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeFalse) + }) + + everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeFalse) + }) + + everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeFalse) + }) + + userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeTrue) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeTrue) + }) + + teamWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeTrue) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + teamWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + teamWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeTrue) + }) + + Convey("When trying to update permissions should return false", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeFalse) + }) + }) + + orgRoleScenario("Given user has viewer org role", m.ROLE_VIEWER, func(sc *scenarioContext) { + everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeFalse) + }) + + everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeFalse) + }) + + everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeFalse) + }) + + everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeTrue) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeTrue) + }) + + userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeTrue) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeTrue) + So(canSave, ShouldBeTrue) + So(canView, ShouldBeTrue) + }) + + userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) { + canAdmin, _ := sc.g.CanAdmin() + canEdit, _ := sc.g.CanEdit() + canSave, _ := sc.g.CanSave() + canView, _ := sc.g.CanView() + So(canAdmin, ShouldBeFalse) + So(canEdit, ShouldBeFalse) + So(canSave, ShouldBeFalse) + So(canView, ShouldBeTrue) + }) + + Convey("When trying to update permissions should return false", func() { + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeFalse) + }) + }) + }) +} + +type scenarioContext struct { + g DashboardGuardian +} + +type scenarioFunc func(c *scenarioContext) + +func orgRoleScenario(desc string, role m.RoleType, fn scenarioFunc) { + user := &m.SignedInUser{ + UserId: 1, + OrgId: 1, + OrgRole: role, + } + guard := New(1, 1, user) + sc := &scenarioContext{ + g: guard, + } + + Convey(desc, func() { + fn(sc) + }) +} + +func permissionScenario(desc string, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) { + bus.ClearBusHandlers() + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = permissions + return nil + }) + + teams := []*m.Team{} + + for _, p := range permissions { + if p.TeamId > 0 { + teams = append(teams, &m.Team{Id: p.TeamId}) + } + } + + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { + query.Result = teams + return nil + }) + + Convey(desc, func() { + fn(sc) + }) +} + +func userWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) { + p := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 1, UserId: 1, Permission: permission}, + } + permissionScenario(fmt.Sprintf("and user has permission to %s item", permission), sc, p, fn) +} + +func teamWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) { + p := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: permission}, + } + permissionScenario(fmt.Sprintf("and team has permission to %s item", permission), sc, p, fn) +} + +func everyoneWithRoleScenario(role m.RoleType, permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) { + p := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 1, UserId: -1, Role: &role, Permission: permission}, + } + permissionScenario(fmt.Sprintf("and everyone with %s role can %s item", role, permission), sc, p, fn) +} From 3c14cecd506feb99711efaac0580e0ab6c134c26 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 26 Feb 2018 20:14:21 +0100 Subject: [PATCH 55/65] folders: handle new guardian error responses and add tests --- pkg/api/folder_permission.go | 17 +++-- pkg/api/folder_permission_test.go | 119 +++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 7453552d092..c4c762dc3cd 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -19,13 +19,13 @@ func GetFolderPermissionList(c *middleware.Context) Response { return toFolderError(err) } - guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser) + g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) - if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return toFolderError(m.ErrFolderAccessDenied) } - acl, err := guardian.GetAcl() + acl, err := g.GetAcl() if err != nil { return ApiError(500, "Failed to get folder permissions", err) } @@ -50,8 +50,8 @@ func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardA return toFolderError(err) } - guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser) - canAdmin, err := guardian.CanAdmin() + g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) + canAdmin, err := g.CanAdmin() if err != nil { return toFolderError(err) } @@ -76,8 +76,13 @@ func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardA }) } - if okToUpdate, err := guardian.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { + if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { if err != nil { + if err == guardian.ErrGuardianDuplicatePermission || + err == guardian.ErrGuardianOverrideLowerPresedence { + return ApiError(400, err.Error(), err) + } + return ApiError(500, "Error while checking folder permissions", err) } diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index bbae5390b80..bc35031ce52 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" @@ -15,6 +16,35 @@ import ( func TestFolderPermissionApiEndpoint(t *testing.T) { Convey("Folder permissions test", t, func() { + Convey("Given folder not exists", func() { + mock := &fakeFolderService{ + GetFolderByUidError: m.ErrFolderNotFound, + } + + 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, 404) + }) + + 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, 404) + }) + + Reset(func() { + dashboards.NewFolderService = origNewFolderService + }) + }) + Convey("Given user has no admin permissions", func() { origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) @@ -54,7 +84,17 @@ func TestFolderPermissionApiEndpoint(t *testing.T) { Convey("Given user has admin permissions and permissions to update", func() { origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: true, CheckPermissionBeforeUpdateValue: true}) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: true, + GetAclValue: []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, + {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, + }, + }) mock := &fakeFolderService{ GetFolderByUidResult: &m.Folder{ @@ -70,6 +110,11 @@ func TestFolderPermissionApiEndpoint(t *testing.T) { 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) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(len(respJSON.MustArray()), ShouldEqual, 5) + So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2) + So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW) }) cmd := dtos.UpdateDashboardAclCommand{ @@ -88,6 +133,78 @@ func TestFolderPermissionApiEndpoint(t *testing.T) { dashboards.NewFolderService = origNewFolderService }) }) + + Convey("When trying to update permissions with duplicate permissions", func() { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: false, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianDuplicatePermission, + }) + + mock := &fakeFolderService{ + GetFolderByUidResult: &m.Folder{ + Id: 1, + Uid: "uid", + Title: "Folder", + }, + } + + origNewFolderService := dashboards.NewFolderService + mockFolderService(mock) + + 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, 400) + }) + + Reset(func() { + guardian.New = origNewGuardian + dashboards.NewFolderService = origNewFolderService + }) + }) + + Convey("When trying to override inherited permissions with lower presedence", func() { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: false, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverrideLowerPresedence}, + ) + + mock := &fakeFolderService{ + GetFolderByUidResult: &m.Folder{ + Id: 1, + Uid: "uid", + Title: "Folder", + }, + } + + origNewFolderService := dashboards.NewFolderService + mockFolderService(mock) + + 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, 400) + }) + + Reset(func() { + guardian.New = origNewGuardian + dashboards.NewFolderService = origNewFolderService + }) + }) }) } From 03f8eff8800176759fcb971cfadcb55b6746534b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 26 Feb 2018 20:15:57 +0100 Subject: [PATCH 56/65] dashboards: handle new guardian error responses and update tests Using a mocked guardian instead and relies on the actual guardian tests for verifying permission handling correctness --- pkg/api/dashboard_permission.go | 17 +- pkg/api/dashboard_permission_test.go | 290 +++++++++++++-------------- 2 files changed, 154 insertions(+), 153 deletions(-) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 351b81885d6..dafa5777803 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -18,13 +18,13 @@ func GetDashboardPermissionList(c *middleware.Context) Response { return rsp } - guardian := guardian.New(dashId, c.OrgId, c.SignedInUser) + g := guardian.New(dashId, c.OrgId, c.SignedInUser) - if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) } - acl, err := guardian.GetAcl() + acl, err := g.GetAcl() if err != nil { return ApiError(500, "Failed to get dashboard permissions", err) } @@ -46,8 +46,8 @@ func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboa return rsp } - guardian := guardian.New(dashId, c.OrgId, c.SignedInUser) - if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + g := guardian.New(dashId, c.OrgId, c.SignedInUser) + if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) } @@ -67,8 +67,13 @@ func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboa }) } - if okToUpdate, err := guardian.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { + if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { if err != nil { + if err == guardian.ErrGuardianDuplicatePermission || + err == guardian.ErrGuardianOverrideLowerPresedence { + return ApiError(400, err.Error(), err) + } + return ApiError(500, "Error while checking dashboard permissions", err) } diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index fd399cfb096..0bb9908df6d 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -8,183 +8,180 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/guardian" . "github.com/smartystreets/goconvey/convey" ) 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}, - {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, - {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, - {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, - } - dtoRes := transformDashboardAclsToDTOs(mockResult) - - getDashboardQueryResult := m.NewDashboard("Dash") - var getDashboardNotFoundError error - - bus.AddHandler("test", func(query *m.GetDashboardQuery) error { - query.Result = getDashboardQueryResult - return getDashboardNotFoundError - }) - - bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { - query.Result = dtoRes - return nil - }) - - bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { - query.Result = mockResult - return nil - }) - - teamResp := []*m.Team{} - bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = teamResp - return nil - }) - - // This tests four scenarios: - // 1. user is an org admin - // 2. user is an org editor AND has been granted admin permission for the dashboard - // 3. user is an org viewer AND has been granted edit permission for the dashboard - // 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/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() - - So(sc.resp.Code, ShouldEqual, 200) - - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(len(respJSON.MustArray()), ShouldEqual, 5) - So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2) - So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW) - }) + Convey("Dashboard permissions test", t, func() { + Convey("Given dashboard not exists", func() { + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + return m.ErrDashboardNotFound }) - 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() - - Convey("Should not be able to access ACL", func() { - So(sc.resp.Code, ShouldEqual, 404) - }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) { + callGetDashboardPermissions(sc) + So(sc.resp.Code, ShouldEqual, 404) }) - Convey("Should not be able to update permissions for non-existing dashboard", func() { - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserId: 1000, Permission: m.PERMISSION_ADMIN}, - }, - } + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: m.PERMISSION_ADMIN}, + }, + } - 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) - }) + updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + callUpdateDashboardPermissions(sc) + So(sc.resp.Code, ShouldEqual, 404) }) }) - Convey("When user is org editor and has admin permission in the ACL", func() { - 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("Given user has no admin permissions", func() { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) - Convey("Should be able to access ACL", func() { - sc.handlerFunc = GetDashboardPermissionList - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 200) - }) + getDashboardQueryResult := m.NewDashboard("Dash") + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return nil }) - Convey("Should not be able to downgrade their own Admin permission", func() { - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserId: TestUserID, Permission: m.PERMISSION_EDIT}, - }, - } - - 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) - So(sc.resp.Code, ShouldEqual, 403) - }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) { + callGetDashboardPermissions(sc) + So(sc.resp.Code, ShouldEqual, 403) }) - Convey("Should be able to update permissions", func() { - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserId: TestUserID, Permission: m.PERMISSION_ADMIN}, - {UserId: 2, Permission: m.PERMISSION_EDIT}, - }, - } + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: m.PERMISSION_ADMIN}, + }, + } - 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) - So(sc.resp.Code, ShouldEqual, 200) - }) + updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + callUpdateDashboardPermissions(sc) + So(sc.resp.Code, ShouldEqual, 403) }) - }) - - Convey("When user is org viewer and has edit permission in the ACL", func() { - 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 - Convey("Should not be able to get list of permissions from ACL", func() { - sc.handlerFunc = GetDashboardPermissionList - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 403) - }) + Reset(func() { + guardian.New = origNewGuardian }) }) - Convey("When user is org editor and not in the ACL", func() { - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardsId/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) { + Convey("Given user has admin permissions and permissions to update", func() { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: true, + GetAclValue: []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, + {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, + }, + }) - Convey("Should not be able to access ACL", func() { - sc.handlerFunc = GetDashboardPermissionList - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + getDashboardQueryResult := m.NewDashboard("Dash") + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return nil + }) - So(sc.resp.Code, ShouldEqual, 403) - }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) { + callGetDashboardPermissions(sc) + So(sc.resp.Code, ShouldEqual, 200) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(len(respJSON.MustArray()), ShouldEqual, 5) + So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2) + So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW) + }) + + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: m.PERMISSION_ADMIN}, + }, + } + + updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + callUpdateDashboardPermissions(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + Reset(func() { + guardian.New = origNewGuardian + }) + }) + + Convey("When trying to update permissions with duplicate permissions", func() { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: false, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianDuplicatePermission, + }) + + getDashboardQueryResult := m.NewDashboard("Dash") + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return nil + }) + + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: m.PERMISSION_ADMIN}, + }, + } + + updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + callUpdateDashboardPermissions(sc) + So(sc.resp.Code, ShouldEqual, 400) + }) + + Reset(func() { + guardian.New = origNewGuardian + }) + }) + + Convey("When trying to override inherited permissions with lower presedence", func() { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: false, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverrideLowerPresedence}, + ) + + getDashboardQueryResult := m.NewDashboard("Dash") + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return nil + }) + + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: m.PERMISSION_ADMIN}, + }, + } + + updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + callUpdateDashboardPermissions(sc) + So(sc.resp.Code, ShouldEqual, 400) + }) + + Reset(func() { + guardian.New = origNewGuardian }) }) }) } -func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardAclInfoDTO { - dtos := make([]*m.DashboardAclInfoDTO, 0) - - for _, acl := range acls { - dto := &m.DashboardAclInfoDTO{ - OrgId: acl.OrgId, - DashboardId: acl.DashboardId, - Permission: acl.Permission, - UserId: acl.UserId, - TeamId: acl.TeamId, - } - dtos = append(dtos, dto) - } - - return dtos +func callGetDashboardPermissions(sc *scenarioContext) { + sc.handlerFunc = GetDashboardPermissionList + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func CallPostAcl(sc *scenarioContext) { +func callUpdateDashboardPermissions(sc *scenarioContext) { bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error { return nil }) @@ -192,7 +189,7 @@ func CallPostAcl(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } -func postAclScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) { +func updateDashboardPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() @@ -200,9 +197,8 @@ func postAclScenario(desc string, url string, routePattern string, role m.RoleTy sc.defaultHandler = wrap(func(c *middleware.Context) Response { sc.context = c - sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID - sc.context.OrgRole = role + sc.context.UserId = TestUserID return UpdateDashboardPermissions(c, cmd) }) From f76b98d252b70b10a716c9e6329d8a9917006280 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 27 Feb 2018 16:03:11 +0100 Subject: [PATCH 57/65] dashboards: change dashboard/folder permission error messages --- pkg/api/dashboard_permission.go | 4 +-- pkg/api/dashboard_permission_test.go | 4 +-- pkg/api/folder_permission.go | 4 +-- pkg/api/folder_permission_test.go | 4 +-- pkg/services/guardian/guardian.go | 8 +++--- pkg/services/guardian/guardian_test.go | 36 +++++++++++++------------- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index dafa5777803..419825644c8 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -69,8 +69,8 @@ func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboa if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { if err != nil { - if err == guardian.ErrGuardianDuplicatePermission || - err == guardian.ErrGuardianOverrideLowerPresedence { + if err == guardian.ErrGuardianPermissionExists || + err == guardian.ErrGuardianOverride { return ApiError(400, err.Error(), err) } diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 0bb9908df6d..03231338268 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -119,7 +119,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianDuplicatePermission, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, }) getDashboardQueryResult := m.NewDashboard("Dash") @@ -149,7 +149,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverrideLowerPresedence}, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, ) getDashboardQueryResult := m.NewDashboard("Dash") diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index c4c762dc3cd..7c8aba87337 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -78,8 +78,8 @@ func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardA if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { if err != nil { - if err == guardian.ErrGuardianDuplicatePermission || - err == guardian.ErrGuardianOverrideLowerPresedence { + if err == guardian.ErrGuardianPermissionExists || + err == guardian.ErrGuardianOverride { return ApiError(400, err.Error(), err) } diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index bc35031ce52..552577963b4 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -139,7 +139,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianDuplicatePermission, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, }) mock := &fakeFolderService{ @@ -175,7 +175,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverrideLowerPresedence}, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, ) mock := &fakeFolderService{ diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 972e87de39b..54b5386f40d 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -10,8 +10,8 @@ import ( ) var ( - ErrGuardianDuplicatePermission = errors.New("You cannot add multiple permissions for a user, team or role") - ErrGuardianOverrideLowerPresedence = errors.New("You cannot override a permission with a lower presedence permission") + ErrGuardianPermissionExists = errors.New("This permission already exists") + ErrGuardianOverride = errors.New("You can only override a permission to be higher") ) // DashboardGuardian to be used for guard against operations without access on dashboard and acl @@ -133,7 +133,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == p.UserId && a.TeamId == p.TeamId && a.Role == p.Role) || (a.UserId > 0 && a.UserId == p.UserId) || (a.TeamId > 0 && a.TeamId == p.TeamId) { - return false, ErrGuardianDuplicatePermission + return false, ErrGuardianPermissionExists } } @@ -154,7 +154,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == existingPerm.UserId && a.TeamId == existingPerm.TeamId && *a.Role == *existingPerm.Role && a.Permission <= existingPerm.Permission) || (a.UserId > 0 && a.UserId == existingPerm.UserId && a.Permission <= existingPerm.Permission) || (a.TeamId > 0 && a.TeamId == existingPerm.TeamId && a.Permission <= existingPerm.Permission) { - return false, ErrGuardianOverrideLowerPresedence + return false, ErrGuardianOverride } } } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index d420d1add97..0da14a51c5c 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -29,7 +29,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianDuplicatePermission) + So(err, ShouldEqual, ErrGuardianPermissionExists) }) Convey("With duplicate team/role permissions should return error", func() { @@ -38,7 +38,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianDuplicatePermission) + So(err, ShouldEqual, ErrGuardianPermissionExists) }) Convey("With duplicate everyone/role permissions should return error", func() { @@ -47,7 +47,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianDuplicatePermission) + So(err, ShouldEqual, ErrGuardianPermissionExists) }) }) @@ -66,7 +66,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with edit user permission should return error", func() { @@ -74,7 +74,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with view user permission should return error", func() { @@ -82,7 +82,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -109,7 +109,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with view user permission should return error", func() { @@ -117,7 +117,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -152,7 +152,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -171,7 +171,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with edit team permission should return error", func() { @@ -179,7 +179,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with view team permission should return error", func() { @@ -187,7 +187,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -214,7 +214,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with view team permission should return error", func() { @@ -222,7 +222,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -257,7 +257,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -285,7 +285,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) Convey("When trying to update dashboard permissions with everyone with editor role can view permission should return error", func() { @@ -293,7 +293,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) @@ -329,7 +329,7 @@ func TestGuardian(t *testing.T) { {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) - So(err, ShouldEqual, ErrGuardianOverrideLowerPresedence) + So(err, ShouldEqual, ErrGuardianOverride) }) }) }) From 71a0a298bf7a5010bd9b6dfdebc692d3ea631098 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 27 Feb 2018 16:04:26 +0100 Subject: [PATCH 58/65] permissions: remove client validation and handle server validation --- .../Permissions/AddPermissions.jest.tsx | 2 +- .../components/Permissions/AddPermissions.tsx | 8 -- .../PermissionsStore/PermissionsStore.jest.ts | 75 +++---------------- .../PermissionsStore/PermissionsStore.ts | 51 +++++-------- 4 files changed, 29 insertions(+), 107 deletions(-) diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.jest.tsx index 9c01eee70b1..fe97c4c7e62 100644 --- a/public/app/core/components/Permissions/AddPermissions.jest.tsx +++ b/public/app/core/components/Permissions/AddPermissions.jest.tsx @@ -17,7 +17,7 @@ describe('AddPermissions', () => { ]) ); - backendSrv.post = jest.fn(); + backendSrv.post = jest.fn(() => Promise.resolve({})); store = RootStore.create( {}, diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index 94afa7c1180..07ccfdbbef5 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -135,14 +135,6 @@ class AddPermissions extends Component { - {permissions.error ? ( -
- - - {permissions.error} - -
- ) : null} ); } diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index f7332516bef..c3bc6016e50 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -1,10 +1,10 @@ -import { PermissionsStore, aclTypeValues } from './PermissionsStore'; +import { PermissionsStore } from './PermissionsStore'; import { backendSrv } from 'test/mocks/common'; describe('PermissionsStore', () => { let store; - beforeEach(() => { + beforeEach(async () => { backendSrv.get.mockReturnValue( Promise.resolve([ { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, @@ -20,7 +20,7 @@ describe('PermissionsStore', () => { ]) ); - backendSrv.post = jest.fn(); + backendSrv.post = jest.fn(() => Promise.resolve({})); store = PermissionsStore.create( { @@ -32,14 +32,14 @@ describe('PermissionsStore', () => { } ); - return store.load(1, false, false); + await store.load(1, false, false); }); - it('should save update on permission change', () => { + it('should save update on permission change', async () => { expect(store.items[0].permission).toBe(1); expect(store.items[0].permissionName).toBe('View'); - store.updatePermissionOnIndex(0, 2, 'Edit'); + await store.updatePermissionOnIndex(0, 2, 'Edit'); expect(store.items[0].permission).toBe(2); expect(store.items[0].permissionName).toBe('Edit'); @@ -47,69 +47,18 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); }); - it('should save removed permissions automatically', () => { + it('should save removed permissions automatically', async () => { expect(store.items.length).toBe(3); - store.removeStoreItem(2); + await store.removeStoreItem(2); 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/permissions'); }); - describe('when duplicate team permissions are added', () => { - beforeEach(() => { - const newItem = { - teamId: 10, - team: 'tester-team', - permission: 1, - dashboardId: 1, - }; - store.resetNewType(); - store.newItem.setTeam(newItem.teamId, newItem.team); - store.newItem.setPermission(newItem.permission); - store.addStoreItem(); - - store.newItem.setTeam(newItem.teamId, newItem.team); - store.newItem.setPermission(newItem.permission); - store.addStoreItem(); - }); - - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); - - describe('when duplicate user permissions are added', () => { - beforeEach(() => { - expect(store.items.length).toBe(3); - const newItem = { - userId: 10, - userLogin: 'tester1', - permission: 1, - dashboardId: 1, - }; - store.setNewType(aclTypeValues.USER.value); - store.newItem.setUser(newItem.userId, newItem.userLogin); - store.newItem.setPermission(newItem.permission); - store.addStoreItem(); - store.setNewType(aclTypeValues.USER.value); - store.newItem.setUser(newItem.userId, newItem.userLogin); - store.newItem.setPermission(newItem.permission); - store.addStoreItem(); - }); - - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); - describe('when one inherited and one not inherited team permission are added', () => { - beforeEach(() => { + beforeEach(async () => { const overridingItemForChildDashboard = { team: 'MyTestTeam', dashboardId: 1, @@ -120,11 +69,7 @@ describe('PermissionsStore', () => { store.resetNewType(); store.newItem.setTeam(overridingItemForChildDashboard.teamId, overridingItemForChildDashboard.team); store.newItem.setPermission(overridingItemForChildDashboard.permission); - store.addStoreItem(); - }); - - it('should allowing overriding the inherited permission and not throw a validation error', () => { - expect(store.error).toBe(null); + await store.addStoreItem(); }); it('should add new overriding permission', () => { diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index 88b26a2886f..79df593f06e 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -1,8 +1,6 @@ import { types, getEnv, flow } from 'mobx-state-tree'; import { PermissionsStoreItem } from './PermissionsStoreItem'; -const duplicateError = 'This permission exists already.'; - export const permissionOptions = [ { value: 1, label: 'View', description: 'Can view dashboards.' }, { value: 2, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, @@ -75,7 +73,6 @@ export const PermissionsStore = types isFolder: types.maybe(types.boolean), dashboardId: types.maybe(types.number), items: types.optional(types.array(PermissionsStoreItem), []), - error: types.maybe(types.string), originalItems: types.optional(types.array(PermissionsStoreItem), []), newType: types.optional(types.string, defaultNewType), newItem: types.maybe(NewPermissionsItem), @@ -88,7 +85,6 @@ export const PermissionsStore = types return isDuplicate(it, item); }); if (dupe) { - self.error = duplicateError; return false; } @@ -96,8 +92,7 @@ export const PermissionsStore = types }, })) .actions(self => { - const resetNewType = () => { - self.error = null; + const resetNewTypeInternal = () => { self.newItem = NewPermissionsItem.create(); }; @@ -115,11 +110,9 @@ export const PermissionsStore = types self.items = items; self.originalItems = items; self.fetching = false; - self.error = null; }), addStoreItem: flow(function* addStoreItem() { - self.error = null; let item = { type: self.newItem.type, permission: self.newItem.permission, @@ -147,19 +140,21 @@ export const PermissionsStore = types throw Error('Unknown type: ' + self.newItem.type); } - if (!self.isValid(item)) { - return undefined; - } + const updatedItems = self.items.peek(); + const newItem = prepareItem(item, self.dashboardId, self.isFolder, self.isInRoot); + updatedItems.push(newItem); - self.items.push(prepareItem(item, self.dashboardId, self.isFolder, self.isInRoot)); - resetNewType(); - return updateItems(self); + try { + yield updateItems(self, updatedItems); + self.items.push(newItem); + resetNewTypeInternal(); + } catch {} + yield Promise.resolve(); }), removeStoreItem: flow(function* removeStoreItem(idx: number) { - self.error = null; self.items.splice(idx, 1); - return updateItems(self); + yield updateItems(self, self.items.peek()); }), updatePermissionOnIndex: flow(function* updatePermissionOnIndex( @@ -167,9 +162,8 @@ export const PermissionsStore = types permission: number, permissionName: string ) { - self.error = null; self.items[idx].updatePermission(permission, permissionName); - return updateItems(self); + yield updateItems(self, self.items.peek()); }), setNewType(newType: string) { @@ -177,7 +171,7 @@ export const PermissionsStore = types }, resetNewType() { - resetNewType(); + resetNewTypeInternal(); }, toggleAddPermissions() { @@ -190,12 +184,10 @@ export const PermissionsStore = types }; }); -const updateItems = self => { - self.error = null; - +const updateItems = (self, items) => { const backendSrv = getEnv(self).backendSrv; const updated = []; - for (let item of self.items) { + for (let item of items) { if (item.inherited) { continue; } @@ -208,16 +200,9 @@ const updateItems = self => { }); } - let res; - try { - res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, { - items: updated, - }); - } catch (error) { - self.error = error; - } - - return res; + return backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, { + items: updated, + }); }; const prepareServerResponse = (response, dashboardId: number, isFolder: boolean, isInRoot: boolean) => { From ecf438f8d01a521c63129164783fd3c77abafd73 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 27 Feb 2018 17:53:30 +0100 Subject: [PATCH 59/65] dashboard: add permission check for diff api route ref #10770 --- pkg/api/dashboard.go | 16 +++++++-- pkg/api/dashboard_test.go | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 88502804919..53174075c25 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -411,6 +411,18 @@ func GetDashboardVersion(c *middleware.Context) Response { // POST /api/dashboards/calculate-diff performs diffs on two dashboards func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiffOptions) Response { + guardianBase := guardian.New(apiOptions.Base.DashboardId, c.OrgId, c.SignedInUser) + if canSave, err := guardianBase.CanSave(); err != nil || !canSave { + return dashboardGuardianResponse(err) + } + + if apiOptions.Base.DashboardId != apiOptions.New.DashboardId { + guardianNew := guardian.New(apiOptions.New.DashboardId, c.OrgId, c.SignedInUser) + if canSave, err := guardianNew.CanSave(); err != nil || !canSave { + return dashboardGuardianResponse(err) + } + } + options := dashdiffs.Options{ OrgId: c.OrgId, DiffType: dashdiffs.ParseDiffType(apiOptions.DiffType), @@ -436,9 +448,9 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff if options.DiffType == dashdiffs.DiffDelta { return Respond(200, result.Delta).Header("Content-Type", "application/json") - } else { - return Respond(200, result.Delta).Header("Content-Type", "text/html") } + + return Respond(200, result.Delta).Header("Content-Type", "text/html") } // RestoreDashboardVersion restores a dashboard to the given version. diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 734df3da00d..3857fa0b9e1 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -743,6 +743,53 @@ func TestDashboardApiEndpoint(t *testing.T) { } }) }) + + Convey("Given two dashboards being compared", t, func() { + mockResult := []*m.DashboardAclInfoDTO{} + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + + bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error { + query.Result = &m.DashboardVersion{ + Data: simplejson.NewFromAny(map[string]interface{}{ + "title": "Dash" + string(query.DashboardId), + }), + } + return nil + }) + + cmd := dtos.CalculateDiffOptions{ + Base: dtos.CalculateDiffTarget{ + DashboardId: 1, + Version: 1, + }, + New: dtos.CalculateDiffTarget{ + DashboardId: 2, + Version: 2, + }, + DiffType: "basic", + } + + Convey("when user does not have permission", func() { + role := m.ROLE_VIEWER + + postDiffScenario("When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + Convey("when user does have permission", func() { + role := m.ROLE_ADMIN + + postDiffScenario("When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + }) } func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { @@ -835,6 +882,28 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d }) } +func postDiffScenario(desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role m.RoleType, 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, + } + sc.context.OrgRole = role + + return CalculateDashboardDiff(c, cmd) + }) + + sc.m.Post(routePattern, sc.defaultHandler) + + fn(sc) + }) +} + func (sc *scenarioContext) ToJson() *simplejson.Json { var result *simplejson.Json err := json.NewDecoder(sc.resp.Body).Decode(&result) From f44e476580d7b4fe0ca06be802dde4689131820e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 28 Feb 2018 08:48:28 +0100 Subject: [PATCH 60/65] permissions: fix validation of permissions before update Did a bad pointer comparison so extended the tests for duplicate permissions. --- pkg/models/dashboard_acl.go | 21 +++++++++ pkg/services/guardian/guardian.go | 33 ++++++++++---- pkg/services/guardian/guardian_test.go | 62 +++++++++++++++++++++++--- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 1fbd2b451b9..5b91b2a70b4 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -68,6 +68,27 @@ type DashboardAclInfoDTO struct { Url string `json:"url"` } +func (dto *DashboardAclInfoDTO) hasSameRoleAs(other *DashboardAclInfoDTO) bool { + if dto.Role == nil || other.Role == nil { + return false + } + + return dto.UserId <= 0 && dto.TeamId <= 0 && dto.UserId == other.UserId && dto.TeamId == other.TeamId && *dto.Role == *other.Role +} + +func (dto *DashboardAclInfoDTO) hasSameUserAs(other *DashboardAclInfoDTO) bool { + return dto.UserId > 0 && dto.UserId == other.UserId +} + +func (dto *DashboardAclInfoDTO) hasSameTeamAs(other *DashboardAclInfoDTO) bool { + return dto.TeamId > 0 && dto.TeamId == other.TeamId +} + +// IsDuplicateOf returns true if other item has same role, same user or same team +func (dto *DashboardAclInfoDTO) IsDuplicateOf(other *DashboardAclInfoDTO) bool { + return dto.hasSameRoleAs(other) || dto.hasSameUserAs(other) || dto.hasSameTeamAs(other) +} + // // COMMANDS // diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 54b5386f40d..811b38cac86 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -10,7 +10,7 @@ import ( ) var ( - ErrGuardianPermissionExists = errors.New("This permission already exists") + ErrGuardianPermissionExists = errors.New("Permission already exists") ErrGuardianOverride = errors.New("You can only override a permission to be higher") ) @@ -127,17 +127,23 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) { acl := []*m.DashboardAclInfoDTO{} + adminRole := m.ROLE_ADMIN + everyoneWithAdminRole := &m.DashboardAclInfoDTO{DashboardId: g.dashId, UserId: 0, TeamId: 0, Role: &adminRole, Permission: m.PERMISSION_ADMIN} + // validate that duplicate permissions don't exists for _, p := range updatePermissions { + aclItem := &m.DashboardAclInfoDTO{DashboardId: p.DashboardId, UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission} + if aclItem.IsDuplicateOf(everyoneWithAdminRole) { + return false, ErrGuardianPermissionExists + } + for _, a := range acl { - if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == p.UserId && a.TeamId == p.TeamId && a.Role == p.Role) || - (a.UserId > 0 && a.UserId == p.UserId) || - (a.TeamId > 0 && a.TeamId == p.TeamId) { + if a.IsDuplicateOf(aclItem) { return false, ErrGuardianPermissionExists } } - acl = append(acl, &m.DashboardAclInfoDTO{DashboardId: p.DashboardId, UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}) + acl = append(acl, aclItem) } existingPermissions, err := g.GetAcl() @@ -145,15 +151,19 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss return false, err } + // validate overridden permissions to be higher for _, a := range acl { for _, existingPerm := range existingPermissions { + // handle default permissions + if existingPerm.DashboardId == -1 { + existingPerm.DashboardId = g.dashId + } + if a.DashboardId == existingPerm.DashboardId { continue } - if (a.UserId <= 0 && a.TeamId <= 0 && a.UserId == existingPerm.UserId && a.TeamId == existingPerm.TeamId && *a.Role == *existingPerm.Role && a.Permission <= existingPerm.Permission) || - (a.UserId > 0 && a.UserId == existingPerm.UserId && a.Permission <= existingPerm.Permission) || - (a.TeamId > 0 && a.TeamId == existingPerm.TeamId && a.Permission <= existingPerm.Permission) { + if a.IsDuplicateOf(existingPerm) && a.Permission <= existingPerm.Permission { return false, ErrGuardianOverride } } @@ -177,6 +187,13 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) { return nil, err } + for _, a := range query.Result { + // handle default permissions + if a.DashboardId == -1 { + a.DashboardId = g.dashId + } + } + g.acl = query.Result return g.acl, nil } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index 0da14a51c5c..bb7e6bd1a72 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -23,7 +23,7 @@ func TestGuardian(t *testing.T) { So(canView, ShouldBeTrue) Convey("When trying to update permissions", func() { - Convey("With duplicate user/role permissions should return error", func() { + Convey("With duplicate user permissions should return error", func() { p := []*m.DashboardAcl{ {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW}, {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}, @@ -32,7 +32,7 @@ func TestGuardian(t *testing.T) { So(err, ShouldEqual, ErrGuardianPermissionExists) }) - Convey("With duplicate team/role permissions should return error", func() { + Convey("With duplicate team permissions should return error", func() { p := []*m.DashboardAcl{ {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN}, @@ -41,14 +41,66 @@ func TestGuardian(t *testing.T) { So(err, ShouldEqual, ErrGuardianPermissionExists) }) - Convey("With duplicate everyone/role permissions should return error", func() { + Convey("With duplicate everyone with editor role permission should return error", func() { + r := m.ROLE_EDITOR p := []*m.DashboardAcl{ - {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_VIEW}, - {OrgId: 1, DashboardId: 1, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN}, } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) So(err, ShouldEqual, ErrGuardianPermissionExists) }) + + Convey("With duplicate everyone with viewer role permission should return error", func() { + r := m.ROLE_VIEWER + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianPermissionExists) + }) + + Convey("With everyone with admin role permission should return error", func() { + r := m.ROLE_ADMIN + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN}, + } + _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(err, ShouldEqual, ErrGuardianPermissionExists) + }) + }) + + Convey("Given default permissions", func() { + editor := m.ROLE_EDITOR + viewer := m.ROLE_VIEWER + existingPermissions := []*m.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: -1, Role: &editor, Permission: m.PERMISSION_EDIT}, + {OrgId: 1, DashboardId: -1, Role: &viewer, Permission: m.PERMISSION_VIEW}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = existingPermissions + return nil + }) + + Convey("When trying to update dashboard permissions without everyone with role editor can edit should be allowed", func() { + r := m.ROLE_VIEWER + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) + + Convey("When trying to update dashboard permissions without everyone with role viewer can view should be allowed", func() { + r := m.ROLE_EDITOR + p := []*m.DashboardAcl{ + {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_EDIT}, + } + ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p) + So(ok, ShouldBeTrue) + }) }) Convey("Given parent folder has user admin permission", func() { From 576deb37e8e309738cb7af7877335ac7bc765180 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 28 Feb 2018 13:01:35 +0100 Subject: [PATCH 61/65] login: hide sign up if configured so. Fixes #11041 --- public/app/partials/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 54c59c04429..8680924977f 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -59,7 +59,7 @@ Sign in with {{oauth.generic_oauth.name}} -