SupportBundles: Access control guards (#61914)

* rename routes and fix access control for support bundles

* AccessControl: Hide menu if not authorized

* AccessControl: Add AC guards for create and delete

* lint
This commit is contained in:
Jo 2023-01-23 16:23:20 +00:00 committed by GitHub
parent c5610450b2
commit 1037ef28a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 55 additions and 20 deletions

View File

@ -210,6 +210,7 @@ export interface GrafanaConfig {
angularSupportEnabled: boolean; angularSupportEnabled: boolean;
feedbackLinksEnabled: boolean; feedbackLinksEnabled: boolean;
secretsManagerPluginEnabled: boolean; secretsManagerPluginEnabled: boolean;
supportBundlesEnabled: boolean;
googleAnalyticsId: string | undefined; googleAnalyticsId: string | undefined;
googleAnalytics4Id: string | undefined; googleAnalytics4Id: string | undefined;
googleAnalytics4SendManualPageViews: boolean; googleAnalytics4SendManualPageViews: boolean;

View File

@ -89,6 +89,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
} = { systemRequirements: { met: false, requiredImageRendererPluginVersion: '' }, thumbnailsExist: false }; } = { systemRequirements: { met: false, requiredImageRendererPluginVersion: '' }, thumbnailsExist: false };
rendererVersion = ''; rendererVersion = '';
secretsManagerPluginEnabled = false; secretsManagerPluginEnabled = false;
supportBundlesEnabled = false;
http2Enabled = false; http2Enabled = false;
dateFormats?: SystemDateFormatSettings; dateFormats?: SystemDateFormatSettings;
sentry = { sentry = {

View File

@ -121,9 +121,6 @@ func (hs *HTTPServer) registerRoutes() {
} }
r.Get("/styleguide", reqSignedIn, hs.Index) 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", reqGrafanaAdmin, hs.Index)
r.Get("/live/pipeline", reqGrafanaAdmin, hs.Index) r.Get("/live/pipeline", reqGrafanaAdmin, hs.Index)
r.Get("/live/cloud", reqGrafanaAdmin, hs.Index) r.Get("/live/cloud", reqGrafanaAdmin, hs.Index)

View File

@ -185,6 +185,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"expressionsEnabled": hs.Cfg.ExpressionsEnabled, "expressionsEnabled": hs.Cfg.ExpressionsEnabled,
"awsAllowedAuthProviders": hs.Cfg.AWSAllowedAuthProviders, "awsAllowedAuthProviders": hs.Cfg.AWSAllowedAuthProviders,
"awsAssumeRoleEnabled": hs.Cfg.AWSAssumeRoleEnabled, "awsAssumeRoleEnabled": hs.Cfg.AWSAssumeRoleEnabled,
"supportBundlesEnabled": isSupportBundlesEnabled(hs),
"azure": map[string]interface{}{ "azure": map[string]interface{}{
"cloud": hs.Cfg.Azure.Cloud, "cloud": hs.Cfg.Azure.Cloud,
"managedIdentityEnabled": hs.Cfg.Azure.ManagedIdentityEnabled, "managedIdentityEnabled": hs.Cfg.Azure.ManagedIdentityEnabled,
@ -222,6 +223,11 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
return jsonObj, nil 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) { func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins EnabledPlugins) (map[string]plugins.DataSourceDTO, error) {
orgDataSources := make([]*datasources.DataSource, 0) orgDataSources := make([]*datasources.DataSource, 0)
if c.OrgID != 0 { if c.OrgID != 0 {

View File

@ -250,7 +250,7 @@ func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqC
supportBundleNode := &navtree.NavLink{ supportBundleNode := &navtree.NavLink{
Text: "Support bundles", Text: "Support bundles",
Id: "support-bundles", Id: "support-bundles",
Url: "/admin/support-bundles", Url: "/support-bundles",
Icon: "wrench", Icon: "wrench",
Section: navtree.NavSectionConfig, Section: navtree.NavSectionConfig,
SortWeight: navtree.WeightHelp, SortWeight: navtree.WeightHelp,

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
grafanaApi "github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
@ -18,7 +19,7 @@ import (
const rootUrl = "/api/support-bundles" 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) authorize := ac.Middleware(s.accessControl)
orgRoleMiddleware := middleware.ReqGrafanaAdmin orgRoleMiddleware := middleware.ReqGrafanaAdmin
@ -26,6 +27,14 @@ func (s *Service) registerAPIEndpoints(routeRegister routing.RouteRegister) {
orgRoleMiddleware = middleware.RoleAuth(roletype.RoleAdmin) 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) { routeRegister.Group(rootUrl, func(subrouter routing.RouteRegister) {
subrouter.Get("/", authorize(orgRoleMiddleware, subrouter.Get("/", authorize(orgRoleMiddleware,
ac.EvalPermission(ActionRead)), routing.Wrap(s.handleList)) 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"] uid := web.Params(ctx.Req)[":uid"]
bundle, err := s.get(ctx.Req.Context(), uid) bundle, err := s.get(ctx.Req.Context(), uid)
if err != nil { if err != nil {
return response.Redirect("/admin/support-bundles") return response.Redirect("/support-bundles")
} }
if bundle.State != supportbundles.StateComplete { 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") ctx.Resp.Header().Set("Content-Type", "application/tar+gzip")

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
grafanaApi "github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/kvstore"
@ -51,6 +52,7 @@ func ProvideService(cfg *setting.Cfg,
pluginStore plugins.Store, pluginStore plugins.Store,
pluginSettings pluginsettings.Service, pluginSettings pluginsettings.Service,
features *featuremgmt.FeatureManager, features *featuremgmt.FeatureManager,
httpServer *grafanaApi.HTTPServer,
usageStats usagestats.Service) (*Service, error) { usageStats usagestats.Service) (*Service, error) {
section := cfg.SectionWithEnvOverrides("support_bundles") section := cfg.SectionWithEnvOverrides("support_bundles")
s := &Service{ s := &Service{
@ -76,7 +78,7 @@ func ProvideService(cfg *setting.Cfg,
} }
} }
s.registerAPIEndpoints(routeRegister) s.registerAPIEndpoints(httpServer, routeRegister)
// TODO: move to relevant services // TODO: move to relevant services
s.RegisterSupportItemCollector(basicCollector(cfg)) s.RegisterSupportItemCollector(basicCollector(cfg))

View File

@ -4,6 +4,7 @@ import { locationUtil, NavModelItem, NavSection } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction } from 'app/types';
import { ShowModalReactEvent } from '../../../types/events'; import { ShowModalReactEvent } from '../../../types/events';
import appEvents from '../../app_events'; import appEvents from '../../app_events';
@ -79,7 +80,11 @@ export const enrichConfigItems = (items: NavModelItem[], location: Location<unkn
}; };
export let getSupportBundleFooterLinks = (cfg = config): FooterLink[] => { export let getSupportBundleFooterLinks = (cfg = config): FooterLink[] => {
if (!cfg.featureToggles.supportBundles) { const hasAccess =
contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesCreate, contextSrv.isGrafanaAdmin) ||
contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesRead, contextSrv.isGrafanaAdmin);
if (!cfg.supportBundlesEnabled || !hasAccess) {
return []; return [];
} }
@ -89,7 +94,7 @@ export let getSupportBundleFooterLinks = (cfg = config): FooterLink[] => {
id: 'support-bundle', id: 'support-bundle',
text: t('nav.help/support-bundle', 'Support Bundles'), text: t('nav.help/support-bundle', 'Support Bundles'),
icon: 'question-circle', icon: 'question-circle',
url: '/admin/support-bundles', url: '/support-bundles',
}, },
]; ];
}; };

View File

@ -5,7 +5,8 @@ import { dateTimeFormat } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { LinkButton, Spinner, IconButton } from '@grafana/ui'; import { LinkButton, Spinner, IconButton } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; 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'; import { loadBundles, removeBundle, checkBundles } from './state/actions';
@ -17,7 +18,7 @@ const subTitle = (
); );
const NewBundleButton = ( const NewBundleButton = (
<LinkButton icon="plus" href="admin/support-bundles/create" variant="primary"> <LinkButton icon="plus" href="support-bundles/create" variant="primary">
New support bundle New support bundle
</LinkButton> </LinkButton>
); );
@ -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 ( return (
<Page navId="support-bundles" subTitle={subTitle} actions={actions}> <Page navId="support-bundles" subTitle={subTitle} actions={actions}>
<Page.Contents isLoading={isLoading}> <Page.Contents isLoading={isLoading}>
{!config.featureToggles.topnav && NewBundleButton} {!config.featureToggles.topnav && hasAccess && NewBundleButton}
<table className="filter-table form-inline"> <table className="filter-table form-inline">
<thead> <thead>
@ -88,7 +95,9 @@ const SupportBundlesUnconnected = ({ supportBundles, isLoading, loadBundles, rem
</LinkButton> </LinkButton>
</th> </th>
<th> <th>
<IconButton onClick={() => removeBundle(bundle.uid)} name="trash-alt" variant="destructive" /> {hasDeleteAccess && (
<IconButton onClick={() => removeBundle(bundle.uid)} name="trash-alt" variant="destructive" />
)}
</th> </th>
</tr> </tr>
))} ))}

View File

@ -82,7 +82,7 @@ export const SupportBundlesCreateUnconnected = ({
})} })}
<HorizontalGroup> <HorizontalGroup>
<Button type="submit">Create</Button> <Button type="submit">Create</Button>
<LinkButton href="/admin/support-bundles" variant="secondary"> <LinkButton href="/support-bundles" variant="secondary">
Cancel Cancel
</LinkButton> </LinkButton>
</HorizontalGroup> </HorizontalGroup>

