diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 9b5d21811b6ae..5555771f2bb62 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -44,6 +44,7 @@ import { UEBA_PATH, CASES_FEATURE_ID, HOST_ISOLATION_EXCEPTIONS_PATH, + SERVER_APP_ID, } from '../../../common/constants'; import { ExperimentalFeatures } from '../../../common/experimental_features'; @@ -365,7 +366,7 @@ export function getDeepLinks( if (deepLink.id === SecurityPageName.investigate) { return true; } - return capabilities?.siem.show ?? false; + return capabilities == null || capabilities[SERVER_APP_ID].show === true; }) .map((deepLink) => { if ( diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 2acb09483af29..c76d34a41c5e1 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -22,12 +22,8 @@ export const renderApp = ({ services, store, usageCollection, - subPlugins, + subPluginRoutes, }: RenderAppProps): (() => void) => { - const allRoutes = Object.entries(subPlugins).reduce( - (acc, [, value]) => [...acc, ...value.routes], - [] - ); const ApplicationUsageTrackingProvider = usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; render( @@ -40,7 +36,7 @@ export const renderApp = ({ > - {allRoutes.map((route, index) => { + {subPluginRoutes.map((route, index) => { return ; })} diff --git a/x-pack/plugins/security_solution/public/app/no_privileges.tsx b/x-pack/plugins/security_solution/public/app/no_privileges.tsx new file mode 100644 index 0000000000000..354e6eaf27198 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/no_privileges.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { EuiPageTemplate } from '@elastic/eui'; +import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper'; +import { EmptyPage } from '../common/components/empty_page'; +import { useKibana } from '../common/lib/kibana'; +import * as i18n from './translations'; + +interface NoPrivilegesPageProps { + subPluginKey: string; +} + +export const NoPrivilegesPage = React.memo(({ subPluginKey }) => { + const { docLinks } = useKibana().services; + const emptyPageActions = useMemo( + () => ({ + feature: { + icon: 'documents', + label: i18n.GO_TO_DOCUMENTATION, + url: `${docLinks.links.siem.privileges}`, + target: '_blank', + }, + }), + [docLinks] + ); + return ( + + + + + + ); +}); + +NoPrivilegesPage.displayName = 'NoPrivilegePage'; diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index da680bf45dc8d..15fa34841d964 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -80,3 +80,21 @@ export const INVESTIGATE = i18n.translate('xpack.securitySolution.navigation.inv export const MANAGE = i18n.translate('xpack.securitySolution.navigation.manage', { defaultMessage: 'Manage', }); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.securitySolution.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); + +export const NO_PERMISSIONS_MSG = (subPluginKey: string) => + i18n.translate('xpack.securitySolution.noPermissionsMessage', { + values: { subPluginKey }, + defaultMessage: + 'To view {subPluginKey}, you must update privileges. For more information, contact your Kibana administrator.', + }); + +export const NO_PERMISSIONS_TITLE = i18n.translate('xpack.securitySolution.noPermissionsTitle', { + defaultMessage: 'Privileges required', +}); diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 1942d2f836b1c..52d69d7c4e7d9 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -18,7 +18,7 @@ import { import { RouteProps } from 'react-router-dom'; import { AppMountParameters } from '../../../../../src/core/public'; import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; -import { StartedSubPlugins, StartServices } from '../types'; +import { StartServices } from '../types'; /** * The React properties used to render `SecurityApp` as well as the `element` to render it into. @@ -26,7 +26,7 @@ import { StartedSubPlugins, StartServices } from '../types'; export interface RenderAppProps extends AppMountParameters { services: StartServices; store: Store; - subPlugins: StartedSubPlugins; + subPluginRoutes: RouteProps[]; usageCollection?: UsageCollectionSetup; } diff --git a/x-pack/plugins/security_solution/public/helpers.test.ts b/x-pack/plugins/security_solution/public/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/helpers.test.ts rename to x-pack/plugins/security_solution/public/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/helpers.ts rename to x-pack/plugins/security_solution/public/helpers.tsx index 7bc6e02ec83fb..521853a31fe71 100644 --- a/x-pack/plugins/security_solution/public/helpers.ts +++ b/x-pack/plugins/security_solution/public/helpers.tsx @@ -6,9 +6,10 @@ */ import { isEmpty } from 'lodash/fp'; -import { matchPath } from 'react-router-dom'; +import React from 'react'; +import { matchPath, RouteProps } from 'react-router-dom'; -import { CoreStart } from '../../../../src/core/public'; +import { Capabilities, CoreStart } from '../../../../src/core/public'; import { ALERTS_PATH, APP_UI_ID, @@ -16,14 +17,17 @@ import { RULES_PATH, UEBA_PATH, RISKY_HOSTS_INDEX_PREFIX, + SERVER_APP_ID, + CASES_FEATURE_ID, } from '../common/constants'; import { FactoryQueryTypes, StrategyResponseType, } from '../common/search_strategy/security_solution'; import { TimelineEqlResponse } from '../common/search_strategy/timeline'; +import { NoPrivilegesPage } from './app/no_privileges'; import { SecurityPageName } from './app/types'; -import { InspectResponse } from './types'; +import { CASES_SUB_PLUGIN_KEY, InspectResponse, StartedSubPlugins } from './types'; export const parseRoute = (location: Pick) => { if (!isEmpty(location.hash)) { @@ -158,3 +162,28 @@ export const isDetectionsPath = (pathname: string): boolean => { export const getHostRiskIndex = (spaceId: string): string => { return `${RISKY_HOSTS_INDEX_PREFIX}${spaceId}`; }; + +export const getSubPluginRoutesByCapabilities = ( + subPlugins: StartedSubPlugins, + capabilities: Capabilities +): RouteProps[] => { + return Object.entries(subPlugins).reduce((acc, [key, value]) => { + if (isSubPluginAvailable(key, capabilities)) { + return [...acc, ...value.routes]; + } + return [ + ...acc, + ...value.routes.map((route: RouteProps) => ({ + path: route.path, + component: , + })), + ]; + }, []); +}; + +const isSubPluginAvailable = (pluginKey: string, capabilities: Capabilities): boolean => { + if (CASES_SUB_PLUGIN_KEY === pluginKey) { + return capabilities[CASES_FEATURE_ID].read_cases === true; + } + return capabilities[SERVER_APP_ID].show === true; +}; diff --git a/x-pack/plugins/security_solution/public/index.tsx b/x-pack/plugins/security_solution/public/index.tsx index 028473f5c2001..3d2412b326b54 100644 --- a/x-pack/plugins/security_solution/public/index.tsx +++ b/x-pack/plugins/security_solution/public/index.tsx @@ -5,72 +5,11 @@ * 2.0. */ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { DeepReadonly } from 'utility-types'; +import { PluginInitializerContext } from '../../../../src/core/public'; +import { Plugin } from './plugin'; +import { PluginSetup } from './types'; +export type { TimelineModel } from './timelines/store/timeline/model'; -import { Capabilities } from '../../../../../../../src/core/public'; -import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; -import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; -import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); -import { SERVER_APP_ID } from '../../../../common/constants'; -export interface UserPrivilegesState { - listPrivileges: ReturnType; - detectionEnginePrivileges: ReturnType; - endpointPrivileges: EndpointPrivileges; - kibanaSecuritySolutionsPrivileges: { crud: boolean; read: boolean }; -} - -export const initialUserPrivilegesState = (): UserPrivilegesState => ({ - listPrivileges: { loading: false, error: undefined, result: undefined }, - detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, - endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, - kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, -}); - -const UserPrivilegesContext = createContext(initialUserPrivilegesState()); - -interface UserPrivilegesProviderProps { - kibanaCapabilities: Capabilities; - children: React.ReactNode; -} - -export const UserPrivilegesProvider = ({ - kibanaCapabilities, - children, -}: UserPrivilegesProviderProps) => { - const listPrivileges = useFetchListPrivileges(); - const detectionEnginePrivileges = useFetchDetectionEnginePrivileges(); - const endpointPrivileges = useEndpointPrivileges(); - const [kibanaSecuritySolutionsPrivileges, setKibanaSecuritySolutionsPrivileges] = useState({ - crud: false, - read: false, - }); - const crud: boolean = kibanaCapabilities[SERVER_APP_ID].crud === true; - const read: boolean = kibanaCapabilities[SERVER_APP_ID].show === true; - - useEffect(() => { - setKibanaSecuritySolutionsPrivileges((currPrivileges) => { - if (currPrivileges.read !== read || currPrivileges.crud !== crud) { - return { read, crud }; - } - return currPrivileges; - }); - }, [crud, read]); - - return ( - - {children} - - ); -}; - -export const useUserPrivileges = (): DeepReadonly => - useContext(UserPrivilegesContext); +export { Plugin, PluginSetup }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index a9cc74ff883a7..811dc4ed773a6 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -46,7 +46,7 @@ import { } from '../common/constants'; import { getDeepLinks } from './app/deep_links'; -import { manageOldSiemRoutes } from './helpers'; +import { getSubPluginRoutesByCapabilities, manageOldSiemRoutes } from './helpers'; import { IndexFieldsStrategyRequest, IndexFieldsStrategyResponse, @@ -153,7 +153,10 @@ export class Plugin implements IPlugin; rules: ReturnType; exceptions: ReturnType; - cases: ReturnType; + [CASES_SUB_PLUGIN_KEY]: ReturnType; hosts: ReturnType; network: ReturnType; ueba: ReturnType;