[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:
parent
aa17c1b509
commit
109e0e71c7
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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' ||
|
||||
|
|
Loading…
Reference in a new issue