[Alerting] Control Alerts Management via feature controls & privileges (#72029)

This PR removes the alerting and actions ui privileges (alerting:show, actions:show, etc...) and instead relies on the standard Kibana feature control model to decide whether management displays the Alerts Management section under management.
This commit is contained in:
Gidi Meir Morris 2020-07-28 15:20:24 +01:00 committed by GitHub
parent dca4a23597
commit f4104743e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 157 additions and 139 deletions

View file

@ -44,6 +44,9 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
defaultMessage: 'Alerts Example',
}),
app: [],
management: {
insightsAndAlerting: ['triggersActions'],
},
alerting: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
privileges: {
all: {
@ -54,7 +57,10 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
all: [],
read: [],
},
ui: ['alerting:show'],
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: [],
},
read: {
alerting: {
@ -64,7 +70,10 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
all: [],
read: [],
},
ui: ['alerting:show'],
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: [],
},
},
});

View file

@ -15,11 +15,17 @@ export const ACTIONS_FEATURE = {
icon: 'bell',
navLinkId: 'actions',
app: [],
management: {
insightsAndAlerting: ['triggersActions'],
},
privileges: {
all: {
app: [],
api: [],
catalogue: [],
management: {
insightsAndAlerting: ['triggersActions'],
},
savedObject: {
all: [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE],
read: [],
@ -30,6 +36,9 @@ export const ACTIONS_FEATURE = {
app: [],
api: [],
catalogue: [],
management: {
insightsAndAlerting: ['triggersActions'],
},
savedObject: {
// action execution requires 'read' over `actions`, but 'all' over `action_task_params`
all: [ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE],

View file

@ -15,11 +15,17 @@ export const BUILT_IN_ALERTS_FEATURE = {
}),
icon: 'bell',
app: [],
management: {
insightsAndAlerting: ['triggersActions'],
},
alerting: [IndexThreshold],
privileges: {
all: {
app: [],
catalogue: [],
management: {
insightsAndAlerting: ['triggersActions'],
},
alerting: {
all: [IndexThreshold],
read: [],
@ -29,11 +35,14 @@ export const BUILT_IN_ALERTS_FEATURE = {
read: [],
},
api: [],
ui: ['alerting:show'],
ui: [],
},
read: {
app: [],
catalogue: [],
management: {
insightsAndAlerting: ['triggersActions'],
},
alerting: {
all: [],
read: [IndexThreshold],
@ -43,7 +52,7 @@ export const BUILT_IN_ALERTS_FEATURE = {
read: [],
},
api: [],
ui: ['alerting:show'],
ui: [],
},
},
};

View file

@ -129,6 +129,16 @@ export class AlertingPlugin {
this.spaces = plugins.spaces?.spacesService;
this.security = plugins.security;
core.capabilities.registerProvider(() => {
return {
management: {
insightsAndAlerting: {
triggersActions: true,
},
},
};
});
this.isESOUsingEphemeralEncryptionKey =
plugins.encryptedSavedObjects.usingEphemeralEncryptionKey;

View file

@ -17,6 +17,9 @@ export const APM_FEATURE = {
navLinkId: 'apm',
app: ['apm', 'kibana'],
catalogue: ['apm'],
management: {
insightsAndAlerting: ['triggersActions'],
},
alerting: Object.values(AlertType),
// see x-pack/plugins/features/common/feature_kibana_privileges.ts
privileges: {
@ -31,6 +34,9 @@ export const APM_FEATURE = {
alerting: {
all: Object.values(AlertType),
},
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show', 'save', 'alerting:show', 'alerting:save'],
},
read: {
@ -44,6 +50,9 @@ export const APM_FEATURE = {
alerting: {
all: Object.values(AlertType),
},
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show', 'alerting:show', 'alerting:save'],
},
},

View file

@ -19,6 +19,9 @@ export const METRICS_FEATURE = {
navLinkId: 'metrics',
app: ['infra', 'metrics', 'kibana'],
catalogue: ['infraops'],
management: {
insightsAndAlerting: ['triggersActions'],
},
alerting: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID],
privileges: {
all: {
@ -32,7 +35,10 @@ export const METRICS_FEATURE = {
alerting: {
all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID],
},
ui: ['show', 'configureSource', 'save', 'alerting:show'],
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show', 'configureSource', 'save'],
},
read: {
app: ['infra', 'metrics', 'kibana'],
@ -45,7 +51,10 @@ export const METRICS_FEATURE = {
alerting: {
all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID],
},
ui: ['show', 'alerting:show'],
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show'],
},
},
};

View file

@ -174,6 +174,9 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
navLinkId: APP_ID,
app: [...securitySubPlugins, 'kibana'],
catalogue: ['securitySolution'],
management: {
insightsAndAlerting: ['triggersActions'],
},
alerting: [SIGNALS_ID, NOTIFICATIONS_ID],
privileges: {
all: {
@ -194,7 +197,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
alerting: {
all: [SIGNALS_ID, NOTIFICATIONS_ID],
},
ui: ['show', 'crud', 'alerting:show'],
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show', 'crud'],
},
read: {
app: [...securitySubPlugins, 'kibana'],
@ -214,7 +220,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
alerting: {
all: [SIGNALS_ID, NOTIFICATIONS_ID],
},
ui: ['show', 'alerting:show'],
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show'],
},
},
});

View file

@ -17,8 +17,7 @@ import {
ScopedHistory,
} from 'kibana/public';
import { Section, routeToAlertDetails } from './constants';
import { AppContextProvider, useAppDependencies } from './app_context';
import { hasShowAlertsCapability } from './lib/capabilities';
import { AppContextProvider } from './app_context';
import { ActionTypeModel, AlertTypeModel } from '../types';
import { TypeRegistry } from './type_registry';
import { ChartsPluginStart } from '../../../../../src/plugins/charts/public';
@ -63,22 +62,17 @@ export const App = (appDeps: AppDeps) => {
};
export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => {
const { capabilities } = useAppDependencies();
const canShowAlerts = hasShowAlertsCapability(capabilities);
const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors';
return (
<Switch>
<Route
path={`/:section(${sectionsRegex})`}
component={suspendedComponentWithProps(TriggersActionsUIHome, 'xl')}
/>
{canShowAlerts && (
<Route
path={routeToAlertDetails}
component={suspendedComponentWithProps(AlertDetailsRoute, 'xl')}
/>
)}
<Redirect from={'/'} to={`${DEFAULT_SECTION}`} />
<Route
path={routeToAlertDetails}
component={suspendedComponentWithProps(AlertDetailsRoute, 'xl')}
/>
<Redirect from={'/'} to="alerts" />
</Switch>
);
};

View file

@ -25,7 +25,7 @@ import { Section, routeToConnectors, routeToAlerts } from './constants';
import { getCurrentBreadcrumb } from './lib/breadcrumb';
import { getCurrentDocTitle } from './lib/doc_title';
import { useAppDependencies } from './app_context';
import { hasShowActionsCapability, hasShowAlertsCapability } from './lib/capabilities';
import { hasShowActionsCapability } from './lib/capabilities';
import { ActionsConnectorsList } from './sections/actions_connectors_list/components/actions_connectors_list';
import { AlertsList } from './sections/alerts_list/components/alerts_list';
@ -45,23 +45,17 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
const { chrome, capabilities, setBreadcrumbs, docLinks, http } = useAppDependencies();
const canShowActions = hasShowActionsCapability(capabilities);
const canShowAlerts = hasShowAlertsCapability(capabilities);
const tabs: Array<{
id: Section;
name: React.ReactNode;
}> = [];
if (canShowAlerts) {
tabs.push({
id: 'alerts',
name: (
<FormattedMessage
id="xpack.triggersActionsUI.home.alertsTabTitle"
defaultMessage="Alerts"
/>
),
});
}
tabs.push({
id: 'alerts',
name: (
<FormattedMessage id="xpack.triggersActionsUI.home.alertsTabTitle" defaultMessage="Alerts" />
),
});
if (canShowActions) {
tabs.push({
@ -151,17 +145,15 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
)}
/>
)}
{canShowAlerts && (
<Route
exact
path={routeToAlerts}
component={() => (
<HealthCheck docLinks={docLinks} http={http}>
<AlertsList />
</HealthCheck>
)}
/>
)}
<Route
exact
path={routeToAlerts}
component={() => (
<HealthCheck docLinks={docLinks} http={http}>
<AlertsList />
</HealthCheck>
)}
/>
</Switch>
</EuiPageContent>
</EuiPageBody>

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../../alerting_builtins/common';
import { Alert, AlertType } from '../../types';
/**
@ -15,18 +14,6 @@ import { Alert, AlertType } from '../../types';
type Capabilities = Record<string, any>;
const apps = ['apm', 'siem', 'uptime', 'infrastructure', 'actions', BUILT_IN_ALERTS_FEATURE_ID];
function hasCapability(capabilities: Capabilities, capability: string) {
return apps.some((app) => capabilities[app]?.[capability]);
}
function createCapabilityCheck(capability: string) {
return (capabilities: Capabilities) => hasCapability(capabilities, capability);
}
export const hasShowAlertsCapability = createCapabilityCheck('alerting:show');
export const hasShowActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.show;
export const hasSaveActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.save;
export const hasExecuteActionsCapability = (capabilities: Capabilities) =>

View file

@ -29,9 +29,6 @@ jest.mock('../../../app_context', () => ({
http: jest.fn(),
capabilities: {
get: jest.fn(() => ({})),
securitySolution: {
'alerting:show': true,
},
},
actionTypeRegistry: jest.fn(),
alertTypeRegistry: {

View file

@ -244,15 +244,7 @@ export const AlertForm = ({
) : null}
</EuiFlexGroup>
{AlertParamsExpressionComponent ? (
<Suspense
fallback={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
}
>
<Suspense fallback={CenterJustifiedSpinner}>
<AlertParamsExpressionComponent
alertParams={alert.params}
alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`}
@ -509,13 +501,23 @@ export const AlertForm = ({
{alertTypeNodes}
</EuiFlexGroup>
</Fragment>
) : (
) : alertTypesIndex ? (
<NoAuthorizedAlertTypes operation={operation} />
) : (
<CenterJustifiedSpinner />
)}
</EuiForm>
);
};
const CenterJustifiedSpinner = () => (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
);
const NoAuthorizedAlertTypes = ({ operation }: { operation: string }) => (
<EuiEmptyPrompt
iconType="lock"

View file

@ -106,12 +106,7 @@ describe('alerts_list component empty', () => {
http: mockes.http,
uiSettings: mockes.uiSettings,
navigateToApp,
capabilities: {
...capabilities,
securitySolution: {
'alerting:show': true,
},
},
capabilities,
history: scopedHistoryMock.create(),
setBreadcrumbs: jest.fn(),
actionTypeRegistry: actionTypeRegistry as any,
@ -223,12 +218,7 @@ describe('alerts_list component with items', () => {
http: mockes.http,
uiSettings: mockes.uiSettings,
navigateToApp,
capabilities: {
...capabilities,
securitySolution: {
'alerting:show': true,
},
},
capabilities,
history: scopedHistoryMock.create(),
setBreadcrumbs: jest.fn(),
actionTypeRegistry: actionTypeRegistry as any,
@ -303,12 +293,7 @@ describe('alerts_list component empty with show only capability', () => {
http: mockes.http,
uiSettings: mockes.uiSettings,
navigateToApp,
capabilities: {
...capabilities,
securitySolution: {
'alerting:show': true,
},
},
capabilities,
history: scopedHistoryMock.create(),
setBreadcrumbs: jest.fn(),
actionTypeRegistry: {
@ -417,12 +402,7 @@ describe('alerts_list with show only capability', () => {
http: mockes.http,
uiSettings: mockes.uiSettings,
navigateToApp,
capabilities: {
...capabilities,
securitySolution: {
'alerting:show': true,
},
},
capabilities,
history: scopedHistoryMock.create(),
setBreadcrumbs: jest.fn(),
actionTypeRegistry: actionTypeRegistry as any,

View file

@ -93,7 +93,7 @@ export const AlertsList: React.FunctionComponent = () => {
useEffect(() => {
loadAlertsData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, searchText, typesFilter, actionTypesFilter]);
}, [alertTypesState, page, searchText, typesFilter, actionTypesFilter]);
useEffect(() => {
(async () => {
@ -136,30 +136,33 @@ export const AlertsList: React.FunctionComponent = () => {
}, []);
async function loadAlertsData() {
setAlertsState({ ...alertsState, isLoading: true });
try {
const alertsResponse = await loadAlerts({
http,
page,
searchText,
typesFilter,
actionTypesFilter,
});
setAlertsState({
isLoading: false,
data: alertsResponse.data,
totalItemCount: alertsResponse.total,
});
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsMessage',
{
defaultMessage: 'Unable to load alerts',
}
),
});
setAlertsState({ ...alertsState, isLoading: false });
const hasAnyAuthorizedAlertType = alertTypesState.data.size > 0;
if (hasAnyAuthorizedAlertType) {
setAlertsState({ ...alertsState, isLoading: true });
try {
const alertsResponse = await loadAlerts({
http,
page,
searchText,
typesFilter,
actionTypesFilter,
});
setAlertsState({
isLoading: false,
data: alertsResponse.data,
totalItemCount: alertsResponse.total,
});
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsMessage',
{
defaultMessage: 'Unable to load alerts',
}
),
});
setAlertsState({ ...alertsState, isLoading: false });
}
}
}

View file

@ -14,13 +14,11 @@ import {
import { i18n } from '@kbn/i18n';
import { registerBuiltInActionTypes } from './application/components/builtin_action_types';
import { registerBuiltInAlertTypes } from './application/components/builtin_alert_types';
import { hasShowActionsCapability, hasShowAlertsCapability } from './application/lib/capabilities';
import { ActionTypeModel, AlertTypeModel } from './types';
import { TypeRegistry } from './application/type_registry';
import {
ManagementSetup,
ManagementAppMountParams,
ManagementApp,
} from '../../../../src/plugins/management/public';
import { boot } from './application/boot';
import { ChartsPluginStart } from '../../../../src/plugins/charts/public';
@ -50,10 +48,14 @@ interface PluginsStart {
export class Plugin
implements
CorePlugin<TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart> {
CorePlugin<
TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart,
PluginsSetup,
PluginsStart
> {
private actionTypeRegistry: TypeRegistry<ActionTypeModel>;
private alertTypeRegistry: TypeRegistry<AlertTypeModel>;
private managementApp?: ManagementApp;
constructor(initializerContext: PluginInitializerContext) {
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
@ -67,7 +69,7 @@ export class Plugin
const actionTypeRegistry = this.actionTypeRegistry;
const alertTypeRegistry = this.alertTypeRegistry;
this.managementApp = plugins.management.sections.section.insightsAndAlerting.registerApp({
plugins.management.sections.section.insightsAndAlerting.registerApp({
id: 'triggersActions',
title: i18n.translate('xpack.triggersActionsUI.managementSection.displayName', {
defaultMessage: 'Alerts and Actions',
@ -116,19 +118,7 @@ export class Plugin
};
}
public start(core: CoreStart): TriggersAndActionsUIPublicPluginStart {
const { capabilities } = core.application;
const canShowActions = hasShowActionsCapability(capabilities);
const canShowAlerts = hasShowAlertsCapability(capabilities);
const managementApp = this.managementApp as ManagementApp;
// Don't register routes when user doesn't have access to the application
if (canShowActions || canShowAlerts) {
managementApp.enable();
} else {
managementApp.disable();
}
public start(): TriggersAndActionsUIPublicPluginStart {
return {
actionTypeRegistry: this.actionTypeRegistry,
alertTypeRegistry: this.alertTypeRegistry,

View file

@ -35,6 +35,9 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
icon: 'uptimeApp',
app: ['uptime', 'kibana'],
catalogue: ['uptime'],
management: {
insightsAndAlerting: ['triggersActions'],
},
alerting: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'],
privileges: {
all: {
@ -48,7 +51,10 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
alerting: {
all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'],
},
ui: ['save', 'configureSettings', 'show', 'alerting:show'],
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['save', 'configureSettings', 'show'],
},
read: {
app: ['uptime', 'kibana'],
@ -61,7 +67,10 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
alerting: {
all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'],
},
ui: ['show', 'alerting:show'],
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show'],
},
},
});