Skip to content

Commit

Permalink
[Security Solution][Endpoint][Admin] Malware user notification is a p…
Browse files Browse the repository at this point in the history
…latinum tiered feature (#82894)
  • Loading branch information
parkiino committed Nov 18, 2020
1 parent b9fc45b commit 69e3ceb
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 20 deletions.
60 changes: 60 additions & 0 deletions 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<ILicense> | null = null;
private subscription: Subscription | null = null;
private licenseInformation: ILicense | null = null;

private updateInformation(licenseInformation: ILicense) {
this.licenseInformation = licenseInformation;
}

public start(license$: Observable<ILicense>) {
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')
);
}
}
@@ -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;
}
Expand Up @@ -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<ReturnType<typeof render>['find']>;
Expand Down Expand Up @@ -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(<PolicyDetails />);
});

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(<PolicyDetails />);
});

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);
});
});
});
});
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Array<{
id: ProtectionModes;
Expand Down Expand Up @@ -217,24 +219,31 @@ export const MalwareProtections = React.memo(() => {
);
})}
</ProtectionRadioGroup>
<EuiSpacer size="s" />
<ConfigFormHeading>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetailsConfig.userNotification"
defaultMessage="User Notification"
/>
</ConfigFormHeading>
<SupportedVersionNotice optionName="malware" />
<EuiSpacer size="s" />
<EuiCheckbox
id="xpack.securitySolution.endpoint.policyDetail.malware.userNotification"
onChange={handleUserNotificationCheckbox}
checked={userNotificationSelected}
label={i18n.translate('xpack.securitySolution.endpoint.policyDetail.malware.notifyUser', {
defaultMessage: 'Notify User',
})}
/>
{userNotificationSelected && (
{isPlatinumPlus && (
<>
<ConfigFormHeading>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetailsConfig.userNotification"
defaultMessage="User Notification"
/>
</ConfigFormHeading>
<SupportedVersionNotice optionName="malware" />
<EuiSpacer size="s" />
<EuiCheckbox
data-test-subj="malwareUserNotificationCheckbox"
id="xpack.securitySolution.endpoint.policyDetail.malware.userNotification"
onChange={handleUserNotificationCheckbox}
checked={userNotificationSelected}
label={i18n.translate(
'xpack.securitySolution.endpoint.policyDetail.malware.notifyUser',
{
defaultMessage: 'Notify User',
}
)}
/>
</>
)}
{isPlatinumPlus && userNotificationSelected && (
<>
<EuiSpacer size="s" />
<EuiText size="xs">
Expand All @@ -256,13 +265,15 @@ export const MalwareProtections = React.memo(() => {
value={userNotificationMessage}
onChange={handleCustomUserNotification}
fullWidth={true}
data-test-subj="malwareUserNotificationCustomMessage"
/>
</>
)}
</>
);
}, [
radios,
isPlatinumPlus,
handleUserNotificationCheckbox,
userNotificationSelected,
userNotificationMessage,
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/public/plugin.tsx
Expand Up @@ -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';

Expand Down Expand Up @@ -345,6 +346,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
component: LazyEndpointPolicyCreateExtension,
});
}
licenseService.start(plugins.licensing.license$);

return {};
}
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/public/types.ts
Expand Up @@ -33,6 +33,7 @@ import { Network } from './network';
import { Overview } from './overview';
import { Timelines } from './timelines';
import { Management } from './management';
import { LicensingPluginStart } from '../../licensing/public';

export interface SetupPlugins {
home?: HomePublicPluginSetup;
Expand All @@ -49,6 +50,7 @@ export interface StartPlugins {
inspector: InspectorStart;
ingestManager?: IngestManagerStart;
lists?: ListsPluginStart;
licensing: LicensingPluginStart;
newsfeed?: NewsfeedPublicPluginStart;
triggersActionsUi: TriggersActionsStart;
uiActions: UiActionsStart;
Expand Down
@@ -0,0 +1,9 @@
/*
* 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();
9 changes: 7 additions & 2 deletions x-pack/plugins/security_solution/server/plugin.ts
Expand Up @@ -33,7 +33,7 @@ import { MlPluginSetup as MlSetup } from '../../ml/server';
import { ListPluginSetup } from '../../lists/server';
import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server';
import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { ILicense, LicensingPluginStart } from '../../licensing/server';
import { IngestManagerStartContract, ExternalCallback } from '../../fleet/server';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import { initServer } from './init_server';
Expand Down Expand Up @@ -74,13 +74,13 @@ import {
TelemetryPluginStart,
TelemetryPluginSetup,
} from '../../../../src/plugins/telemetry/server';
import { licenseService } from './lib/license/license';

export interface SetupPlugins {
alerts: AlertingSetup;
data: DataPluginSetup;
encryptedSavedObjects?: EncryptedSavedObjectsSetup;
features: FeaturesSetup;
licensing: LicensingPluginSetup;
lists?: ListPluginSetup;
ml?: MlSetup;
security?: SecuritySetup;
Expand All @@ -94,6 +94,7 @@ export interface StartPlugins {
alerts: AlertPluginStartContract;
data: DataPluginStart;
ingestManager?: IngestManagerStartContract;
licensing: LicensingPluginStart;
taskManager?: TaskManagerStartContract;
telemetry?: TelemetryPluginStart;
}
Expand Down Expand Up @@ -125,6 +126,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
private readonly telemetryEventsSender: TelemetryEventsSender;

private lists: ListPluginSetup | undefined; // TODO: can we create ListPluginStart?
private licensing$!: Observable<ILicense>;

private manifestTask: ManifestTask | undefined;
private exceptionsCache: LRU<string, Buffer>;
Expand Down Expand Up @@ -364,6 +366,8 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
}

this.telemetryEventsSender.start(core, plugins.telemetry);
this.licensing$ = plugins.licensing.license$;
licenseService.start(this.licensing$);

return {};
}
Expand All @@ -372,5 +376,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
this.logger.debug('Stopping plugin');
this.telemetryEventsSender.stop();
this.endpointAppContextService.stop();
licenseService.stop();
}
}

0 comments on commit 69e3ceb

Please sign in to comment.