Skip to content

Commit

Permalink
[Security Solution] Static config settings for serverless (elastic#16…
Browse files Browse the repository at this point in the history
…7856)

## Summary

This PR implements a standard way to have different static settings for
the serverless and ess (stateful) environments. It centralizes flags,
which were set using different approaches previously, in a single
configuration.

This aims to make it easier for developers to enable/disable parts of
the application in serverless projects.

Default:
```
  sideNavEnabled: true,
  ILMEnabled: true,
  ESQLEnabled: true,
```

Serverless:
```
xpack.securitySolution.offeringSettings: {
    sideNavEnabled: false, # Internal security side navigation disabled, the serverless global chrome navigation is used instead
    ILMEnabled: false, # Index Lifecycle Management (ILM) functionalities disabled, not supported by serverless Elasticsearch
    ESQLEnabled: false, # ES|QL disabled, not supported by serverless Elasticsearch
  }
```

### Consume the settings

#### Server 
- Plugin parsed `ConfigType`:
`this.config.settings.ESQLEnabled`

#### UI
- Plugin attribute: 
`this.configSettings.ESQLEnabled`.
- Components can access it from Kibana services:
`useKibana().services.configSettings.ESQLEnabled;`

---------

Co-authored-by: Vitalii Dmyterko <[email protected]>
  • Loading branch information
semd and vitaliidm authored Oct 9, 2023
1 parent 6e548c0 commit c7df950
Show file tree
Hide file tree
Showing 25 changed files with 161 additions and 68 deletions.
10 changes: 6 additions & 4 deletions config/serverless.security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ xpack.securitySolutionServerless.productTypes:
{ product_line: 'endpoint', product_tier: 'complete' },
]

xpack.securitySolution.offeringSettings: {
sideNavEnabled: false, # Internal security side navigation disabled, the serverless global chrome navigation is used instead
ILMEnabled: false, # Index Lifecycle Management (ILM) functionalities disabled, not supported by serverless Elasticsearch
ESQLEnabled: false, # ES|QL disabled, not supported by serverless Elasticsearch
}

## Set the home route
uiSettings.overrides.defaultRoute: /app/security/get_started

Expand All @@ -33,10 +39,6 @@ xpack.fleet.internal.registry.spec.max: '3.0'
# xpack.fleet.internal.registry.kibanaVersionCheckEnabled: false
# xpack.fleet.internal.registry.spec.min: '3.0'

# Serverless security specific options
xpack.securitySolution.enableExperimental:
- esqlRulesDisabled

xpack.ml.ad.enabled: true
xpack.ml.dfa.enabled: true
xpack.ml.nlp.enabled: false
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.spaces.allowFeatureVisibility (any)',
'xpack.securitySolution.enableExperimental (array)',
'xpack.securitySolution.prebuiltRulesPackageVersion (string)',
'xpack.securitySolution.offeringSettings (record)',
'xpack.snapshot_restore.slm_ui.enabled (boolean)',
'xpack.snapshot_restore.ui.enabled (boolean)',
'xpack.stack_connectors.enableExperimental (array)',
Expand Down
65 changes: 65 additions & 0 deletions x-pack/plugins/security_solution/common/config_settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export interface ConfigSettings {
/**
* Security solution internal side navigation enabled
*/
sideNavEnabled: boolean;
/**
* Index Lifecycle Management (ILM) feature enabled.
*/
ILMEnabled: boolean;
/**
* ESQL queries enabled.
*/
ESQLEnabled: boolean;
}

/**
* A list of allowed values that can be override in `xpack.securitySolution.offeringSettings`.
* This object is then used to validate and parse the value entered.
*/
export const defaultSettings: ConfigSettings = Object.freeze({
sideNavEnabled: true,
ILMEnabled: true,
ESQLEnabled: true,
});

type ConfigSettingsKey = keyof ConfigSettings;

/**
* Parses the string value used in `xpack.securitySolution.offeringSettings` kibana configuration,
*
* @param offeringSettings
*/
export const parseConfigSettings = (
offeringSettings: Record<string, boolean>
): { settings: ConfigSettings; invalid: string[] } => {
const configSettings: Partial<ConfigSettings> = {};
const invalidKeys: string[] = [];

for (const optionKey in offeringSettings) {
if (defaultSettings[optionKey as ConfigSettingsKey] == null) {
invalidKeys.push(optionKey);
} else {
configSettings[optionKey as ConfigSettingsKey] = offeringSettings[optionKey];
}
}

const settings: ConfigSettings = Object.freeze({
...defaultSettings,
...configSettings,
});

return {
settings,
invalid: invalidKeys,
};
};

export const getDefaultConfigSettings = (): ConfigSettings => ({ ...defaultSettings });
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,27 @@
*/

import { renderHook } from '@testing-library/react-hooks';
import { BehaviorSubject } from 'rxjs';
import { useSecuritySolutionNavigation } from './use_security_solution_navigation';

jest.mock('../breadcrumbs', () => ({
useBreadcrumbsNav: () => jest.fn(),
}));

