Skip to content

Commit

Permalink
[SECURITY SOLUTION] [CASES] Allow cases to be there when security sol…
Browse files Browse the repository at this point in the history
…utions privileges is none (#113573)

* allow case to itself when security solutions privileges is none

* bring back the right owner for cases

* bring no privilege msg when needed it

* fix types

* fix test

* adding test

* review

* deepLinks generation fixed

* register home solution with old app id

* fix get deep links

* fix home link

* fix unit test

* add test

* fix telemetry

Co-authored-by: semd <sergi.massaneda@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 25, 2021
1 parent 1061835 commit 436c74a
Show file tree
Hide file tree
Showing 82 changed files with 800 additions and 447 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pageLoadAssetSize:
expressionShape: 34008
interactiveSetup: 80000
expressionTagcloud: 27505
securitySolution: 231753
securitySolution: 273763
customIntegrations: 28810
expressionMetricVis: 23121
visTypeMetric: 23332
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export const applicationUsageSchema = {
security_login: commonSchema,
security_logout: commonSchema,
security_overwritten_session: commonSchema,
securitySolution: commonSchema,
securitySolutionUI: commonSchema,
siem: commonSchema,
space_selector: commonSchema,
uptime: commonSchema,
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/telemetry/schema/oss_plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -5018,7 +5018,7 @@
}
}
},
"securitySolution": {
"securitySolutionUI": {
"properties": {
"appId": {
"type": "keyword",
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ENABLE_CASE_CONNECTOR } from '../../cases/common';
import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants';

export const APP_ID = 'securitySolution';
export const APP_UI_ID = 'securitySolutionUI';
export const CASES_FEATURE_ID = 'securitySolutionCases';
export const SERVER_APP_ID = 'siem';
export const APP_NAME = 'Security';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getDeepLinks, PREMIUM_DEEP_LINK_IDS } from '.';
import { AppDeepLink, Capabilities } from '../../../../../../src/core/public';
import { SecurityPageName } from '../types';
import { mockGlobalState } from '../../common/mock';
import { CASES_FEATURE_ID } from '../../../common/constants';
import { CASES_FEATURE_ID, SERVER_APP_ID } from '../../../common/constants';

const findDeepLink = (id: string, deepLinks: AppDeepLink[]): AppDeepLink | null =>
deepLinks.reduce((deepLinkFound: AppDeepLink | null, deepLink) => {
Expand All @@ -24,10 +24,11 @@ const findDeepLink = (id: string, deepLinks: AppDeepLink[]): AppDeepLink | null
return null;
}, null);

const basicLicense = 'basic';
const platinumLicense = 'platinum';

describe('deepLinks', () => {
it('should return a subset of links for basic license and the full set for platinum', () => {
const basicLicense = 'basic';
const platinumLicense = 'platinum';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense);
const platinumLinks = getDeepLinks(mockGlobalState.app.enableExperimental, platinumLicense);

Expand Down Expand Up @@ -57,44 +58,58 @@ describe('deepLinks', () => {
});

it('should return case links for basic license with only read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
[SERVER_APP_ID]: { show: true },
} as unknown as Capabilities);

expect(findDeepLink(SecurityPageName.case, basicLinks)).toBeTruthy();
});

it('should return case links with NO deepLinks for basic license with only read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
[SERVER_APP_ID]: { show: true },
} as unknown as Capabilities);
expect(findDeepLink(SecurityPageName.case, basicLinks)?.deepLinks?.length === 0).toBeTruthy();
});

it('should return case links with deepLinks for basic license with crud_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
[SERVER_APP_ID]: { show: true },
} as unknown as Capabilities);

expect(
(findDeepLink(SecurityPageName.case, basicLinks)?.deepLinks?.length ?? 0) > 0
).toBeTruthy();
});

it('should return case links with deepLinks for basic license with crud_cases capabilities and security disabled', () => {
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, platinumLicense, {
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
[SERVER_APP_ID]: { show: false },
} as unknown as Capabilities);
expect(findDeepLink(SecurityPageName.case, basicLinks)).toBeTruthy();
});

