mirror of
https://github.com/grafana/grafana.git
synced 2025-01-07 22:53:56 -06:00
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:
parent
c5610450b2
commit
1037ef28a9
@ -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;
|
||||||
|
@ -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 = {
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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")
|
||||||
|
@ -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))
|
||||||
|
@ -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',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
@ -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>
|
||||||
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
@ -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')
|
||||||
),
|
),
|
||||||
|
@ -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',
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user