View File

@ -64,7 +64,7 @@ export function createSupportBundle(data: SupportBundleCreateRequest): ThunkResu
return async (dispatch) => { return async (dispatch) => {
try { try {
await getBackendSrv().post('/api/support-bundles', data); await getBackendSrv().post('/api/support-bundles', data);
locationService.push('/admin/support-bundles'); locationService.push('/support-bundles');
} catch (err) { } catch (err) {
dispatch(setCreateBundleError('Error creating support bundle')); dispatch(setCreateBundleError('Error creating support bundle'));
} }

View File

@ -567,13 +567,13 @@ export function getSupportBundleRoutes(cfg = config): RouteDescriptor[] {
return [ return [
{ {
path: '/admin/support-bundles', path: '/support-bundles',
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "SupportBundles" */ 'app/features/support-bundles/SupportBundles') () => import(/* webpackChunkName: "SupportBundles" */ 'app/features/support-bundles/SupportBundles')
), ),
}, },
{ {
path: '/admin/support-bundles/create', path: '/support-bundles/create',
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "SupportBundlesCreate" */ 'app/features/support-bundles/SupportBundlesCreate') () => import(/* webpackChunkName: "SupportBundlesCreate" */ 'app/features/support-bundles/SupportBundlesCreate')
), ),

View File

@ -85,6 +85,11 @@ export enum AccessControlAction {
FoldersPermissionsRead = 'folders.permissions:read', FoldersPermissionsRead = 'folders.permissions:read',
FoldersPermissionsWrite = 'folders.permissions:write', FoldersPermissionsWrite = 'folders.permissions:write',
// Support bundle actions
ActionSupportBundlesCreate = 'support.bundles:create',
ActionSupportBundlesRead = 'support.bundles:read',
ActionSupportBundlesDelete = 'support.bundles:delete',
// Alerting rules // Alerting rules
AlertingRuleCreate = 'alert.rules:create', AlertingRuleCreate = 'alert.rules:create',
AlertingRuleRead = 'alert.rules:read', AlertingRuleRead = 'alert.rules:read',

View File

@ -1198,7 +1198,7 @@ export const mockNavModel: NavIndex = {
id: 'support-bundles', id: 'support-bundles',
text: 'Support bundles', text: 'Support bundles',
icon: 'sliders-v-alt', icon: 'sliders-v-alt',
url: '/admin/support-bundles', url: '/support-bundles',
}, },
'server-settings': { 'server-settings': {
id: 'server-settings', id: 'server-settings',