it('should return NO case links for basic license with NO read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
[SERVER_APP_ID]: { show: true },
} as unknown as Capabilities);

expect(findDeepLink(SecurityPageName.case, basicLinks)).toBeFalsy();
});

it('should return empty links for any license', () => {
const emptyDeepLinks = getDeepLinks(
mockGlobalState.app.enableExperimental,
basicLicense,
{} as unknown as Capabilities
);
expect(emptyDeepLinks.length).toBe(0);
});

it('should return case links for basic license with undefined capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(
mockGlobalState.app.enableExperimental,
basicLicense,
Expand All @@ -105,7 +120,6 @@ describe('deepLinks', () => {
});

it('should return case deepLinks for basic license with undefined capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(
mockGlobalState.app.enableExperimental,
basicLicense,
Expand Down
59 changes: 24 additions & 35 deletions x-pack/plugins/security_solution/public/app/deep_links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,11 @@
*/

import { i18n } from '@kbn/i18n';
import { Subject } from 'rxjs';

import { isEmpty } from 'lodash';
import { LicenseType } from '../../../../licensing/common/types';
import { SecurityPageName } from '../types';
import {
AppDeepLink,
ApplicationStart,
AppNavLinkStatus,
AppUpdater,
} from '../../../../../../src/core/public';
import { AppDeepLink, ApplicationStart, AppNavLinkStatus } from '../../../../../../src/core/public';
import {
OVERVIEW,
DETECT,
Expand Down Expand Up @@ -50,6 +45,7 @@ import {
UEBA_PATH,
CASES_FEATURE_ID,
HOST_ISOLATION_EXCEPTIONS_PATH,
SERVER_APP_ID,
} from '../../../common/constants';
import { ExperimentalFeatures } from '../../../common/experimental_features';

Expand Down Expand Up @@ -356,25 +352,18 @@ export function getDeepLinks(
): AppDeepLink[] {
const isPremium = isPremiumLicense(licenseType);

/**
* Recursive DFS function to filter deepLinks by permissions (licence and capabilities).
* Checks "end" deepLinks with no children first, the other parent deepLinks will be included if
* they still have children deepLinks after filtering
*/
const filterDeepLinks = (deepLinks: AppDeepLink[]): AppDeepLink[] => {
return deepLinks
.filter((deepLink) => {
if (!isPremium && PREMIUM_DEEP_LINK_IDS.has(deepLink.id)) {
return false;
}
if (deepLink.id === SecurityPageName.case) {
return capabilities == null || capabilities[CASES_FEATURE_ID].read_cases === true;
}
if (deepLink.id === SecurityPageName.ueba) {
return enableExperimental.uebaEnabled;
}
return true;
})
.map((deepLink) => {
if (
deepLink.id === SecurityPageName.case &&
capabilities != null &&
capabilities[CASES_FEATURE_ID].crud_cases === false
capabilities[CASES_FEATURE_ID]?.crud_cases === false
) {
return {
...deepLink,
Expand All @@ -388,6 +377,21 @@ export function getDeepLinks(
};
}
return deepLink;
})
.filter((deepLink) => {
if (!isPremium && PREMIUM_DEEP_LINK_IDS.has(deepLink.id)) {
return false;
}
if (deepLink.path && deepLink.path.startsWith(CASES_PATH)) {
return capabilities == null || capabilities[CASES_FEATURE_ID]?.read_cases === true;
}
if (deepLink.id === SecurityPageName.ueba) {
return enableExperimental.uebaEnabled;
}
if (!isEmpty(deepLink.deepLinks)) {
return true;
}
return capabilities == null || capabilities[SERVER_APP_ID]?.show === true;
});
};

Expand All @@ -402,18 +406,3 @@ export function isPremiumLicense(licenseType?: LicenseType): boolean {
licenseType === 'trial'
);
}

export function updateGlobalNavigation({
capabilities,
updater$,
enableExperimental,
}: {
capabilities: ApplicationStart['capabilities'];
updater$: Subject<AppUpdater>;
enableExperimental: ExperimentalFeatures;
}) {
updater$.next(() => ({
navLinkStatus: AppNavLinkStatus.hidden, // needed to prevent showing main nav link
deepLinks: getDeepLinks(enableExperimental, undefined, capabilities),
}));
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,12 @@ export const SecuritySolutionTemplateWrapper: React.FC<SecuritySolutionPageWrapp
const showEmptyState = useShowPagesWithEmptyView();
const emptyStateProps = showEmptyState ? NO_DATA_PAGE_TEMPLATE_PROPS : {};

// StyledKibanaPageTemplate is a styled EuiPageTemplate. Security solution currently passes the header and page content as the children of StyledKibanaPageTemplate, as opposed to using the pageHeader prop, which may account for any style discrepancies, such as the bottom border not extending the full width of the page, between EuiPageTemplate and the security solution pages.

/*
* StyledKibanaPageTemplate is a styled EuiPageTemplate. Security solution currently passes the header
* and page content as the children of StyledKibanaPageTemplate, as opposed to using the pageHeader prop,
* which may account for any style discrepancies, such as the bottom border not extending the full width of the page,
* between EuiPageTemplate and the security solution pages.
*/
return (
<StyledKibanaPageTemplate
$isTimelineBottomBarVisible={isTimelineBottomBarVisible}
Expand Down
27 changes: 5 additions & 22 deletions x-pack/plugins/security_solution/public/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Redirect, Route, Switch } from 'react-router-dom';
import { OVERVIEW_PATH } from '../../common/constants';
import { Route, Switch } from 'react-router-dom';

import { NotFoundPage } from './404';
import { SecurityApp } from './app';
Expand All @@ -22,7 +21,7 @@ export const renderApp = ({
services,
store,
usageCollection,
subPlugins,
subPluginRoutes,
}: RenderAppProps): (() => void) => {
const ApplicationUsageTrackingProvider =
usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
Expand All @@ -36,25 +35,9 @@ export const renderApp = ({
>
<ApplicationUsageTrackingProvider>
<Switch>
{[
...subPlugins.overview.routes,
...subPlugins.alerts.routes,
...subPlugins.rules.routes,
...subPlugins.exceptions.routes,
...subPlugins.hosts.routes,
...subPlugins.network.routes,
// will be undefined if enabledExperimental.uebaEnabled === false
...(subPlugins.ueba != null ? subPlugins.ueba.routes : []),
...subPlugins.timelines.routes,
...subPlugins.cases.routes,
...subPlugins.management.routes,
].map((route, index) => (
<Route key={`route-${index}`} {...route} />
))}

<Route path="" exact>
<Redirect to={OVERVIEW_PATH} />
</Route>
{subPluginRoutes.map((route, index) => {
return <Route key={`route-${index}`} {...route} />;
})}
<Route>
<NotFoundPage />
</Route>
Expand Down
47 changes: 47 additions & 0 deletions x-pack/plugins/security_solution/public/app/no_privileges.tsx
Original file line number Diff line number Diff line change
@@ -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<NoPrivilegesPageProps>(({ 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 (
<SecuritySolutionPageWrapper>
<EuiPageTemplate template="centeredContent">
<EmptyPage
actions={emptyPageActions}
message={i18n.NO_PERMISSIONS_MSG(subPluginKey)}
data-test-subj="no_feature_permissions-alerts"
title={i18n.NO_PERMISSIONS_TITLE}
/>
</EuiPageTemplate>
</SecuritySolutionPageWrapper>
);
});

NoPrivilegesPage.displayName = 'NoPrivilegePage';
18 changes: 18 additions & 0 deletions x-pack/plugins/security_solution/public/app/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
4 changes: 2 additions & 2 deletions x-pack/plugins/security_solution/public/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ 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.
*/
export interface RenderAppProps extends AppMountParameters {
services: StartServices;
store: Store<State, Action>;
subPlugins: StartedSubPlugins;
subPluginRoutes: RouteProps[];
usageCollection?: UsageCollectionSetup;
}

Expand Down
Loading

0 comments on commit 436c74a

Please sign in to comment.