diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts index 479ff4753dd7..cc493d53c029 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts @@ -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(); }); 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 8daec76f280b..c8b058ef2913 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 @@ -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 = 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' ||