[Security Solutions] Improve deep links generation by capabilities (#116274)

* deep links structure changed

* feature on child deep links not required
This commit is contained in:
Sergi Massaneda 2021-10-26 18:50:48 +02:00 committed by GitHub
parent aa17c1b509
commit 109e0e71c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 73 additions and 53 deletions

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getDeepLinks, PREMIUM_DEEP_LINK_IDS } from '.';
import { getDeepLinks } from '.';
import { AppDeepLink, Capabilities } from '../../../../../../src/core/public';
import { SecurityPageName } from '../types';
import { mockGlobalState } from '../../common/mock';
@ -28,7 +28,7 @@ const basicLicense = 'basic';
const platinumLicense = 'platinum';
describe('deepLinks', () => {
it('should return a subset of links for basic license and the full set for platinum', () => {
it('should return a all basic license deep links in the premium deep links', () => {
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense);
const platinumLinks = getDeepLinks(mockGlobalState.app.enableExperimental, platinumLicense);
@ -50,8 +50,17 @@ describe('deepLinks', () => {
});
};
testAllBasicInPlatinum(basicLinks, platinumLinks);
});
PREMIUM_DEEP_LINK_IDS.forEach((premiumDeepLinkId) => {
it('should not return premium deep links in basic license deep links', () => {
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense);
const platinumLinks = getDeepLinks(mockGlobalState.app.enableExperimental, platinumLicense);
[
SecurityPageName.hostsAnomalies,
SecurityPageName.networkAnomalies,
SecurityPageName.caseConfigure,
].forEach((premiumDeepLinkId) => {
expect(findDeepLink(premiumDeepLinkId, platinumLinks)).toBeTruthy();
expect(findDeepLink(premiumDeepLinkId, basicLinks)).toBeFalsy();
});

View file

@ -7,10 +7,10 @@
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { get } from 'lodash';
import { LicenseType } from '../../../../licensing/common/types';
import { SecurityPageName } from '../types';
import { AppDeepLink, ApplicationStart, AppNavLinkStatus } from '../../../../../../src/core/public';
import { AppDeepLink, AppNavLinkStatus, Capabilities } from '../../../../../../src/core/public';
import {
OVERVIEW,
DETECT,
@ -49,18 +49,28 @@ import {
} from '../../../common/constants';
import { ExperimentalFeatures } from '../../../common/experimental_features';
export const PREMIUM_DEEP_LINK_IDS: Set<string> = new Set([
SecurityPageName.hostsAnomalies,
SecurityPageName.networkAnomalies,
SecurityPageName.caseConfigure,
]);
const FEATURE = {
general: `${SERVER_APP_ID}.show`,
casesRead: `${CASES_FEATURE_ID}.read_cases`,
casesCrud: `${CASES_FEATURE_ID}.crud_cases`,
} as const;
export const securitySolutionsDeepLinks: AppDeepLink[] = [
type Feature = typeof FEATURE[keyof typeof FEATURE];
type SecuritySolutionDeepLink = AppDeepLink & {
isPremium?: boolean;
features?: Feature[];
experimentalKey?: keyof ExperimentalFeatures;
deepLinks?: SecuritySolutionDeepLink[];
};
export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
{
id: SecurityPageName.overview,
title: OVERVIEW,
path: OVERVIEW_PATH,
navLinkStatus: AppNavLinkStatus.visible,
features: [FEATURE.general],
keywords: [
i18n.translate('xpack.securitySolution.search.overview', {
defaultMessage: 'Overview',
@ -73,6 +83,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
title: DETECT,
path: ALERTS_PATH,
navLinkStatus: AppNavLinkStatus.hidden,
features: [FEATURE.general],
keywords: [
i18n.translate('xpack.securitySolution.search.detect', {
defaultMessage: 'Detect',
@ -122,6 +133,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
id: SecurityPageName.explore,
title: EXPLORE,
navLinkStatus: AppNavLinkStatus.hidden,
features: [FEATURE.general],
keywords: [
i18n.translate('xpack.securitySolution.search.explore', {
defaultMessage: 'Explore',
@ -174,6 +186,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
defaultMessage: 'Anomalies',
}),
path: `${HOSTS_PATH}/anomalies`,
isPremium: true,
},
],
},
@ -223,6 +236,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
defaultMessage: 'Anomalies',
}),
path: `${NETWORK_PATH}/anomalies`,
isPremium: true,
},
],
},
@ -233,6 +247,8 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
title: UEBA,
path: UEBA_PATH,
navLinkStatus: AppNavLinkStatus.visible,
features: [FEATURE.general],
experimentalKey: 'uebaEnabled',
keywords: [
i18n.translate('xpack.securitySolution.search.ueba', {
defaultMessage: 'Users & Entities',
@ -244,6 +260,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
id: SecurityPageName.investigate,
title: INVESTIGATE,
navLinkStatus: AppNavLinkStatus.hidden,
features: [FEATURE.general, FEATURE.casesRead],
keywords: [
i18n.translate('xpack.securitySolution.search.investigate', {
defaultMessage: 'Investigate',
@ -255,6 +272,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
title: TIMELINES,
path: TIMELINES_PATH,
navLinkStatus: AppNavLinkStatus.visible,
features: [FEATURE.general],
keywords: [
i18n.translate('xpack.securitySolution.search.timelines', {
defaultMessage: 'Timelines',
@ -276,6 +294,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
title: CASE,
path: CASES_PATH,
navLinkStatus: AppNavLinkStatus.visible,
features: [FEATURE.casesRead],
keywords: [
i18n.translate('xpack.securitySolution.search.cases', {
defaultMessage: 'Cases',
@ -289,6 +308,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
defaultMessage: 'Create New Case',
}),
path: `${CASES_PATH}/create`,
features: [FEATURE.casesCrud],
},
{
id: SecurityPageName.caseConfigure,
@ -296,6 +316,8 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
defaultMessage: 'Configure Cases',
}),
path: `${CASES_PATH}/configure`,
features: [FEATURE.casesCrud],
isPremium: true,
},
],
},
@ -306,6 +328,7 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
title: MANAGE,
path: ENDPOINTS_PATH,
navLinkStatus: AppNavLinkStatus.hidden,
features: [FEATURE.general],
keywords: [
i18n.translate('xpack.securitySolution.search.manage', {
defaultMessage: 'Manage',
@ -348,56 +371,44 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
export function getDeepLinks(
enableExperimental: ExperimentalFeatures,
licenseType?: LicenseType,
capabilities?: ApplicationStart['capabilities']
capabilities?: Capabilities
): AppDeepLink[] {
const isPremium = isPremiumLicense(licenseType);
const hasPremium = 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
.map((deepLink) => {
if (
deepLink.id === SecurityPageName.case &&
capabilities != null &&
capabilities[CASES_FEATURE_ID]?.crud_cases === false
) {
return {
...deepLink,
deepLinks: [],
};
const filterDeepLinks = (securityDeepLinks: SecuritySolutionDeepLink[]): AppDeepLink[] =>
securityDeepLinks.reduce(
(deepLinks: AppDeepLink[], { isPremium, features, experimentalKey, ...deepLink }) => {
if (isPremium && !hasPremium) {
return deepLinks;
}
if (experimentalKey && !enableExperimental[experimentalKey]) {
return deepLinks;
}
if (capabilities != null && !hasFeaturesCapability(features, capabilities)) {
return deepLinks;
}
if (deepLink.deepLinks) {
return {
...deepLink,
deepLinks: filterDeepLinks(deepLink.deepLinks),
};
deepLinks.push({ ...deepLink, deepLinks: filterDeepLinks(deepLink.deepLinks) });
} else {
deepLinks.push(deepLink);
}
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;
});
};
return deepLinks;
},
[]
);
return filterDeepLinks(securitySolutionsDeepLinks);
}
function hasFeaturesCapability(
features: Feature[] | undefined,
capabilities: Capabilities
): boolean {
if (!features) {
return true;
}
return features.some((featureKey) => get(capabilities, featureKey, false));
}
export function isPremiumLicense(licenseType?: LicenseType): boolean {
return (
licenseType === 'gold' ||