From 48ae068b88669f6fea0e79e01e5dc06e7c3934b0 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Wed, 18 Nov 2020 09:57:55 -0500 Subject: [PATCH] [Security Solution][Endpoint][Admin] Malware user notification is a platinum tiered feature (#82894) --- .../common/license/license.ts | 60 +++++++++++++++++++ .../public/common/hooks/use_license.ts | 13 ++++ .../pages/policy/view/policy_details.test.tsx | 47 +++++++++++++++ .../view/policy_forms/protections/malware.tsx | 47 +++++++++------ .../security_solution/public/plugin.tsx | 2 + .../plugins/security_solution/public/types.ts | 2 + .../server/lib/license/license.ts | 9 +++ .../security_solution/server/plugin.ts | 9 ++- 8 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/license/license.ts create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_license.ts create mode 100644 x-pack/plugins/security_solution/server/lib/license/license.ts diff --git a/x-pack/plugins/security_solution/common/license/license.ts b/x-pack/plugins/security_solution/common/license/license.ts new file mode 100644 index 0000000000000..96c1a14ceb1f4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/license/license.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Observable, Subscription } from 'rxjs'; +import { ILicense } from '../../../licensing/common/types'; + +// Generic license service class that works with the license observable +// Both server and client plugins instancates a singleton version of this class +export class LicenseService { + private observable: Observable | null = null; + private subscription: Subscription | null = null; + private licenseInformation: ILicense | null = null; + + private updateInformation(licenseInformation: ILicense) { + this.licenseInformation = licenseInformation; + } + + public start(license$: Observable) { + this.observable = license$; + this.subscription = this.observable.subscribe(this.updateInformation.bind(this)); + } + + public stop() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public getLicenseInformation() { + return this.licenseInformation; + } + + public getLicenseInformation$() { + return this.observable; + } + + public isGoldPlus() { + return ( + this.licenseInformation?.isAvailable && + this.licenseInformation?.isActive && + this.licenseInformation?.hasAtLeast('gold') + ); + } + public isPlatinumPlus() { + return ( + this.licenseInformation?.isAvailable && + this.licenseInformation?.isActive && + this.licenseInformation?.hasAtLeast('platinum') + ); + } + public isEnterprise() { + return ( + this.licenseInformation?.isAvailable && + this.licenseInformation?.isActive && + this.licenseInformation?.hasAtLeast('enterprise') + ); + } +} diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_license.ts b/x-pack/plugins/security_solution/public/common/hooks/use_license.ts new file mode 100644 index 0000000000000..db4d588bf293f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_license.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LicenseService } from '../../../common/license/license'; + +export const licenseService = new LicenseService(); + +export function useLicense() { + return licenseService; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index abc9a2cbd027c..bfa592b1f9c8e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -13,8 +13,20 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_da import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { getPolicyDetailPath, getEndpointListPath } from '../../../common/routing'; import { policyListApiPathHandlers } from '../store/policy_list/test_mock_utils'; +import { licenseService } from '../../../../common/hooks/use_license'; jest.mock('../../../../common/components/link_to'); +jest.mock('../../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); describe('Policy Details', () => { type FindReactWrapperResponse = ReturnType['find']>; @@ -275,5 +287,40 @@ describe('Policy Details', () => { }); }); }); + describe('when the subscription tier is platinum or higher', () => { + beforeEach(() => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); + policyView = render(); + }); + + it('malware popup and message customization options are shown', () => { + // use query for finding stuff, if it doesn't find it, just returns null + const userNotificationCheckbox = policyView.find( + 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' + ); + const userNotificationCustomMessageTextArea = policyView.find( + 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' + ); + expect(userNotificationCheckbox).toHaveLength(1); + expect(userNotificationCustomMessageTextArea).toHaveLength(1); + }); + }); + describe('when the subscription tier is gold or lower', () => { + beforeEach(() => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + policyView = render(); + }); + + it('malware popup and message customization options are hidden', () => { + const userNotificationCheckbox = policyView.find( + 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' + ); + const userNotificationCustomMessageTextArea = policyView.find( + 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' + ); + expect(userNotificationCheckbox).toHaveLength(0); + expect(userNotificationCustomMessageTextArea).toHaveLength(0); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 7259b2ec19ee2..c72093552f551 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -30,6 +30,7 @@ import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; import { popupVersionsMap } from './popup_options_to_versions'; +import { useLicense } from '../../../../../../common/hooks/use_license'; const ProtectionRadioGroup = styled.div` display: flex; @@ -116,6 +117,7 @@ export const MalwareProtections = React.memo(() => { policyDetailsConfig && policyDetailsConfig.windows.popup.malware.enabled; const userNotificationMessage = policyDetailsConfig && policyDetailsConfig.windows.popup.malware.message; + const isPlatinumPlus = useLicense().isPlatinumPlus(); const radios: Immutable { ); })} - - - - - - - - {userNotificationSelected && ( + {isPlatinumPlus && ( + <> + + + + + + + + )} + {isPlatinumPlus && userNotificationSelected && ( <> @@ -256,6 +265,7 @@ export const MalwareProtections = React.memo(() => { value={userNotificationMessage} onChange={handleCustomUserNotification} fullWidth={true} + data-test-subj="malwareUserNotificationCustomMessage" /> )} @@ -263,6 +273,7 @@ export const MalwareProtections = React.memo(() => { ); }, [ radios, + isPlatinumPlus, handleUserNotificationCheckbox, userNotificationSelected, userNotificationMessage, diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 5895880adb26a..5cc0d79a3f9a3 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -60,6 +60,7 @@ import { } from '../common/search_strategy/index_fields'; import { SecurityAppStore } from './common/store/store'; import { getCaseConnectorUI } from './common/lib/connectors'; +import { licenseService } from './common/hooks/use_license'; import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; @@ -345,6 +346,7 @@ export class Plugin implements IPlugin; private manifestTask: ManifestTask | undefined; private exceptionsCache: LRU; @@ -364,6 +366,8 @@ export class Plugin implements IPlugin