diff --git a/pkg/api/api.go b/pkg/api/api.go index 4f4163bde77..63044fb4e85 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -250,6 +250,8 @@ func (hs *HttpServer) registerRoutes() { r.Group("/:id/acl", func() { r.Get("/", wrap(GetDashboardAcl)) + r.Delete("/user/:userId", wrap(DeleteDashboardAclByUser)) + r.Delete("/user-group/:userGroupId", wrap(DeleteDashboardAclByUserGroup)) }, reqSignedIn) }) diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go index 72e15c13239..33f52b4064c 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_acl.go @@ -29,3 +29,45 @@ func GetDashboardAcl(c *middleware.Context) Response { return Json(200, &query.Result) } + +func DeleteDashboardAclByUser(c *middleware.Context) Response { + dashboardId := c.ParamsInt64(":id") + userId := c.ParamsInt64(":userId") + cmd := m.RemoveDashboardPermissionCommand{DashboardId: dashboardId, UserId: userId, OrgId: c.OrgId} + + hasPermission, err := guardian.CanDeleteFromAcl(dashboardId, c.OrgRole, c.IsGrafanaAdmin, c.OrgId, c.UserId) + if err != nil { + return ApiError(500, "Failed to delete from Dashboard ACL", err) + } + + if !hasPermission { + return Json(403, util.DynMap{"status": "Forbidden", "message": "Does not have access to this Dashboard ACL"}) + } + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to delete permission for user", err) + } + + return Json(200, "") +} + +func DeleteDashboardAclByUserGroup(c *middleware.Context) Response { + dashboardId := c.ParamsInt64(":id") + userGroupId := c.ParamsInt64(":userGroupId") + cmd := m.RemoveDashboardPermissionCommand{DashboardId: dashboardId, UserGroupId: userGroupId, OrgId: c.OrgId} + + hasPermission, err := guardian.CanDeleteFromAcl(dashboardId, c.OrgRole, c.IsGrafanaAdmin, c.OrgId, c.UserId) + if err != nil { + return ApiError(500, "Failed to delete from Dashboard ACL", err) + } + + if !hasPermission { + return Json(403, util.DynMap{"status": "Forbidden", "message": "Does not have access to this Dashboard ACL"}) + } + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to delete permission for user", err) + } + + return Json(200, "") +} diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_acl_test.go index 26636ad87f6..08fb8ef44e1 100644 --- a/pkg/api/dashboard_acl_test.go +++ b/pkg/api/dashboard_acl_test.go @@ -15,6 +15,8 @@ func TestDashboardAclApiEndpoint(t *testing.T) { mockResult := []*models.DashboardAclInfoDTO{ {Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permissions: models.PERMISSION_EDIT}, {Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permissions: models.PERMISSION_VIEW}, + {Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permissions: models.PERMISSION_EDIT}, + {Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permissions: models.PERMISSION_READ_ONLY_EDIT}, } bus.AddHandler("test", func(query *models.GetDashboardPermissionsQuery) error { query.Result = mockResult @@ -22,7 +24,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { }) Convey("When user is org admin", func() { - loggedInUserScenarioWithRole("When calling GET on", "/api/dashboard/1/acl", models.ROLE_ADMIN, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/1/acl", "/api/dashboards/:id/acl", models.ROLE_ADMIN, func(sc *scenarioContext) { Convey("Should be able to access ACL", func() { sc.handlerFunc = GetDashboardAcl sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() @@ -37,14 +39,68 @@ func TestDashboardAclApiEndpoint(t *testing.T) { }) }) - Convey("When user is editor and not in the ACL", func() { - loggedInUserScenarioWithRole("When calling GET on", "/api/dashboard/1/acl", models.ROLE_EDITOR, func(sc *scenarioContext) { + Convey("When user is editor and in the ACL", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/1/acl", "/api/dashboards/:id/acl", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockResult = append(mockResult, &models.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_EDIT}) bus.AddHandler("test2", func(query *models.GetAllowedDashboardsQuery) error { query.Result = []int64{1} return nil }) + Convey("Should be able to access ACL", func() { + sc.handlerFunc = GetDashboardAcl + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/1/acl/user/1", "/api/dashboards/:id/acl/user/:userId", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockResult = append(mockResult, &models.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_EDIT}) + + bus.AddHandler("test3", func(cmd *models.RemoveDashboardPermissionCommand) error { + return nil + }) + + Convey("Should be able to delete permission", func() { + sc.handlerFunc = DeleteDashboardAclByUser + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + Convey("When user is a member of a user group in the ACL with edit permission", func() { + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/1/acl/user/1", "/api/dashboards/:id/acl/user/:userId", models.ROLE_EDITOR, func(sc *scenarioContext) { + + bus.AddHandler("test3", func(query *models.GetUserGroupsByUserQuery) error { + query.Result = []*models.UserGroup{{Id: 1, OrgId: 1, Name: "UG1"}} + return nil + }) + + bus.AddHandler("test3", func(cmd *models.RemoveDashboardPermissionCommand) error { + return nil + }) + + Convey("Should be able to delete permission", func() { + sc.handlerFunc = DeleteDashboardAclByUser + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + }) + }) + + Convey("When user is editor and not in the ACL", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/1/acl", "/api/dashboards/:id/acl", models.ROLE_EDITOR, func(sc *scenarioContext) { + + bus.AddHandler("test2", func(query *models.GetAllowedDashboardsQuery) error { + query.Result = []int64{} + return nil + }) + Convey("Should not be able to access ACL", func() { sc.handlerFunc = GetDashboardAcl sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() @@ -52,6 +108,20 @@ func TestDashboardAclApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/1/acl/user/1", "/api/dashboards/:id/acl/user/:userId", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockResult = append(mockResult, &models.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_VIEW}) + bus.AddHandler("test3", func(cmd *models.RemoveDashboardPermissionCommand) error { + return nil + }) + + Convey("Should be not be able to delete permission", func() { + sc.handlerFunc = DeleteDashboardAclByUser + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 403) + }) + }) }) }) } diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 6d65486e5e7..72336693363 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -56,10 +56,10 @@ func TestDataSourcesProxy(t *testing.T) { } func loggedInUserScenario(desc string, url string, fn scenarioFunc) { - loggedInUserScenarioWithRole(desc, url, models.ROLE_EDITOR, fn) + loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn) } -func loggedInUserScenarioWithRole(desc string, url string, role models.RoleType, fn scenarioFunc) { +func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() @@ -89,7 +89,12 @@ func loggedInUserScenarioWithRole(desc string, url string, role models.RoleType, return nil }) - sc.m.Get(url, sc.defaultHandler) + switch method { + case "GET": + sc.m.Get(routePattern, sc.defaultHandler) + case "DELETE": + sc.m.Delete(routePattern, sc.defaultHandler) + } fn(sc) }) diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 491c7acb08c..aca2935b278 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -32,13 +32,45 @@ func CanViewAcl(dashboardId int64, role m.RoleType, isGrafanaAdmin bool, orgId i return false, err } - if len(filteredList) > 1 && filteredList[0] == dashboardId { + if len(filteredList) > 0 && filteredList[0] == dashboardId { return true, nil } return false, nil } +// CanDeleteFromAcl determines if a user has permission to delete from a dashboard's ACL +func CanDeleteFromAcl(dashboardId int64, role m.RoleType, isGrafanaAdmin bool, orgId int64, userId int64) (bool, error) { + if role == m.ROLE_ADMIN || isGrafanaAdmin { + return true, nil + } + + permissions, err := getDashboardPermissions(dashboardId) + if err != nil { + return false, err + } + + if len(permissions) == 0 { + return true, nil + } + + userGroups, err := getUserGroupsByUser(userId) + + for _, p := range permissions { + if p.UserId == userId && p.Permissions == m.PERMISSION_EDIT { + return true, nil + } + + for _, ug := range userGroups { + if ug.Id == p.UserGroupId && p.Permissions == m.PERMISSION_EDIT { + return true, nil + } + } + } + + return false, nil +} + func getUser(userId int64) (*m.SignedInUser, error) { query := m.GetSignedInUserQuery{UserId: userId} err := bus.Dispatch(&query) @@ -52,3 +84,17 @@ func getAllowedDashboards(dashList []int64, orgId int64, userId int64) ([]int64, return query.Result, err } + +func getDashboardPermissions(dashboardId int64) ([]*m.DashboardAclInfoDTO, error) { + query := m.GetDashboardPermissionsQuery{DashboardId: dashboardId} + err := bus.Dispatch(&query) + + return query.Result, err +} + +func getUserGroupsByUser(userId int64) ([]*m.UserGroup, error) { + query := m.GetUserGroupsByUserQuery{UserId: userId} + err := bus.Dispatch(&query) + + return query.Result, err +}