diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index c287633e921..a9668155e67 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -210,6 +210,7 @@ export interface GrafanaConfig { angularSupportEnabled: boolean; feedbackLinksEnabled: boolean; secretsManagerPluginEnabled: boolean; + supportBundlesEnabled: boolean; googleAnalyticsId: string | undefined; googleAnalytics4Id: string | undefined; googleAnalytics4SendManualPageViews: boolean; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index dc8bc585d4f..a9ef4dbf9b3 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -89,6 +89,7 @@ export class GrafanaBootConfig implements GrafanaConfig { } = { systemRequirements: { met: false, requiredImageRendererPluginVersion: '' }, thumbnailsExist: false }; rendererVersion = ''; secretsManagerPluginEnabled = false; + supportBundlesEnabled = false; http2Enabled = false; dateFormats?: SystemDateFormatSettings; sentry = { diff --git a/pkg/api/api.go b/pkg/api/api.go index 41cadf05fb9..89b0749a59b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -121,9 +121,6 @@ func (hs *HTTPServer) registerRoutes() { } r.Get("/styleguide", reqSignedIn, hs.Index) - r.Get("/admin/support-bundles", reqGrafanaAdmin, hs.Index) - r.Get("/admin/support-bundles/create", reqGrafanaAdmin, hs.Index) - r.Get("/live", reqGrafanaAdmin, hs.Index) r.Get("/live/pipeline", reqGrafanaAdmin, hs.Index) r.Get("/live/cloud", reqGrafanaAdmin, hs.Index) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index f2711a2895f..725b488acc9 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -185,6 +185,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "expressionsEnabled": hs.Cfg.ExpressionsEnabled, "awsAllowedAuthProviders": hs.Cfg.AWSAllowedAuthProviders, "awsAssumeRoleEnabled": hs.Cfg.AWSAssumeRoleEnabled, + "supportBundlesEnabled": isSupportBundlesEnabled(hs), "azure": map[string]interface{}{ "cloud": hs.Cfg.Azure.Cloud, "managedIdentityEnabled": hs.Cfg.Azure.ManagedIdentityEnabled, @@ -222,6 +223,11 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i return jsonObj, nil } +func isSupportBundlesEnabled(hs *HTTPServer) bool { + return hs.Cfg.SectionWithEnvOverrides("support_bundles").Key("enabled").MustBool(false) && + hs.Features.IsEnabled(featuremgmt.FlagSupportBundles) +} + func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins EnabledPlugins) (map[string]plugins.DataSourceDTO, error) { orgDataSources := make([]*datasources.DataSource, 0) if c.OrgID != 0 { diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 3a68ecb7a56..5e7a3d024dd 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -250,7 +250,7 @@ func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqC supportBundleNode := &navtree.NavLink{ Text: "Support bundles", Id: "support-bundles", - Url: "/admin/support-bundles", + Url: "/support-bundles", Icon: "wrench", Section: navtree.NavSectionConfig, SortWeight: navtree.WeightHelp, diff --git a/pkg/services/supportbundles/supportbundlesimpl/api.go b/pkg/services/supportbundles/supportbundlesimpl/api.go index 6f4639e4266..373789d1a31 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/api.go +++ b/pkg/services/supportbundles/supportbundlesimpl/api.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" + grafanaApi "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" @@ -18,7 +19,7 @@ import ( const rootUrl = "/api/support-bundles" -func (s *Service) registerAPIEndpoints(routeRegister routing.RouteRegister) { +func (s *Service) registerAPIEndpoints(httpServer *grafanaApi.HTTPServer, routeRegister routing.RouteRegister) { authorize := ac.Middleware(s.accessControl) orgRoleMiddleware := middleware.ReqGrafanaAdmin @@ -26,6 +27,14 @@ func (s *Service) registerAPIEndpoints(routeRegister routing.RouteRegister) { orgRoleMiddleware = middleware.RoleAuth(roletype.RoleAdmin) } + supportBundlePageAccess := ac.EvalAny( + ac.EvalPermission(ActionRead), + ac.EvalPermission(ActionCreate), + ) + + routeRegister.Get("/support-bundles", authorize(orgRoleMiddleware, supportBundlePageAccess), httpServer.Index) + routeRegister.Get("/support-bundles/create", authorize(orgRoleMiddleware, ac.EvalPermission(ActionCreate)), httpServer.Index) + routeRegister.Group(rootUrl, func(subrouter routing.RouteRegister) { subrouter.Get("/", authorize(orgRoleMiddleware, ac.EvalPermission(ActionRead)), routing.Wrap(s.handleList)) @@ -81,11 +90,11 @@ func (s *Service) handleDownload(ctx *models.ReqContext) response.Response { uid := web.Params(ctx.Req)[":uid"] bundle, err := s.get(ctx.Req.Context(), uid) if err != nil { - return response.Redirect("/admin/support-bundles") + return response.Redirect("/support-bundles") } if bundle.State != supportbundles.StateComplete { - return response.Redirect("/admin/support-bundles") + return response.Redirect("/support-bundles") } ctx.Resp.Header().Set("Content-Type", "application/tar+gzip") diff --git a/pkg/services/supportbundles/supportbundlesimpl/service.go b/pkg/services/supportbundles/supportbundlesimpl/service.go index 5934d5df91e..50fa8fe7d58 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + grafanaApi "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/kvstore" @@ -51,6 +52,7 @@ func ProvideService(cfg *setting.Cfg, pluginStore plugins.Store, pluginSettings pluginsettings.Service, features *featuremgmt.FeatureManager, + httpServer *grafanaApi.HTTPServer, usageStats usagestats.Service) (*Service, error) { section := cfg.SectionWithEnvOverrides("support_bundles") s := &Service{ @@ -76,7 +78,7 @@ func ProvideService(cfg *setting.Cfg, } } - s.registerAPIEndpoints(routeRegister) + s.registerAPIEndpoints(httpServer, routeRegister) // TODO: move to relevant services s.RegisterSupportItemCollector(basicCollector(cfg)) diff --git a/public/app/core/components/NavBar/utils.ts b/public/app/core/components/NavBar/utils.ts index 69b0a425009..eef4c0cece1 100644 --- a/public/app/core/components/NavBar/utils.ts +++ b/public/app/core/components/NavBar/utils.ts @@ -4,6 +4,7 @@ import { locationUtil, NavModelItem, NavSection } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction } from 'app/types'; import { ShowModalReactEvent } from '../../../types/events'; import appEvents from '../../app_events'; @@ -79,7 +80,11 @@ export const enrichConfigItems = (items: NavModelItem[], location: Location { - if (!cfg.featureToggles.supportBundles) { + const hasAccess = + contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesCreate, contextSrv.isGrafanaAdmin) || + contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesRead, contextSrv.isGrafanaAdmin); + + if (!cfg.supportBundlesEnabled || !hasAccess) { return []; } @@ -89,7 +94,7 @@ export let getSupportBundleFooterLinks = (cfg = config): FooterLink[] => { id: 'support-bundle', text: t('nav.help/support-bundle', 'Support Bundles'), icon: 'question-circle', - url: '/admin/support-bundles', + url: '/support-bundles', }, ]; }; diff --git a/public/app/features/support-bundles/SupportBundles.tsx b/public/app/features/support-bundles/SupportBundles.tsx index a2a6fa98f72..f815126d296 100644 --- a/public/app/features/support-bundles/SupportBundles.tsx +++ b/public/app/features/support-bundles/SupportBundles.tsx @@ -5,7 +5,8 @@ import { dateTimeFormat } from '@grafana/data'; import { config } from '@grafana/runtime'; import { LinkButton, Spinner, IconButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { StoreState } from 'app/types'; +import { contextSrv } from 'app/core/core'; +import { AccessControlAction, StoreState } from 'app/types'; import { loadBundles, removeBundle, checkBundles } from './state/actions'; @@ -17,7 +18,7 @@ const subTitle = ( ); const NewBundleButton = ( - + New support bundle ); @@ -52,12 +53,18 @@ const SupportBundlesUnconnected = ({ supportBundles, isLoading, loadBundles, rem } }); - const actions = config.featureToggles.topnav ? NewBundleButton : undefined; + const hasAccess = contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesCreate, contextSrv.isGrafanaAdmin); + const hasDeleteAccess = contextSrv.hasAccess( + AccessControlAction.ActionSupportBundlesDelete, + contextSrv.isGrafanaAdmin + ); + + const actions = config.featureToggles.topnav && hasAccess ? NewBundleButton : undefined; return ( - {!config.featureToggles.topnav && NewBundleButton} + {!config.featureToggles.topnav && hasAccess && NewBundleButton} @@ -88,7 +95,9 @@ const SupportBundlesUnconnected = ({ supportBundles, isLoading, loadBundles, rem ))} diff --git a/public/app/features/support-bundles/SupportBundlesCreate.tsx b/public/app/features/support-bundles/SupportBundlesCreate.tsx index d8070c1c096..4d1b59ee72c 100644 --- a/public/app/features/support-bundles/SupportBundlesCreate.tsx +++ b/public/app/features/support-bundles/SupportBundlesCreate.tsx @@ -82,7 +82,7 @@ export const SupportBundlesCreateUnconnected = ({ })} - + Cancel diff --git a/public/app/features/support-bundles/state/actions.ts b/public/app/features/support-bundles/state/actions.ts index 6deb633c207..f42babe81ec 100644 --- a/public/app/features/support-bundles/state/actions.ts +++ b/public/app/features/support-bundles/state/actions.ts @@ -64,7 +64,7 @@ export function createSupportBundle(data: SupportBundleCreateRequest): ThunkResu return async (dispatch) => { try { await getBackendSrv().post('/api/support-bundles', data); - locationService.push('/admin/support-bundles'); + locationService.push('/support-bundles'); } catch (err) { dispatch(setCreateBundleError('Error creating support bundle')); } diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 7db846c12bb..0973b7420c6 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -567,13 +567,13 @@ export function getSupportBundleRoutes(cfg = config): RouteDescriptor[] { return [ { - path: '/admin/support-bundles', + path: '/support-bundles', component: SafeDynamicImport( () => import(/* webpackChunkName: "SupportBundles" */ 'app/features/support-bundles/SupportBundles') ), }, { - path: '/admin/support-bundles/create', + path: '/support-bundles/create', component: SafeDynamicImport( () => import(/* webpackChunkName: "SupportBundlesCreate" */ 'app/features/support-bundles/SupportBundlesCreate') ), diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index 26dfda777a4..ef517ce387c 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -85,6 +85,11 @@ export enum AccessControlAction { FoldersPermissionsRead = 'folders.permissions:read', FoldersPermissionsWrite = 'folders.permissions:write', + // Support bundle actions + ActionSupportBundlesCreate = 'support.bundles:create', + ActionSupportBundlesRead = 'support.bundles:read', + ActionSupportBundlesDelete = 'support.bundles:delete', + // Alerting rules AlertingRuleCreate = 'alert.rules:create', AlertingRuleRead = 'alert.rules:read', diff --git a/public/test/mocks/navModel.ts b/public/test/mocks/navModel.ts index 717a1a039fb..0bcd93530f7 100644 --- a/public/test/mocks/navModel.ts +++ b/public/test/mocks/navModel.ts @@ -1198,7 +1198,7 @@ export const mockNavModel: NavIndex = { id: 'support-bundles', text: 'Support bundles', icon: 'sliders-v-alt', - url: '/admin/support-bundles', + url: '/support-bundles', }, 'server-settings': { id: 'server-settings',
- removeBundle(bundle.uid)} name="trash-alt" variant="destructive" /> + {hasDeleteAccess && ( + removeBundle(bundle.uid)} name="trash-alt" variant="destructive" /> + )}