From 62494248e3c26495f85fb049a5094dd17f70d7eb Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 11 Jul 2024 13:20:04 +0300 Subject: [PATCH] Restore dashboards: Add RBAC (#90270) * Restore dashboards: Add RBAC * Add check to navtree * Prevent non-admins from searching deleted dbs * Add check to the route * Cleanup * Update translations * Update API permissions * Correct permissions * Update warning message * Update translation * Return 401 for deleted query without admin role --- pkg/api/api.go | 6 +++--- pkg/api/search.go | 6 ++++++ pkg/services/navtree/navtreeimpl/navtree.go | 6 +++--- public/app/core/utils/navBarItem-translations.ts | 2 +- .../components/BrowseActions/DeleteModal.tsx | 4 ++-- public/app/routes/routes.tsx | 2 +- public/locales/en-US/grafana.json | 4 ++-- public/locales/pseudo-LOCALE/grafana.json | 4 ++-- 8 files changed, 20 insertions(+), 14 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index fdec78dc0ff..82734adfda4 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -170,7 +170,7 @@ func (hs *HTTPServer) registerRoutes() { } if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { - r.Get("/dashboard/recently-deleted", reqSignedIn, hs.Index) + r.Get("/dashboard/recently-deleted", reqOrgAdmin, hs.Index) } r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) @@ -477,8 +477,8 @@ func (hs *HTTPServer) registerRoutes() { dashUidRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion)) if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { - dashUidRoute.Patch("/trash", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDeletedDashboard)) - dashUidRoute.Delete("/trash", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete)), routing.Wrap(hs.HardDeleteDashboardByUID)) + dashUidRoute.Patch("/trash", reqOrgAdmin, routing.Wrap(hs.RestoreDeletedDashboard)) + dashUidRoute.Delete("/trash", reqOrgAdmin, routing.Wrap(hs.HardDeleteDashboardByUID)) } dashUidRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { diff --git a/pkg/api/search.go b/pkg/api/search.go index e6fa99db380..2cd77a7d157 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -4,6 +4,8 @@ import ( "net/http" "strconv" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/metrics" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -31,6 +33,10 @@ func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response { deleted := c.Query("deleted") permission := dashboardaccess.PERMISSION_VIEW + if deleted == "true" && c.SignedInUser.GetOrgRole() != org.RoleAdmin { + return response.Error(http.StatusUnauthorized, "Unauthorized", nil) + } + if limit > 5000 { return response.Error(http.StatusUnprocessableEntity, "Limit is above maximum allowed (5000), use page parameter to access hits beyond limit", nil) } diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 69bd5de2aec..6e0440f88e3 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -330,7 +330,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt if s.cfg.SnapshotEnabled { dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ Text: "Snapshots", - SubTitle: "Interactive, publically available, point-in-time representations of dashboards", + SubTitle: "Interactive, publicly available, point-in-time representations of dashboards", Id: "dashboards/snapshots", Url: s.cfg.AppSubURL + "/dashboard/snapshots", Icon: "camera", @@ -354,9 +354,9 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt }) } - if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestoreUI) && hasAccess(ac.EvalPermission(dashboards.ActionDashboardsDelete)) { + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestoreUI) && c.SignedInUser.GetOrgRole() == org.RoleAdmin { dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ - Text: "Recently Deleted", + Text: "Recently deleted", SubTitle: "Any items listed here for more than 30 days will be automatically deleted.", Id: "dashboards/recently-deleted", Url: s.cfg.AppSubURL + "/dashboard/recently-deleted", diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index b79abe733fd..f9ee3dcd99f 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -41,7 +41,7 @@ export function getNavTitle(navId: string | undefined) { case 'dashboards/public': return t('nav.public.title', 'Public dashboards'); case 'dashboards/recently-deleted': - return t('nav.recently-deleted.title', 'Recently Deleted'); + return t('nav.recently-deleted.title', 'Recently deleted'); case 'dashboards/new': return t('nav.new-dashboard.title', 'New dashboard'); case 'dashboards/folder/new': diff --git a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx index b9fbbcf9348..7cff90fd755 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx @@ -40,8 +40,8 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P This action will delete the selected folders immediately but the selected dashboards will be marked - for deletion in 30 days. You can restore the dashboards anytime before the 30 days expires. Folders - cannot be restored. + for deletion in 30 days. Your organization administrator can restore the dashboards anytime before the + 30 days expire. Folders cannot be restored. diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index fef33159025..98f7f6e5ff4 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -436,7 +436,7 @@ export function getAppRoutes(): RouteDescriptor[] { }, config.featureToggles.dashboardRestoreUI && { path: '/dashboard/recently-deleted', - roles: () => contextSrv.evaluatePermission([AccessControlAction.DashboardsDelete]), + roles: () => ['Admin'], component: SafeDynamicImport( () => import(/* webpackChunkName: "RecentlyDeletedPage" */ 'app/features/browse-dashboards/RecentlyDeletedPage') ), diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7b3fb3c778c..8c827594e52 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -147,7 +147,7 @@ "delete-button": "Delete", "delete-modal-invalid-text": "One or more folders contain library panels or alert rules. Delete these first in order to proceed.", "delete-modal-invalid-title": "Cannot delete folder", - "delete-modal-restore-dashboards-text": "This action will delete the selected folders immediately but the selected dashboards will be marked for deletion in 30 days. You can restore the dashboards anytime before the 30 days expires. Folders cannot be restored.", + "delete-modal-restore-dashboards-text": "This action will delete the selected folders immediately but the selected dashboards will be marked for deletion in 30 days. Your organization administrator can restore the dashboards anytime before the 30 days expire. Folders cannot be restored.", "delete-modal-text": "This action will delete the following content:", "delete-modal-title": "Delete", "deleting": "Deleting...", @@ -1309,7 +1309,7 @@ }, "recently-deleted": { "subtitle": "Any items listed here for more than 30 days will be automatically deleted.", - "title": "Recently Deleted" + "title": "Recently deleted" }, "recorded-queries": { "title": "Recorded queries" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index f4be7771099..20de0b7aaae 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -147,7 +147,7 @@ "delete-button": "Đęľęŧę", "delete-modal-invalid-text": "Øʼnę őř mőřę ƒőľđęřş čőʼnŧäįʼn ľįþřäřy päʼnęľş őř äľęřŧ řūľęş. Đęľęŧę ŧĥęşę ƒįřşŧ įʼn őřđęř ŧő přőčęęđ.", "delete-modal-invalid-title": "Cäʼnʼnőŧ đęľęŧę ƒőľđęř", - "delete-modal-restore-dashboards-text": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę ŧĥę şęľęčŧęđ ƒőľđęřş įmmęđįäŧęľy þūŧ ŧĥę şęľęčŧęđ đäşĥþőäřđş ŵįľľ þę mäřĸęđ ƒőř đęľęŧįőʼn įʼn 30 đäyş. Ÿőū čäʼn řęşŧőřę ŧĥę đäşĥþőäřđş äʼnyŧįmę þęƒőřę ŧĥę 30 đäyş ęχpįřęş. Főľđęřş čäʼnʼnőŧ þę řęşŧőřęđ.", + "delete-modal-restore-dashboards-text": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę ŧĥę şęľęčŧęđ ƒőľđęřş įmmęđįäŧęľy þūŧ ŧĥę şęľęčŧęđ đäşĥþőäřđş ŵįľľ þę mäřĸęđ ƒőř đęľęŧįőʼn įʼn 30 đäyş. Ÿőūř őřģäʼnįžäŧįőʼn äđmįʼnįşŧřäŧőř čäʼn řęşŧőřę ŧĥę đäşĥþőäřđş äʼnyŧįmę þęƒőřę ŧĥę 30 đäyş ęχpįřę. Főľđęřş čäʼnʼnőŧ þę řęşŧőřęđ.", "delete-modal-text": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę ŧĥę ƒőľľőŵįʼnģ čőʼnŧęʼnŧ:", "delete-modal-title": "Đęľęŧę", "deleting": "Đęľęŧįʼnģ...", @@ -1309,7 +1309,7 @@ }, "recently-deleted": { "subtitle": "Åʼny įŧęmş ľįşŧęđ ĥęřę ƒőř mőřę ŧĥäʼn 30 đäyş ŵįľľ þę äūŧőmäŧįčäľľy đęľęŧęđ.", - "title": "Ŗęčęʼnŧľy Đęľęŧęđ" + "title": "Ŗęčęʼnŧľy đęľęŧęđ" }, "recorded-queries": { "title": "Ŗęčőřđęđ qūęřįęş"