Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SECURITY SOLUTION] [CASES] Allow cases to be there when security solutions privileges is none #113573

Merged
merged 25 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7b2ce23
allow case to itself when security solutions privileges is none
XavierM Sep 30, 2021
60609ba
Merge branch 'master' of github.com:elastic/kibana into cases-top-fea…
XavierM Oct 14, 2021
ddeb0b7
bring back the right owner for cases
XavierM Oct 14, 2021
dc8e77f
bring no privilege msg when needed it
XavierM Oct 14, 2021
0081e9f
Merge branch 'master' of github.com:elastic/kibana into cases-top-fea…
XavierM Oct 15, 2021
e950051
fix types
XavierM Oct 15, 2021
dd2fa10
fix test
XavierM Oct 15, 2021
44d54b2
adding test
XavierM Oct 16, 2021
0575e04
Merge branch 'master' of github.com:elastic/kibana into cases-top-fea…
XavierM Oct 16, 2021
a689200
merge and type fix
semd Oct 18, 2021
e4c7f01
Merge branch 'master' of github.com:elastic/kibana into cases-top-fea…
XavierM Oct 18, 2021
b4ac98c
Merge branch 'master' of github.com:elastic/kibana into cases-top-fea…
XavierM Oct 20, 2021
266904a
review
XavierM Oct 20, 2021
d4c87a5
Merge branch 'master' of github.com:elastic/kibana into cases-top-fea…
XavierM Oct 20, 2021
11e89a3
deepLinks generation fixed
semd Oct 21, 2021
bc24a21
register home solution with old app id
semd Oct 21, 2021
3548d79
fix get deep links
semd Oct 21, 2021
74a3871
fix home link
XavierM Oct 21, 2021
516a654
Merge branch 'cases-top-feature' of github.com:XavierM/kibana into ca…
XavierM Oct 21, 2021
1dcccff
fix unit test
XavierM Oct 21, 2021
d6cce9d
add test
XavierM Oct 21, 2021
0fd969a
Merge branch 'master' of github.com:elastic/kibana into cases-top-fea…
XavierM Oct 21, 2021
ce015c9
fix telemetry
XavierM Oct 21, 2021
2953dce
Merge branch 'master' of github.com:elastic/kibana into cases-top-fea…
XavierM Oct 21, 2021
401ba0f
Merge branch 'master' into cases-top-feature
kibanamachine Oct 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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