diff --git a/x-pack/plugins/security_solution/common/license/license.ts b/x-pack/plugins/security_solution/common/license/license.ts index 2d424ab9c960a..9c093016f4a38 100644 --- a/x-pack/plugins/security_solution/common/license/license.ts +++ b/x-pack/plugins/security_solution/common/license/license.ts @@ -51,5 +51,5 @@ export class LicenseService { } export const isAtLeast = (license: ILicense | null, level: LicenseType): boolean => { - return license !== null && license.isAvailable && license.isActive && license.hasAtLeast(level); + return !!license && license.isAvailable && license.isActive && license.hasAtLeast(level); }; diff --git a/x-pack/plugins/security_solution/public/common/components/current_license/index.tsx b/x-pack/plugins/security_solution/public/common/components/current_license/index.tsx new file mode 100644 index 0000000000000..27d34f5cf418f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/current_license/index.tsx @@ -0,0 +1,29 @@ +/* + * 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 React, { FC, memo, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { licenseService } from '../../hooks/use_license'; +import { AppAction } from '../../store/actions'; +import { ILicense } from '../../../../../licensing/common/types'; + +export const CurrentLicense: FC = memo(({ children }) => { + const dispatch = useDispatch>(); + useEffect(() => { + const subscription = licenseService + .getLicenseInformation$() + ?.subscribe((licenseInformation: ILicense) => { + dispatch({ + type: 'licenseChanged', + payload: licenseInformation, + }); + }); + return () => subscription?.unsubscribe(); + }, [dispatch]); + return <>{children}; +}); + +CurrentLicense.displayName = 'CurrentLicense'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts index bda408cd00e75..573442de807ae 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ILicense } from '../../../../../../../licensing/common/types'; import { GetAgentStatusResponse } from '../../../../../../../fleet/common/types/rest_spec'; import { PolicyData, UIPolicyConfig } from '../../../../../../common/endpoint/types'; import { ServerApiError } from '../../../../../common/types'; @@ -62,6 +63,11 @@ interface UserClickedPolicyDetailsSaveButton { type: 'userClickedPolicyDetailsSaveButton'; } +interface LicenseChanged { + type: 'licenseChanged'; + payload: ILicense; +} + export type PolicyDetailsAction = | ServerReturnedPolicyDetailsData | UserClickedPolicyDetailsSaveButton @@ -70,4 +76,5 @@ export type PolicyDetailsAction = | ServerReturnedUpdatedPolicyDetailsData | ServerFailedToReturnPolicyDetailsData | UserChangedPolicyConfig - | UserChangedAntivirusRegistration; + | UserChangedAntivirusRegistration + | LicenseChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 69c2afbd01960..70ffc1f8a9fc4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -20,6 +20,7 @@ import { } from '../../../../../common/mock/endpoint'; import { HttpFetchOptions } from 'kibana/public'; import { cloneDeep } from 'lodash'; +import { licenseMock } from '../../../../../../../licensing/common/licensing.mock'; describe('policy details: ', () => { let store: Store; @@ -151,6 +152,49 @@ describe('policy details: ', () => { expect(config!.linux.events.file).toEqual(true); }); }); + + describe('when the policy config has paid features enabled', () => { + const CustomMessage = 'Some Popup message change'; + const Basic = licenseMock.createLicense({ license: { type: 'basic', mode: 'basic' } }); + const Platinum = licenseMock.createLicense({ + license: { type: 'platinum', mode: 'platinum' }, + }); + + beforeEach(() => { + const config = policyConfig(getState()); + if (!config) { + throw new Error(); + } + + // have a paid-policy field existing in the store from a previous time + const newPayload1 = cloneDeep(config); + newPayload1.windows.popup.malware.message = CustomMessage; + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload1 }, + }); + }); + + it('preserves paid fields when license level allows', () => { + dispatch({ + type: 'licenseChanged', + payload: Platinum, + }); + const config = policyConfig(getState()); + + expect(config.windows.popup.malware.message).toEqual(CustomMessage); + }); + + it('reverts paid fields to default when license level does not allow', () => { + dispatch({ + type: 'licenseChanged', + payload: Basic, + }); + const config = policyConfig(getState()); + + expect(config.windows.popup.malware.message).not.toEqual(CustomMessage); + }); + }); }); describe('when saving policy data', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts index f039324b3af64..2f9f0d6723749 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts @@ -26,7 +26,6 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory { const http = coreStart.http; - return ({ getState, dispatch }) => (next) => async (action) => { next(action); const state = getState(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index bcdc7ba2089c6..a6e94d3715ca3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -45,6 +45,7 @@ export const initialPolicyDetailsState: () => Immutable = () total: 0, other: 0, }, + license: undefined, }); export const policyDetailsReducer: ImmutableReducer = ( @@ -93,6 +94,13 @@ export const policyDetailsReducer: ImmutableReducer = { ...state, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts index 77e975a46d37b..c52bef9a23b25 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts @@ -6,6 +6,8 @@ import { createSelector } from 'reselect'; import { matchPath } from 'react-router-dom'; +import { ILicense } from '../../../../../../../licensing/common/types'; +import { unsetPolicyFeaturesAboveLicenseLevel } from '../../../../../../common/license/policy_config'; import { PolicyDetailsState } from '../../types'; import { Immutable, @@ -20,6 +22,24 @@ import { ManagementRoutePolicyDetailsParams } from '../../../../types'; /** Returns the policy details */ export const policyDetails = (state: Immutable) => state.policyItem; +/** Returns current active license */ +export const licenseState = (state: Immutable) => state.license; + +export const licensedPolicy: ( + state: Immutable +) => Immutable | undefined = createSelector( + policyDetails, + licenseState, + (policyData, license) => { + if (policyData) { + unsetPolicyFeaturesAboveLicenseLevel( + policyData?.inputs[0]?.config.policy.value, + license as ILicense + ); + } + return policyData; + } +); /** * Given a Policy Data (package policy) object, return back a new object with only the field @@ -75,7 +95,7 @@ export const getPolicyDataForUpdate = ( */ export const policyDetailsForUpdate: ( state: Immutable -) => Immutable | undefined = createSelector(policyDetails, (policy) => { +) => Immutable | undefined = createSelector(licensedPolicy, (policy) => { if (policy) { return getPolicyDataForUpdate(policy); } @@ -111,7 +131,7 @@ const defaultFullPolicy: Immutable = policyConfigFactory(); * Note: this will return a default full policy if the `policyItem` is `undefined` */ export const fullPolicy: (s: Immutable) => PolicyConfig = createSelector( - policyDetails, + licensedPolicy, (policyData) => { return policyData?.inputs[0]?.config?.policy?.value ?? defaultFullPolicy; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 3926ad2220e35..889bcc15d8df0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ILicense } from '../../../../../licensing/common/types'; import { AppLocation, Immutable, @@ -66,6 +67,8 @@ export interface PolicyDetailsState { success: boolean; error?: ServerApiError; }; + /** current license */ + license?: ILicense; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx index f65dbaf1087d8..118ebdf56db90 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx @@ -8,6 +8,7 @@ import React, { ComponentType, memo } from 'react'; import { CoreStart } from 'kibana/public'; import { combineReducers, createStore, compose, applyMiddleware } from 'redux'; import { Provider as ReduxStoreProvider } from 'react-redux'; +import { CurrentLicense } from '../../../../../common/components/current_license'; import { StartPlugins } from '../../../../../types'; import { managementReducer } from '../../../../store/reducer'; import { managementMiddlewareFactory } from '../../../../store/middleware'; @@ -57,7 +58,9 @@ export const withSecurityContext =

({ return ( - + + + ); }); diff --git a/x-pack/plugins/security_solution/public/management/routes.tsx b/x-pack/plugins/security_solution/public/management/routes.tsx index 209d7dd6dbcde..bc24b9ca51980 100644 --- a/x-pack/plugins/security_solution/public/management/routes.tsx +++ b/x-pack/plugins/security_solution/public/management/routes.tsx @@ -8,13 +8,16 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { ManagementContainer } from './pages'; import { NotFoundPage } from '../app/404'; +import { CurrentLicense } from '../common/components/current_license'; /** * Returns the React Router Routes for the management area */ export const ManagementRoutes = () => ( - - - } /> - + + + + } /> + + );