const mockIsSidebarEnabled$ = new BehaviorSubject(true);
const mockIsSideNavEnabled = jest.fn(() => true);
jest.mock('../../../lib/kibana/kibana_react', () => {
return {
useKibana: () => ({
services: {
isSidebarEnabled$: mockIsSidebarEnabled$.asObservable(),
configSettings: {
sideNavEnabled: mockIsSideNavEnabled(),
},
},
}),
};
});

describe('Security Solution Navigation', () => {
beforeEach(() => {
mockIsSidebarEnabled$.next(true);
jest.clearAllMocks();
});

Expand All @@ -44,7 +44,7 @@ describe('Security Solution Navigation', () => {
});

it('should return undefined props when disabled', () => {
mockIsSidebarEnabled$.next(false);
mockIsSideNavEnabled.mockReturnValueOnce(false);
const { result } = renderHook(useSecuritySolutionNavigation);
expect(result.current).toEqual(undefined);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
*/

import React from 'react';
import useObservable from 'react-use/lib/useObservable';
import { i18n } from '@kbn/i18n';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import { useKibana } from '../../../lib/kibana';
Expand All @@ -24,12 +23,11 @@ const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mai
});

export const useSecuritySolutionNavigation = (): KibanaPageTemplateProps['solutionNav'] => {
const { isSidebarEnabled$ } = useKibana().services;
const isSidebarEnabled = useObservable(isSidebarEnabled$);
const { sideNavEnabled } = useKibana().services.configSettings;

useBreadcrumbsNav();

if (!isSidebarEnabled) {
if (!sideNavEnabled) {
return undefined;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { NavigationProvider } from '@kbn/security-solution-navigation';
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { savedSearchPluginMock } from '@kbn/saved-search-plugin/public/mocks';
import { contractStartServicesMock } from '../../../mocks';
import { getDefaultConfigSettings } from '../../../../common/config_settings';

const mockUiSettings: Record<string, unknown> = {
[DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' },
Expand Down Expand Up @@ -126,6 +127,7 @@ export const createStartServicesMock = (
return {
...core,
...contractStartServicesMock,
configSettings: getDefaultConfigSettings(),
apm,
cases,
unifiedSearch,
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/public/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface ServerApiError {
export interface SecuritySolutionUiConfigType {
enableExperimental: string[];
prebuiltRulesPackageVersion?: string;
offeringSettings: Record<string, boolean>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,20 @@ import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_ex
jest.mock('../../../../common/lib/kibana');

jest.mock('../../../../common/hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(false),
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(false), // enabled (esqlRulesDisabled = false)
}));
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;

const mockESQLEnabled = jest.fn(() => true);
jest.mock('../../../../common/lib/kibana', () => {
const useKibana = jest.requireActual('../../../../common/lib/kibana').useKibana;
return {
useKibana: () => ({
services: { ...useKibana().services, configSettings: { ESQLEnabled: mockESQLEnabled() } },
}),
};
});

describe('SelectRuleType', () => {
it('renders correctly', () => {
const Component = () => {
Expand Down Expand Up @@ -187,8 +197,27 @@ describe('SelectRuleType', () => {
expect(wrapper.find('[data-test-subj="esqlRuleType"]').exists()).toBeTruthy();
});

it('should not render "esql" rule type if its feature disabled', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
it('should not render "esql" rule type if esql setting is disabled', () => {
mockESQLEnabled.mockReturnValueOnce(false);
const Component = () => {
const field = useFormFieldMock();

return (
<SelectRuleType
field={field}
describedByIds={[]}
isUpdateView={false}
hasValidLicense={true}
isMlAdmin={true}
/>
);
};
const wrapper = shallow(<Component />);
expect(wrapper.find('[data-test-subj="esqlRuleType"]').exists()).toBeFalsy();
});

it('should not render "esql" rule type if the feature flag is disabled', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(true); // disabled (esqlRulesDisabled = true)
const Component = () => {
const field = useFormFieldMock();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as i18n from './translations';
import { MlCardDescription } from './ml_card_description';
import { TechnicalPreviewBadge } from '../technical_preview_badge';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useKibana } from '../../../../common/lib/kibana';

interface SelectRuleTypeProps {
describedByIds: string[];
Expand All @@ -49,7 +50,9 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = memo(
const setNewTerms = useCallback(() => setType('new_terms'), [setType]);
const setEsql = useCallback(() => setType('esql'), [setType]);

const isEsqlFeatureEnabled = !useIsExperimentalFeatureEnabled('esqlRulesDisabled');
const isEsqlSettingEnabled = useKibana().services.configSettings.ESQLEnabled;
const isEsqlFeatureFlagEnabled = !useIsExperimentalFeatureEnabled('esqlRulesDisabled');
const isEsqlFeatureEnabled = isEsqlSettingEnabled && isEsqlFeatureFlagEnabled;

const eqlSelectableConfig = useMemo(
() => ({
Expand Down
4 changes: 0 additions & 4 deletions x-pack/plugins/security_solution/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,19 @@ const upselling = new UpsellingService();

export const contractStartServicesMock: ContractStartServices = {
extraRoutes$: of([]),
isSidebarEnabled$: of(true),
getComponent$: jest.fn(),
upselling,
dataQualityPanelConfig: undefined,
};

const setupMock = (): PluginSetup => ({
resolver: jest.fn(),
experimentalFeatures: allowedExperimentalValues, // default values
setAppLinksSwitcher: jest.fn(),
setDeepLinksFormatter: jest.fn(),
setDataQualityPanelConfig: jest.fn(),
});

const startMock = (): PluginStart => ({
getNavLinks$: jest.fn(() => new BehaviorSubject<NavigationLink[]>([])),
setIsSidebarEnabled: jest.fn(),
setComponents: jest.fn(),
getBreadcrumbsNav$: jest.fn(
() => new BehaviorSubject<BreadcrumbsNav>({ leading: [], trailing: [] })
Expand Down
2 changes: 0 additions & 2 deletions x-pack/plugins/security_solution/public/overview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
import type { SecuritySubPlugin } from '../app/types';
import { routes } from './routes';

export * from './types';

export class Overview {
public setup() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@
import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { of } from 'rxjs';

import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__';
import { TestProviders } from '../../common/mock';
import { DataQuality } from './data_quality';
import { HOT, WARM, UNMANAGED } from './translations';

const mockedUseKibana = mockUseKibana();
const mockIsILMAvailable = of(true);

jest.mock('../../common/components/landing_page');
jest.mock('../../common/lib/kibana', () => {
Expand Down Expand Up @@ -51,7 +49,7 @@ jest.mock('../../common/lib/kibana', () => {
useCasesAddToNewCaseFlyout: jest.fn(),
},
},
isILMAvailable$: mockIsILMAvailable,
configSettings: { ILMEnabled: true },
},
}),
useUiSetting$: () => ['0,0.[000]'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,8 @@ import type {
ReportDataQualityCheckAllCompletedParams,
ReportDataQualityIndexCheckedParams,
} from '../../common/lib/telemetry';
import type { DataQualityPanelConfig } from '../types';

const LOCAL_STORAGE_KEY = 'dataQualityDashboardLastChecked';
const defaultDataQualityPanelConfig: DataQualityPanelConfig = { isILMAvailable: true };

const comboBoxStyle: React.CSSProperties = {
width: '322px',
Expand Down Expand Up @@ -158,8 +156,8 @@ const DataQualityComponent: React.FC = () => {
const [selectedOptions, setSelectedOptions] = useState<EuiComboBoxOptionOption[]>(defaultOptions);
const { indicesExist, loading: isSourcererLoading, selectedPatterns } = useSourcererDataView();
const { signalIndexName, loading: isSignalIndexNameLoading } = useSignalIndex();
const { dataQualityPanelConfig = defaultDataQualityPanelConfig, cases } = useKibana().services;
const { isILMAvailable } = dataQualityPanelConfig;
const { configSettings, cases } = useKibana().services;
const isILMAvailable = configSettings.ILMEnabled;

const [startDate, setStartTime] = useState<string>();
const [endDate, setEndTime] = useState<string>();
Expand All @@ -173,7 +171,7 @@ const DataQualityComponent: React.FC = () => {
};

useEffect(() => {
if (isILMAvailable === false) {
if (!isILMAvailable) {
setStartTime(DEFAULT_START_TIME);
setEndTime(DEFAULT_END_TIME);
}
Expand Down
10 changes: 0 additions & 10 deletions x-pack/plugins/security_solution/public/overview/types.ts

This file was deleted.

4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/vie
import type { SecurityAppStore } from './common/store/types';
import { PluginContract } from './plugin_contract';
import { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service';
import { parseConfigSettings, type ConfigSettings } from '../common/config_settings';

export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
/**
Expand Down Expand Up @@ -84,6 +85,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
private telemetry: TelemetryService;

readonly experimentalFeatures: ExperimentalFeatures;
readonly configSettings: ConfigSettings;
private queryService: QueryService = new QueryService();
private nowProvider: NowProvider = new NowProvider();

Expand All @@ -92,6 +94,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
this.experimentalFeatures = parseExperimentalConfigValue(
this.config.enableExperimental || []
).features;
this.configSettings = parseConfigSettings(this.config.offeringSettings ?? {}).settings;
this.kibanaVersion = initializerContext.env.packageInfo.version;
this.kibanaBranch = initializerContext.env.packageInfo.branch;
this.prebuiltRulesPackageVersion = this.config.prebuiltRulesPackageVersion;
Expand Down Expand Up @@ -185,6 +188,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
...coreStart,
...startPlugins,
...this.contract.getStartServices(),
configSettings: this.configSettings,
apm,
savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(),
setHeaderActionMenu: params.setHeaderActionMenu,
Expand Down
Loading

0 comments on commit c7df950

Please sign in to comment.