mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Serverless] AppFeatures improvements (#158935)
## Summary
issue: https://github.com/elastic/kibana/issues/158810
follow-up of: https://github.com/elastic/kibana/pull/158179
Improves the Security AppFeatures architecture:
- SubFeatures now preserve always the same order in the Security Kibana
config, despite the order of processing of enabled appFeatures.

- Change the `productTypes` config format
- Update `getProductAppFeatures` to:
- process the new `productTypes` format.
- include _essentials_ tiers PLIs inside _complete_ tiers automatically.
- AppFeatures module now receives an array of PLIs instead of an object
- AppFeatures config now uses only SubFeature IDS instead of
`subActions` config objects directly
- Upselling components updated and `useProductTypeByPLI` implemented to
display the Product Type required
This commit is contained in:
parent
e2e03cac3b
commit
0fe67b2c04
27 changed files with 807 additions and 478 deletions
|
@ -8,7 +8,7 @@ xpack.uptime.enabled: false
|
|||
|
||||
## Enable the Serverless Security plugin
|
||||
xpack.serverless.security.enabled: true
|
||||
xpack.serverless.security.productLineIds: ['securityComplete']
|
||||
xpack.serverless.security.productTypes: [{ product_line: 'security', product_tier: 'complete' }]
|
||||
|
||||
## Set the home route
|
||||
uiSettings.overrides.defaultRoute: /app/security/get_started
|
||||
|
|
|
@ -119,7 +119,7 @@ pageLoadAssetSize:
|
|||
serverless: 16573
|
||||
serverlessObservability: 30000
|
||||
serverlessSearch: 30000
|
||||
serverlessSecurity: 41807
|
||||
serverlessSecurity: 66019
|
||||
sessionView: 77750
|
||||
share: 71239
|
||||
snapshotRestore: 79032
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AppFeatureKey, AppFeatureKeys } from '@kbn/security-solution-plugin/common';
|
||||
import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-plugin/common';
|
||||
|
||||
export const DEFAULT_APP_FEATURES: AppFeatureKeys = {
|
||||
[AppFeatureKey.advancedInsights]: true,
|
||||
[AppFeatureKey.casesConnectors]: true,
|
||||
};
|
||||
// Just copying all feature keys for now.
|
||||
// We may need a different set of keys in the future if we create serverless-specific appFeatures
|
||||
export const DEFAULT_APP_FEATURES = [...ALL_APP_FEATURE_KEYS];
|
||||
|
|
|
@ -12,7 +12,7 @@ export { APP_UI_ID, APP_ID, CASES_FEATURE_ID, SERVER_APP_ID, SecurityPageName }
|
|||
export { ELASTIC_SECURITY_RULE_ID } from './detection_engine/constants';
|
||||
export { allowedExperimentalValues, type ExperimentalFeatures } from './experimental_features';
|
||||
export type { AppFeatureKeys } from './types/app_features';
|
||||
export { AppFeatureKey } from './types/app_features';
|
||||
export { AppFeatureKey, ALL_APP_FEATURE_KEYS } from './types/app_features';
|
||||
|
||||
// Careful of exporting anything from this file as any file(s) you export here will cause your page bundle size to increase.
|
||||
// If you're using functions/types/etc... internally it's best to import directly from their paths than expose the functions/types/etc... here.
|
||||
|
|
|
@ -20,10 +20,9 @@ export enum AppFeatureCasesKey {
|
|||
}
|
||||
|
||||
// Merges the two enums.
|
||||
export type AppFeatureKey = AppFeatureSecurityKey | AppFeatureCasesKey;
|
||||
export type AppFeatureKeys = AppFeatureKey[];
|
||||
|
||||
// We need to merge the value and the type and export both to replicate how enum works.
|
||||
export const AppFeatureKey = { ...AppFeatureSecurityKey, ...AppFeatureCasesKey };
|
||||
export type AppFeatureKey = AppFeatureSecurityKey | AppFeatureCasesKey;
|
||||
|
||||
type AppFeatureSecurityKeys = { [key in AppFeatureSecurityKey]: boolean };
|
||||
type AppFeatureCasesKeys = { [key in AppFeatureCasesKey]: boolean };
|
||||
export type AppFeatureKeys = AppFeatureSecurityKeys & AppFeatureCasesKeys;
|
||||
export const ALL_APP_FEATURE_KEYS = Object.freeze(Object.values(AppFeatureKey));
|
||||
|
|
|
@ -50,15 +50,28 @@ const CASES_APP_FEATURE_CONFIG = {
|
|||
|
||||
jest.mock('./security_kibana_features', () => {
|
||||
return {
|
||||
getSecurityBaseKibanaFeature: jest.fn().mockReturnValue(SECURITY_BASE_CONFIG),
|
||||
getSecurityAppFeaturesConfig: jest.fn().mockReturnValue(SECURITY_APP_FEATURE_CONFIG),
|
||||
getSecurityBaseKibanaFeature: jest.fn(() => SECURITY_BASE_CONFIG),
|
||||
getSecurityBaseKibanaSubFeatureIds: jest.fn(() => ['subFeature1']),
|
||||
getSecurityAppFeaturesConfig: jest.fn(() => SECURITY_APP_FEATURE_CONFIG),
|
||||
};
|
||||
});
|
||||
jest.mock('./security_kibana_sub_features', () => {
|
||||
return {
|
||||
securitySubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./security_cases_kibana_features', () => {
|
||||
return {
|
||||
getCasesBaseKibanaFeature: jest.fn().mockReturnValue(CASES_BASE_CONFIG),
|
||||
getCasesAppFeaturesConfig: jest.fn().mockReturnValue(CASES_APP_FEATURE_CONFIG),
|
||||
getCasesBaseKibanaFeature: jest.fn(() => CASES_BASE_CONFIG),
|
||||
getCasesBaseKibanaSubFeatureIds: jest.fn(() => ['subFeature1']),
|
||||
getCasesAppFeaturesConfig: jest.fn(() => CASES_APP_FEATURE_CONFIG),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./security_cases_kibana_sub_features', () => {
|
||||
return {
|
||||
casesSubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -69,9 +82,7 @@ describe('AppFeatures', () => {
|
|||
getKibanaFeatures: jest.fn(),
|
||||
} as unknown as PluginSetupContract;
|
||||
|
||||
const appFeatureKeys = {
|
||||
'test-base-feature': true,
|
||||
} as unknown as AppFeatureKeys;
|
||||
const appFeatureKeys = ['test-base-feature'] as unknown as AppFeatureKeys;
|
||||
|
||||
const appFeatures = new AppFeatures(
|
||||
{} as unknown as Logger,
|
||||
|
@ -83,6 +94,7 @@ describe('AppFeatures', () => {
|
|||
expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({
|
||||
...SECURITY_BASE_CONFIG,
|
||||
...SECURITY_APP_FEATURE_CONFIG['test-base-feature'],
|
||||
subFeatures: [{ baz: 'baz' }],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -91,9 +103,7 @@ describe('AppFeatures', () => {
|
|||
registerKibanaFeature: jest.fn(),
|
||||
} as unknown as PluginSetupContract;
|
||||
|
||||
const appFeatureKeys = {
|
||||
'test-cases-feature': true,
|
||||
} as unknown as AppFeatureKeys;
|
||||
const appFeatureKeys = ['test-cases-feature'] as unknown as AppFeatureKeys;
|
||||
|
||||
const appFeatures = new AppFeatures(
|
||||
{} as unknown as Logger,
|
||||
|
@ -105,6 +115,7 @@ describe('AppFeatures', () => {
|
|||
expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({
|
||||
...CASES_BASE_CONFIG,
|
||||
...CASES_APP_FEATURE_CONFIG['test-cases-feature'],
|
||||
subFeatures: [{ baz: 'baz' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,25 +12,32 @@ import type { AppFeatureKibanaConfig, AppFeaturesConfig } from './types';
|
|||
import {
|
||||
getSecurityAppFeaturesConfig,
|
||||
getSecurityBaseKibanaFeature,
|
||||
getSecurityBaseKibanaSubFeatureIds,
|
||||
} from './security_kibana_features';
|
||||
import {
|
||||
getCasesBaseKibanaFeature,
|
||||
getCasesAppFeaturesConfig,
|
||||
getCasesBaseKibanaSubFeatureIds,
|
||||
} from './security_cases_kibana_features';
|
||||
import { AppFeaturesConfigMerger } from './app_features_config_merger';
|
||||
|
||||
type AppFeaturesMap = Map<AppFeatureKey, boolean>;
|
||||
import { casesSubFeaturesMap } from './security_cases_kibana_sub_features';
|
||||
import { securitySubFeaturesMap } from './security_kibana_sub_features';
|
||||
|
||||
export class AppFeatures {
|
||||
private merger: AppFeaturesConfigMerger;
|
||||
private appFeatures?: AppFeaturesMap;
|
||||
private securityFeatureConfigMerger: AppFeaturesConfigMerger;
|
||||
private casesFeatureConfigMerger: AppFeaturesConfigMerger;
|
||||
private appFeatures?: Set<AppFeatureKey>;
|
||||
private featuresSetup?: FeaturesPluginSetup;
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly experimentalFeatures: ExperimentalFeatures
|
||||
) {
|
||||
this.merger = new AppFeaturesConfigMerger(this.logger);
|
||||
this.securityFeatureConfigMerger = new AppFeaturesConfigMerger(
|
||||
this.logger,
|
||||
securitySubFeaturesMap
|
||||
);
|
||||
this.casesFeatureConfigMerger = new AppFeaturesConfigMerger(this.logger, casesSubFeaturesMap);
|
||||
}
|
||||
|
||||
public init(featuresSetup: FeaturesPluginSetup) {
|
||||
|
@ -41,7 +48,7 @@ export class AppFeatures {
|
|||
if (this.appFeatures) {
|
||||
throw new Error('AppFeatures has already been initialized');
|
||||
}
|
||||
this.appFeatures = new Map(Object.entries(appFeatureKeys) as Array<[AppFeatureKey, boolean]>);
|
||||
this.appFeatures = new Set(appFeatureKeys);
|
||||
this.registerEnabledKibanaFeatures();
|
||||
}
|
||||
|
||||
|
@ -49,7 +56,7 @@ export class AppFeatures {
|
|||
if (!this.appFeatures) {
|
||||
throw new Error('AppFeatures has not been initialized');
|
||||
}
|
||||
return this.appFeatures.get(appFeatureKey) ?? false;
|
||||
return this.appFeatures.has(appFeatureKey);
|
||||
}
|
||||
|
||||
private registerEnabledKibanaFeatures() {
|
||||
|
@ -59,25 +66,31 @@ export class AppFeatures {
|
|||
);
|
||||
}
|
||||
// register main security Kibana features
|
||||
const securityBaseKibanaFeature = getSecurityBaseKibanaFeature(this.experimentalFeatures);
|
||||
const securityBaseKibanaFeature = getSecurityBaseKibanaFeature();
|
||||
const securityBaseKibanaSubFeatureIds = getSecurityBaseKibanaSubFeatureIds(
|
||||
this.experimentalFeatures
|
||||
);
|
||||
const enabledSecurityAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
|
||||
getSecurityAppFeaturesConfig()
|
||||
);
|
||||
this.featuresSetup.registerKibanaFeature(
|
||||
this.merger.mergeAppFeatureConfigs(
|
||||
this.securityFeatureConfigMerger.mergeAppFeatureConfigs(
|
||||
securityBaseKibanaFeature,
|
||||
securityBaseKibanaSubFeatureIds,
|
||||
enabledSecurityAppFeaturesConfigs
|
||||
)
|
||||
);
|
||||
|
||||
// register security cases Kibana features
|
||||
const securityCasesBaseKibanaFeature = getCasesBaseKibanaFeature();
|
||||
const securityCasesBaseKibanaSubFeatureIds = getCasesBaseKibanaSubFeatureIds();
|
||||
const enabledCasesAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
|
||||
getCasesAppFeaturesConfig()
|
||||
);
|
||||
this.featuresSetup.registerKibanaFeature(
|
||||
this.merger.mergeAppFeatureConfigs(
|
||||
this.casesFeatureConfigMerger.mergeAppFeatureConfigs(
|
||||
securityCasesBaseKibanaFeature,
|
||||
securityCasesBaseKibanaSubFeatureIds,
|
||||
enabledCasesAppFeaturesConfigs
|
||||
)
|
||||
);
|
||||
|
|
|
@ -9,59 +9,190 @@ import { loggingSystemMock } from '@kbn/core/server/mocks';
|
|||
import { AppFeaturesConfigMerger } from './app_features_config_merger';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { AppFeatureKibanaConfig } from './types';
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
|
||||
const category = {
|
||||
id: 'security',
|
||||
label: 'Security app category',
|
||||
};
|
||||
|
||||
const baseKibanaFeature: KibanaFeatureConfig = {
|
||||
id: 'FEATURE_ID',
|
||||
name: 'Base Feature',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['api-read', 'api-write'],
|
||||
app: ['FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['write', 'read'],
|
||||
},
|
||||
read: {
|
||||
api: ['api-read'],
|
||||
app: ['FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['read'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const subFeature1: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
name: 'subFeature1',
|
||||
description: 'Perform subFeature1 actions.',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: ['api-subFeature1'],
|
||||
id: 'sub-feature-1_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['subFeature1'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const subFeature2: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
name: 'subFeature2',
|
||||
description: 'Perform subFeature2 actions.',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: ['api-readSubFeature2', 'api-writeSubFeature2'],
|
||||
id: 'sub-feature-2_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readSubFeature2', 'writeSubFeature2'],
|
||||
},
|
||||
{
|
||||
api: ['api-readSubFeature2'],
|
||||
id: 'sub-feature-2_read',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readSubFeature2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const subFeature3: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
name: 'subFeature3',
|
||||
description: 'Perform subFeature3 actions.',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: ['api-readSubFeature3', 'api-writeSubFeature3'],
|
||||
id: 'sub-feature-3_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['readSubFeature3', 'writeSubFeature3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Defines the order of the Security Cases sub features
|
||||
export const subFeaturesMap = Object.freeze(
|
||||
new Map<string, SubFeatureConfig>([
|
||||
['subFeature1', subFeature1],
|
||||
['subFeature2', subFeature2],
|
||||
['subFeature3', subFeature3],
|
||||
])
|
||||
);
|
||||
|
||||
const mockLogger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
describe('AppFeaturesConfigMerger', () => {
|
||||
// We don't need to update this test when cases config change
|
||||
// It mocks simplified versions of cases config
|
||||
it('merges a mocked version of cases config', () => {
|
||||
const merger = new AppFeaturesConfigMerger(mockLogger);
|
||||
const merger = new AppFeaturesConfigMerger(mockLogger, subFeaturesMap);
|
||||
|
||||
const category = {
|
||||
id: 'security',
|
||||
label: 'Security app category',
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const securityCasesBaseKibanaFeature: KibanaFeatureConfig = {
|
||||
id: 'CASES_FEATURE_ID',
|
||||
name: 'Cases',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
api: [],
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
describe('main privileges', () => {
|
||||
it('should merge enabled main privileges into base config', () => {
|
||||
const enabledAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['api-write', 'api-extra-all'],
|
||||
ui: ['extra-all'],
|
||||
cases: {
|
||||
create: ['APP_ID'],
|
||||
read: ['APP_ID'],
|
||||
update: ['APP_ID'],
|
||||
push: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['someSavedObjectType'],
|
||||
read: ['someSavedObjectType'],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: ['api-extra-read'],
|
||||
ui: ['extra-read'],
|
||||
cases: {
|
||||
read: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['someSavedObjectType'],
|
||||
},
|
||||
},
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
read: {
|
||||
api: [],
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
];
|
||||
|
||||
const enabledCasesAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
cases: ['APP_ID'],
|
||||
const merged = merger.mergeAppFeatureConfigs(
|
||||
baseKibanaFeature,
|
||||
[],
|
||||
enabledAppFeaturesConfigs
|
||||
);
|
||||
|
||||
expect(merged).toEqual({
|
||||
...baseKibanaFeature,
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['casesApiTags.all'],
|
||||
ui: ['casesCapabilities.all'],
|
||||
api: ['api-read', 'api-write', 'api-extra-all'],
|
||||
app: ['FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
ui: ['write', 'read', 'extra-all'],
|
||||
cases: {
|
||||
create: ['APP_ID'],
|
||||
read: ['APP_ID'],
|
||||
|
@ -69,69 +200,215 @@ describe('AppFeaturesConfigMerger', () => {
|
|||
push: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['filesSavedObjectTypes'],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
all: ['someSavedObjectType'],
|
||||
read: ['someSavedObjectType'],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: ['casesApiTags.read'],
|
||||
ui: ['casesCapabilities.read'],
|
||||
api: ['api-read', 'api-extra-read'],
|
||||
app: ['FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
ui: ['read', 'extra-read'],
|
||||
cases: {
|
||||
read: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
read: ['someSavedObjectType'],
|
||||
},
|
||||
},
|
||||
},
|
||||
subFeatures: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subFeatureIds', () => {
|
||||
it('adds base subFeatures in the correct order', () => {
|
||||
const baseKibanaSubFeatureIds = ['subFeature2', 'subFeature3', 'subFeature1'];
|
||||
|
||||
const merged = merger.mergeAppFeatureConfigs(baseKibanaFeature, baseKibanaSubFeatureIds, []);
|
||||
expect(merged.subFeatures).toEqual([subFeature1, subFeature2, subFeature3]);
|
||||
});
|
||||
|
||||
it('should merge enabled subFeatures into base config in the correct order', () => {
|
||||
const enabledAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
subFeatureIds: ['subFeature3', 'subFeature1'],
|
||||
},
|
||||
];
|
||||
|
||||
const merged = merger.mergeAppFeatureConfigs(
|
||||
baseKibanaFeature,
|
||||
['subFeature2'],
|
||||
enabledAppFeaturesConfigs
|
||||
);
|
||||
|
||||
expect(merged).toEqual({
|
||||
...baseKibanaFeature,
|
||||
subFeatures: [subFeature1, subFeature2, subFeature3],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subFeaturePrivileges', () => {
|
||||
it('should merge enabled subFeatures with extra subFeaturePrivileges into base config in the correct order', () => {
|
||||
const enabledAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
subFeaturesPrivileges: [
|
||||
{
|
||||
id: 'sub-feature-1_all',
|
||||
api: ['api-subFeature1-extra1', 'api-subFeature1-extra2'],
|
||||
ui: ['subFeature1-extra1', 'subFeature1-extra2'],
|
||||
},
|
||||
{
|
||||
id: 'sub-feature-2_read',
|
||||
api: ['api-readSubFeature2-extra1', 'api-readSubFeature2-extra2'],
|
||||
ui: ['readSubFeature2-extra1', 'readSubFeature2-extra2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
subFeatureIds: ['subFeature3', 'subFeature1'],
|
||||
},
|
||||
];
|
||||
|
||||
const merged = merger.mergeAppFeatureConfigs(
|
||||
baseKibanaFeature,
|
||||
['subFeature2'],
|
||||
enabledAppFeaturesConfigs
|
||||
);
|
||||
expect(merged).toEqual({
|
||||
...baseKibanaFeature,
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Delete',
|
||||
...subFeature1,
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
...subFeature1.privilegeGroups[0],
|
||||
privileges: [
|
||||
{
|
||||
api: ['casesApiTags.delete'],
|
||||
id: 'cases_delete',
|
||||
name: 'Delete cases and comments',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['filesSavedObjectTypes'],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
},
|
||||
cases: {
|
||||
delete: ['APP_ID'],
|
||||
},
|
||||
ui: ['casesCapabilities.delete'],
|
||||
...subFeature1.privilegeGroups[0].privileges[0],
|
||||
api: ['api-subFeature1', 'api-subFeature1-extra1', 'api-subFeature1-extra2'],
|
||||
ui: ['subFeature1', 'subFeature1-extra1', 'subFeature1-extra2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...subFeature2,
|
||||
privilegeGroups: [
|
||||
{
|
||||
...subFeature2.privilegeGroups[0],
|
||||
privileges: [
|
||||
subFeature2.privilegeGroups[0].privileges[0],
|
||||
{
|
||||
...subFeature2.privilegeGroups[0].privileges[1],
|
||||
api: [
|
||||
'api-readSubFeature2',
|
||||
'api-readSubFeature2-extra1',
|
||||
'api-readSubFeature2-extra2',
|
||||
],
|
||||
ui: ['readSubFeature2', 'readSubFeature2-extra1', 'readSubFeature2-extra2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
subFeature3,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn if there are subFeaturesPrivileges for a subFeature id that is not found', () => {
|
||||
const subFeaturesPrivilegesId = 'sub-feature-1_all';
|
||||
const enabledAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
subFeaturesPrivileges: [
|
||||
{
|
||||
id: subFeaturesPrivilegesId,
|
||||
api: ['api-subFeature1-extra1', 'api-subFeature1-extra2'],
|
||||
ui: ['subFeature1-extra1', 'subFeature1-extra2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const merged = merger.mergeAppFeatureConfigs(
|
||||
baseKibanaFeature,
|
||||
['subFeature2', 'subFeature3'],
|
||||
enabledAppFeaturesConfigs
|
||||
);
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
`Trying to merge subFeaturesPrivileges ${subFeaturesPrivilegesId} but the subFeature privilege was not found`
|
||||
);
|
||||
expect(merged).toEqual({ ...baseKibanaFeature, subFeatures: [subFeature2, subFeature3] });
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge everything at the same time', () => {
|
||||
const enabledAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['api-write', 'api-extra-all'],
|
||||
ui: ['extra-all'],
|
||||
cases: {
|
||||
create: ['APP_ID'],
|
||||
read: ['APP_ID'],
|
||||
update: ['APP_ID'],
|
||||
push: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['someSavedObjectType'],
|
||||
read: ['someSavedObjectType'],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: ['api-extra-read'],
|
||||
ui: ['extra-read'],
|
||||
cases: {
|
||||
read: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['someSavedObjectType'],
|
||||
},
|
||||
},
|
||||
},
|
||||
subFeatureIds: ['subFeature3', 'subFeature1'],
|
||||
subFeaturesPrivileges: [
|
||||
{
|
||||
id: 'sub-feature-1_all',
|
||||
api: ['api-subFeature1-extra1', 'api-subFeature1-extra2'],
|
||||
ui: ['subFeature1-extra1', 'subFeature1-extra2'],
|
||||
},
|
||||
{
|
||||
id: 'sub-feature-2_all',
|
||||
api: ['api-writeSubFeature2-extra1', 'api-writeSubFeature2-extra2'],
|
||||
ui: ['writeSubFeature2-extra1', 'writeSubFeature2-extra2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const baseKibanaSubFeatureIds = ['subFeature2'];
|
||||
|
||||
const merged = merger.mergeAppFeatureConfigs(
|
||||
securityCasesBaseKibanaFeature,
|
||||
enabledCasesAppFeaturesConfigs
|
||||
baseKibanaFeature,
|
||||
baseKibanaSubFeatureIds,
|
||||
enabledAppFeaturesConfigs
|
||||
);
|
||||
|
||||
expect(merged).toEqual({
|
||||
id: 'CASES_FEATURE_ID',
|
||||
name: 'Cases',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
cases: ['APP_ID'],
|
||||
...baseKibanaFeature,
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['casesApiTags.all'],
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
api: ['api-read', 'api-write', 'api-extra-all'],
|
||||
app: ['FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
ui: ['write', 'read', 'extra-all'],
|
||||
cases: {
|
||||
create: ['APP_ID'],
|
||||
read: ['APP_ID'],
|
||||
|
@ -139,241 +416,67 @@ describe('AppFeaturesConfigMerger', () => {
|
|||
push: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['filesSavedObjectTypes'],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
all: ['someSavedObjectType'],
|
||||
read: ['someSavedObjectType'],
|
||||
},
|
||||
ui: ['casesCapabilities.all'],
|
||||
},
|
||||
read: {
|
||||
api: ['casesApiTags.read'],
|
||||
app: ['CASES_FEATURE_ID', 'kibana'],
|
||||
api: ['api-read', 'api-extra-read'],
|
||||
app: ['FEATURE_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
ui: ['read', 'extra-read'],
|
||||
cases: {
|
||||
read: ['APP_ID'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
read: ['someSavedObjectType'],
|
||||
},
|
||||
ui: ['casesCapabilities.read'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Delete',
|
||||
...subFeature1,
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
...subFeature1.privilegeGroups[0],
|
||||
privileges: [
|
||||
{
|
||||
api: ['casesApiTags.delete'],
|
||||
id: 'cases_delete',
|
||||
name: 'Delete cases and comments',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['filesSavedObjectTypes'],
|
||||
read: ['filesSavedObjectTypes'],
|
||||
},
|
||||
cases: {
|
||||
delete: ['APP_ID'],
|
||||
},
|
||||
ui: ['casesCapabilities.delete'],
|
||||
...subFeature1.privilegeGroups[0].privileges[0],
|
||||
api: ['api-subFeature1', 'api-subFeature1-extra1', 'api-subFeature1-extra2'],
|
||||
ui: ['subFeature1', 'subFeature1-extra1', 'subFeature1-extra2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('merges a mocked version of security basic config', () => {
|
||||
const merger = new AppFeaturesConfigMerger(mockLogger);
|
||||
|
||||
const category = {
|
||||
id: 'security',
|
||||
label: 'Security app category',
|
||||
};
|
||||
|
||||
const securityCasesBaseKibanaFeature: KibanaFeatureConfig = {
|
||||
id: 'SERVER_APP_ID',
|
||||
name: 'Security',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: ['THRESHOLD_RULE_TYPE_ID', 'NEW_TERMS_RULE_TYPE_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
api: ['APP_ID', 'cloud-security-posture-read'],
|
||||
savedObject: {
|
||||
all: ['alert', 'CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE'],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
alert: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'crud'],
|
||||
},
|
||||
read: {
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
api: ['APP_ID', 'lists-read', 'rac', 'cloud-security-posture-read'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE'],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
alert: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: 'All Spaces is required for Host Isolation access.',
|
||||
name: 'Host Isolation',
|
||||
description: 'Perform the "isolate" and "release" response actions.',
|
||||
...subFeature2,
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
...subFeature2.privilegeGroups[0],
|
||||
privileges: [
|
||||
{
|
||||
api: [`APP_ID-writeHostIsolation`],
|
||||
id: 'host_isolation_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeHostIsolation'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enabledCasesAppFeaturesConfigs: AppFeatureKibanaConfig[] = [
|
||||
{
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['rules_load_prepackaged'],
|
||||
ui: ['rules_load_prepackaged'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const merged = merger.mergeAppFeatureConfigs(
|
||||
securityCasesBaseKibanaFeature,
|
||||
enabledCasesAppFeaturesConfigs
|
||||
);
|
||||
|
||||
expect(merged).toEqual({
|
||||
id: 'SERVER_APP_ID',
|
||||
name: 'Security',
|
||||
order: 1100,
|
||||
category,
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: ['THRESHOLD_RULE_TYPE_ID', 'NEW_TERMS_RULE_TYPE_ID'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
api: ['APP_ID', 'cloud-security-posture-read', 'rules_load_prepackaged'],
|
||||
savedObject: {
|
||||
all: ['alert', 'CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE'],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
alert: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'crud', 'rules_load_prepackaged'],
|
||||
},
|
||||
read: {
|
||||
app: ['APP_ID', 'CLOUD_POSTURE_APP_ID', 'kibana'],
|
||||
catalogue: ['APP_ID'],
|
||||
api: ['APP_ID', 'lists-read', 'rac', 'cloud-security-posture-read'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE'],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
alert: {
|
||||
all: ['SECURITY_RULE_TYPES'],
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: 'All Spaces is required for Host Isolation access.',
|
||||
name: 'Host Isolation',
|
||||
description: 'Perform the "isolate" and "release" response actions.',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
api: [`APP_ID-writeHostIsolation`],
|
||||
id: 'host_isolation_all',
|
||||
includeIn: 'none',
|
||||
name: 'All',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['writeHostIsolation'],
|
||||
...subFeature2.privilegeGroups[0].privileges[0],
|
||||
api: [
|
||||
'api-readSubFeature2',
|
||||
'api-writeSubFeature2',
|
||||
'api-writeSubFeature2-extra1',
|
||||
'api-writeSubFeature2-extra2',
|
||||
],
|
||||
ui: [
|
||||
'readSubFeature2',
|
||||
'writeSubFeature2',
|
||||
'writeSubFeature2-extra1',
|
||||
'writeSubFeature2-extra2',
|
||||
],
|
||||
},
|
||||
subFeature2.privilegeGroups[0].privileges[1],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
subFeature3,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,11 +7,18 @@
|
|||
|
||||
import { cloneDeep, mergeWith, isArray, uniq } from 'lodash';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import type { AppFeatureKibanaConfig, SubFeaturesPrivileges } from './types';
|
||||
import type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import type {
|
||||
AppFeatureKibanaConfig,
|
||||
BaseKibanaFeatureConfig,
|
||||
SubFeaturesPrivileges,
|
||||
} from './types';
|
||||
|
||||
export class AppFeaturesConfigMerger {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
export class AppFeaturesConfigMerger<T extends string = string> {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly subFeaturesMap: Map<T, SubFeatureConfig>
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Merges `appFeaturesConfigs` into `kibanaFeatureConfig`.
|
||||
|
@ -20,25 +27,45 @@ export class AppFeaturesConfigMerger {
|
|||
* @returns mergedKibanaFeatureConfig the merged KibanaFeatureConfig
|
||||
* */
|
||||
public mergeAppFeatureConfigs(
|
||||
kibanaFeatureConfig: KibanaFeatureConfig,
|
||||
kibanaFeatureConfig: BaseKibanaFeatureConfig,
|
||||
kibanaSubFeatureIds: T[],
|
||||
appFeaturesConfigs: AppFeatureKibanaConfig[]
|
||||
): KibanaFeatureConfig {
|
||||
const mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig);
|
||||
const mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig) as KibanaFeatureConfig;
|
||||
const subFeaturesPrivilegesToMerge: SubFeaturesPrivileges[] = [];
|
||||
const enabledSubFeaturesIndexed = Object.fromEntries(
|
||||
kibanaSubFeatureIds.map((id) => [id, true])
|
||||
);
|
||||
|
||||
appFeaturesConfigs.forEach((appFeatureConfig) => {
|
||||
const { subFeaturesPrivileges, ...appFeatureConfigToMerge } = cloneDeep(appFeatureConfig);
|
||||
const { subFeaturesPrivileges, subFeatureIds, ...appFeatureConfigToMerge } =
|
||||
cloneDeep(appFeatureConfig);
|
||||
|
||||
subFeatureIds?.forEach((subFeatureId) => {
|
||||
enabledSubFeaturesIndexed[subFeatureId] = true;
|
||||
});
|
||||
|
||||
if (subFeaturesPrivileges) {
|
||||
subFeaturesPrivilegesToMerge.push(...subFeaturesPrivileges);
|
||||
}
|
||||
mergeWith(mergedKibanaFeatureConfig, appFeatureConfigToMerge, featureConfigMerger);
|
||||
});
|
||||
|
||||
// add subFeaturePrivileges at the end to make sure all enabled subFeatures are merged
|
||||
subFeaturesPrivilegesToMerge.forEach((subFeaturesPrivileges) => {
|
||||
this.mergeSubFeaturesPrivileges(mergedKibanaFeatureConfig.subFeatures, subFeaturesPrivileges);
|
||||
// generate sub features configs from enabled sub feature ids, preserving map order
|
||||
const mergedKibanaSubFeatures: SubFeatureConfig[] = [];
|
||||
this.subFeaturesMap.forEach((subFeature, id) => {
|
||||
if (enabledSubFeaturesIndexed[id]) {
|
||||
mergedKibanaSubFeatures.push(cloneDeep(subFeature));
|
||||
}
|
||||
});
|
||||
|
||||
// add extra privileges to subFeatures
|
||||
subFeaturesPrivilegesToMerge.forEach((subFeaturesPrivileges) => {
|
||||
this.mergeSubFeaturesPrivileges(mergedKibanaSubFeatures, subFeaturesPrivileges);
|
||||
});
|
||||
|
||||
mergedKibanaFeatureConfig.subFeatures = mergedKibanaSubFeatures;
|
||||
|
||||
return mergedKibanaFeatureConfig;
|
||||
}
|
||||
|
||||
|
@ -49,13 +76,9 @@ export class AppFeaturesConfigMerger {
|
|||
* @returns void
|
||||
* */
|
||||
private mergeSubFeaturesPrivileges(
|
||||
subFeatures: KibanaFeatureConfig['subFeatures'],
|
||||
subFeatures: SubFeatureConfig[],
|
||||
subFeaturesPrivileges: SubFeaturesPrivileges
|
||||
): void {
|
||||
if (!subFeatures) {
|
||||
this.logger.warn('Trying to merge subFeaturesPrivileges but no subFeatures found');
|
||||
return;
|
||||
}
|
||||
const merged = subFeatures.find(({ privilegeGroups }) =>
|
||||
privilegeGroups.some(({ privileges }) => {
|
||||
const subFeaturePrivilegeToUpdate = privileges.find(
|
||||
|
|
|
@ -8,21 +8,20 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import {
|
||||
createUICapabilities as createCasesUICapabilities,
|
||||
getApiTags as getCasesApiTags,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
import type { AppFeaturesCasesConfig } from './types';
|
||||
import type { AppFeaturesCasesConfig, BaseKibanaFeatureConfig } from './types';
|
||||
import { APP_ID, CASES_FEATURE_ID } from '../../../common/constants';
|
||||
import { casesSubFeatureDelete } from './security_cases_kibana_sub_features';
|
||||
import { CasesSubFeatureId } from './security_cases_kibana_sub_features';
|
||||
import { AppFeatureCasesKey } from '../../../common/types/app_features';
|
||||
|
||||
const casesCapabilities = createCasesUICapabilities();
|
||||
const casesApiTags = getCasesApiTags(APP_ID);
|
||||
|
||||
export const getCasesBaseKibanaFeature = (): KibanaFeatureConfig => ({
|
||||
export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
|
||||
id: CASES_FEATURE_ID,
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
|
@ -63,11 +62,23 @@ export const getCasesBaseKibanaFeature = (): KibanaFeatureConfig => ({
|
|||
ui: casesCapabilities.read,
|
||||
},
|
||||
},
|
||||
subFeatures: [casesSubFeatureDelete],
|
||||
});
|
||||
|
||||
// It maps the AppFeatures keys to Kibana privileges
|
||||
export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [
|
||||
CasesSubFeatureId.deleteCases,
|
||||
];
|
||||
|
||||
/**
|
||||
* Maps the AppFeatures keys to Kibana privileges that will be merged
|
||||
* into the base privileges config for the Security Cases app.
|
||||
*
|
||||
* Privileges can be added in different ways:
|
||||
* - `privileges`: the privileges that will be added directly into the main Security Cases feature.
|
||||
* - `subFeatureIds`: the ids of the sub-features that will be added into the Cases subFeatures entry.
|
||||
* - `subFeaturesPrivileges`: the privileges that will be added into the existing Cases subFeature with the privilege `id` specified.
|
||||
*/
|
||||
export const getCasesAppFeaturesConfig = (): AppFeaturesCasesConfig => ({
|
||||
// TODO Add cases connector configuration
|
||||
[AppFeatureCasesKey.casesConnectors]: {},
|
||||
[AppFeatureCasesKey.casesConnectors]: {
|
||||
// TODO: Add cases connector configuration privileges
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import { APP_ID } from '../../../common/constants';
|
|||
const casesCapabilities = createCasesUICapabilities();
|
||||
const casesApiTags = getCasesApiTags(APP_ID);
|
||||
|
||||
export const casesSubFeatureDelete: SubFeatureConfig = {
|
||||
const deleteCasesSubFeature: SubFeatureConfig = {
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
|
@ -45,3 +45,14 @@ export const casesSubFeatureDelete: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
export enum CasesSubFeatureId {
|
||||
deleteCases = 'deleteCasesSubFeature',
|
||||
}
|
||||
|
||||
// Defines all the ordered Security Cases subFeatures available
|
||||
export const casesSubFeaturesMap = Object.freeze(
|
||||
new Map<CasesSubFeatureId, SubFeatureConfig>([
|
||||
[CasesSubFeatureId.deleteCases, deleteCasesSubFeature],
|
||||
])
|
||||
);
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
|
||||
import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants';
|
||||
|
@ -23,20 +22,8 @@ import {
|
|||
import { APP_ID, LEGACY_NOTIFICATIONS_ID, SERVER_APP_ID } from '../../../common/constants';
|
||||
import { savedObjectTypes } from '../../saved_objects';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import {
|
||||
blocklistSubFeature,
|
||||
endpointListSubFeature,
|
||||
eventFiltersSubFeature,
|
||||
executeActionSubFeature,
|
||||
fileOperationsSubFeature,
|
||||
hostIsolationExceptionsSubFeature,
|
||||
hostIsolationSubFeature,
|
||||
policyManagementSubFeature,
|
||||
processOperationsSubFeature,
|
||||
responseActionsHistorySubFeature,
|
||||
trustedApplicationsSubFeature,
|
||||
} from './security_kibana_sub_features';
|
||||
import type { AppFeaturesSecurityConfig } from './types';
|
||||
import { SecuritySubFeatureId } from './security_kibana_sub_features';
|
||||
import type { AppFeaturesSecurityConfig, BaseKibanaFeatureConfig } from './types';
|
||||
import { AppFeatureSecurityKey } from '../../../common/types/app_features';
|
||||
|
||||
// Same as the plugin id defined by Cloud Security Posture
|
||||
|
@ -55,9 +42,7 @@ const SECURITY_RULE_TYPES = [
|
|||
NEW_TERMS_RULE_TYPE_ID,
|
||||
];
|
||||
|
||||
export const getSecurityBaseKibanaFeature = (
|
||||
experimentalFeatures: ExperimentalFeatures
|
||||
): KibanaFeatureConfig => ({
|
||||
export const getSecurityBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
|
||||
id: SERVER_APP_ID,
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle', {
|
||||
defaultMessage: 'Security',
|
||||
|
@ -135,42 +120,51 @@ export const getSecurityBaseKibanaFeature = (
|
|||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: getSubFeatures(experimentalFeatures),
|
||||
});
|
||||
|
||||
function getSubFeatures(experimentalFeatures: ExperimentalFeatures) {
|
||||
const subFeatures: SubFeatureConfig[] = [];
|
||||
export const getSecurityBaseKibanaSubFeatureIds = (
|
||||
experimentalFeatures: ExperimentalFeatures
|
||||
): SecuritySubFeatureId[] => {
|
||||
const subFeatureIds: SecuritySubFeatureId[] = [];
|
||||
|
||||
if (experimentalFeatures.endpointRbacEnabled) {
|
||||
subFeatures.push(
|
||||
endpointListSubFeature,
|
||||
trustedApplicationsSubFeature,
|
||||
hostIsolationExceptionsSubFeature,
|
||||
blocklistSubFeature,
|
||||
eventFiltersSubFeature,
|
||||
policyManagementSubFeature
|
||||
subFeatureIds.push(
|
||||
SecuritySubFeatureId.endpointList,
|
||||
SecuritySubFeatureId.trustedApplications,
|
||||
SecuritySubFeatureId.hostIsolationExceptions,
|
||||
SecuritySubFeatureId.blocklist,
|
||||
SecuritySubFeatureId.eventFilters,
|
||||
SecuritySubFeatureId.policyManagement
|
||||
);
|
||||
}
|
||||
|
||||
if (experimentalFeatures.endpointRbacEnabled || experimentalFeatures.endpointRbacV1Enabled) {
|
||||
subFeatures.push(
|
||||
responseActionsHistorySubFeature,
|
||||
hostIsolationSubFeature,
|
||||
processOperationsSubFeature
|
||||
subFeatureIds.push(
|
||||
SecuritySubFeatureId.responseActionsHistory,
|
||||
SecuritySubFeatureId.hostIsolation,
|
||||
SecuritySubFeatureId.processOperations
|
||||
);
|
||||
}
|
||||
if (experimentalFeatures.responseActionGetFileEnabled) {
|
||||
subFeatures.push(fileOperationsSubFeature);
|
||||
subFeatureIds.push(SecuritySubFeatureId.fileOperations);
|
||||
}
|
||||
// planned for 8.8
|
||||
if (experimentalFeatures.responseActionExecuteEnabled) {
|
||||
subFeatures.push(executeActionSubFeature);
|
||||
subFeatureIds.push(SecuritySubFeatureId.executeAction);
|
||||
}
|
||||
|
||||
return subFeatures;
|
||||
}
|
||||
return subFeatureIds;
|
||||
};
|
||||
|
||||
// maps the AppFeatures keys to Kibana privileges
|
||||
/**
|
||||
* Maps the AppFeatures keys to Kibana privileges that will be merged
|
||||
* into the base privileges config for the Security app.
|
||||
*
|
||||
* Privileges can be added in different ways:
|
||||
* - `privileges`: the privileges that will be added directly into the main Security feature.
|
||||
* - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry.
|
||||
* - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified.
|
||||
*/
|
||||
export const getSecurityAppFeaturesConfig = (): AppFeaturesSecurityConfig => {
|
||||
return {
|
||||
[AppFeatureSecurityKey.advancedInsights]: {
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { SubFeatureConfig } from '@kbn/features-plugin/common';
|
|||
import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants';
|
||||
import { APP_ID } from '../../../common';
|
||||
|
||||
export const endpointListSubFeature: SubFeatureConfig = {
|
||||
const endpointListSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.endpointList.privilegesTooltip',
|
||||
|
@ -58,7 +58,7 @@ export const endpointListSubFeature: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
export const trustedApplicationsSubFeature: SubFeatureConfig = {
|
||||
const trustedApplicationsSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.privilegesTooltip',
|
||||
|
@ -112,7 +112,7 @@ export const trustedApplicationsSubFeature: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
export const hostIsolationExceptionsSubFeature: SubFeatureConfig = {
|
||||
const hostIsolationExceptionsSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip',
|
||||
|
@ -169,7 +169,7 @@ export const hostIsolationExceptionsSubFeature: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
export const blocklistSubFeature: SubFeatureConfig = {
|
||||
const blocklistSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.blockList.privilegesTooltip',
|
||||
|
@ -223,7 +223,7 @@ export const blocklistSubFeature: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
export const eventFiltersSubFeature: SubFeatureConfig = {
|
||||
const eventFiltersSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.privilegesTooltip',
|
||||
|
@ -277,7 +277,7 @@ export const eventFiltersSubFeature: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
export const policyManagementSubFeature: SubFeatureConfig = {
|
||||
const policyManagementSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.privilegesTooltip',
|
||||
|
@ -326,7 +326,7 @@ export const policyManagementSubFeature: SubFeatureConfig = {
|
|||
],
|
||||
};
|
||||
|
||||
export const responseActionsHistorySubFeature: SubFeatureConfig = {
|
||||
const responseActionsHistorySubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip',
|
||||
|
@ -376,7 +376,7 @@ export const responseActionsHistorySubFeature: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
export const hostIsolationSubFeature: SubFeatureConfig = {
|
||||
const hostIsolationSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.privilegesTooltip',
|
||||
|
@ -411,7 +411,7 @@ export const hostIsolationSubFeature: SubFeatureConfig = {
|
|||
],
|
||||
};
|
||||
|
||||
export const processOperationsSubFeature: SubFeatureConfig = {
|
||||
const processOperationsSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.processOperations.privilegesTooltip',
|
||||
|
@ -447,7 +447,7 @@ export const processOperationsSubFeature: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
export const fileOperationsSubFeature: SubFeatureConfig = {
|
||||
const fileOperationsSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.fileOperations.privilegesTooltip',
|
||||
|
@ -486,7 +486,7 @@ export const fileOperationsSubFeature: SubFeatureConfig = {
|
|||
|
||||
// execute operations are not available in 8.7,
|
||||
// but will be available in 8.8
|
||||
export const executeActionSubFeature: SubFeatureConfig = {
|
||||
const executeActionSubFeature: SubFeatureConfig = {
|
||||
requireAllSpaces: true,
|
||||
privilegesTooltip: i18n.translate(
|
||||
'xpack.securitySolution.featureRegistry.subFeatures.executeOperations.privilegesTooltip',
|
||||
|
@ -523,3 +523,34 @@ export const executeActionSubFeature: SubFeatureConfig = {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
export enum SecuritySubFeatureId {
|
||||
endpointList = 'endpointListSubFeature',
|
||||
trustedApplications = 'trustedApplicationsSubFeature',
|
||||
hostIsolationExceptions = 'hostIsolationExceptionsSubFeature',
|
||||
blocklist = 'blocklistSubFeature',
|
||||
eventFilters = 'eventFiltersSubFeature',
|
||||
policyManagement = 'policyManagementSubFeature',
|
||||
responseActionsHistory = 'responseActionsHistorySubFeature',
|
||||
hostIsolation = 'hostIsolationSubFeature',
|
||||
processOperations = 'processOperationsSubFeature',
|
||||
fileOperations = 'fileOperationsSubFeature',
|
||||
executeAction = 'executeActionSubFeature',
|
||||
}
|
||||
|
||||
// Defines all the ordered Security subFeatures available
|
||||
export const securitySubFeaturesMap = Object.freeze(
|
||||
new Map<SecuritySubFeatureId, SubFeatureConfig>([
|
||||
[SecuritySubFeatureId.endpointList, endpointListSubFeature],
|
||||
[SecuritySubFeatureId.trustedApplications, trustedApplicationsSubFeature],
|
||||
[SecuritySubFeatureId.hostIsolationExceptions, hostIsolationExceptionsSubFeature],
|
||||
[SecuritySubFeatureId.blocklist, blocklistSubFeature],
|
||||
[SecuritySubFeatureId.eventFilters, eventFiltersSubFeature],
|
||||
[SecuritySubFeatureId.policyManagement, policyManagementSubFeature],
|
||||
[SecuritySubFeatureId.responseActionsHistory, responseActionsHistorySubFeature],
|
||||
[SecuritySubFeatureId.hostIsolation, hostIsolationSubFeature],
|
||||
[SecuritySubFeatureId.processOperations, processOperationsSubFeature],
|
||||
[SecuritySubFeatureId.fileOperations, fileOperationsSubFeature],
|
||||
[SecuritySubFeatureId.executeAction, executeActionSubFeature],
|
||||
])
|
||||
);
|
||||
|
|
|
@ -10,10 +10,22 @@ import type { AppFeatureKey } from '../../../common';
|
|||
import type { AppFeatureSecurityKey, AppFeatureCasesKey } from '../../../common/types/app_features';
|
||||
import type { RecursivePartial } from '../../../common/utility_types';
|
||||
|
||||
export type BaseKibanaFeatureConfig = Omit<KibanaFeatureConfig, 'subFeatures'>;
|
||||
export type SubFeaturesPrivileges = RecursivePartial<SubFeaturePrivilegeConfig>;
|
||||
export type AppFeatureKibanaConfig = RecursivePartial<KibanaFeatureConfig> & {
|
||||
subFeaturesPrivileges?: SubFeaturesPrivileges[];
|
||||
};
|
||||
export type AppFeaturesConfig = Record<AppFeatureKey, AppFeatureKibanaConfig>;
|
||||
export type AppFeaturesSecurityConfig = Record<AppFeatureSecurityKey, AppFeatureKibanaConfig>;
|
||||
export type AppFeaturesCasesConfig = Record<AppFeatureCasesKey, AppFeatureKibanaConfig>;
|
||||
export type AppFeatureKibanaConfig<T extends string = string> =
|
||||
RecursivePartial<BaseKibanaFeatureConfig> & {
|
||||
subFeatureIds?: T[];
|
||||
subFeaturesPrivileges?: SubFeaturesPrivileges[];
|
||||
};
|
||||
export type AppFeaturesConfig<T extends string = string> = Record<
|
||||
AppFeatureKey,
|
||||
AppFeatureKibanaConfig<T>
|
||||
>;
|
||||
export type AppFeaturesSecurityConfig<T extends string = string> = Record<
|
||||
AppFeatureSecurityKey,
|
||||
AppFeatureKibanaConfig<T>
|
||||
>;
|
||||
export type AppFeaturesCasesConfig<T extends string = string> = Record<
|
||||
AppFeatureCasesKey,
|
||||
AppFeatureKibanaConfig<T>
|
||||
>;
|
||||
|
|
|
@ -7,13 +7,23 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const productLineId = schema.oneOf([
|
||||
schema.literal('securityEssentials'),
|
||||
schema.literal('securityComplete'),
|
||||
export const productLine = schema.oneOf([
|
||||
schema.literal('security'),
|
||||
schema.literal('endpoint'),
|
||||
schema.literal('cloud'),
|
||||
]);
|
||||
export type SecurityProductLineId = TypeOf<typeof productLineId>;
|
||||
export type SecurityProductLine = TypeOf<typeof productLine>;
|
||||
|
||||
export const productLineIds = schema.arrayOf<SecurityProductLineId>(productLineId, {
|
||||
defaultValue: ['securityEssentials'],
|
||||
export const productTier = schema.oneOf([schema.literal('essentials'), schema.literal('complete')]);
|
||||
export type SecurityProductTier = TypeOf<typeof productTier>;
|
||||
|
||||
export const productType = schema.object({
|
||||
product_line: productLine,
|
||||
product_tier: productTier,
|
||||
});
|
||||
export type SecurityProductLineIds = TypeOf<typeof productLineIds>;
|
||||
export type SecurityProductType = TypeOf<typeof productType>;
|
||||
|
||||
export const productTypes = schema.arrayOf<SecurityProductType>(productType, {
|
||||
defaultValue: [],
|
||||
});
|
||||
export type SecurityProductTypes = TypeOf<typeof productTypes>;
|
||||
|
|
|
@ -5,10 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import type { SecurityProductLineId } from '../config';
|
||||
import { AppFeatureKey, type AppFeatureKeys } from '@kbn/security-solution-plugin/common';
|
||||
import type { SecurityProductLine, SecurityProductTier } from '../config';
|
||||
|
||||
export const PLI_APP_FEATURES: Record<SecurityProductLineId, readonly AppFeatureKey[]> = {
|
||||
securityEssentials: [],
|
||||
securityComplete: [AppFeatureKey.advancedInsights, AppFeatureKey.casesConnectors],
|
||||
type PliAppFeatures = Readonly<
|
||||
Record<SecurityProductLine, Readonly<Record<SecurityProductTier, Readonly<AppFeatureKeys>>>>
|
||||
>;
|
||||
|
||||
export const PLI_APP_FEATURES: PliAppFeatures = {
|
||||
security: {
|
||||
essentials: [],
|
||||
complete: [AppFeatureKey.advancedInsights, AppFeatureKey.casesConnectors],
|
||||
},
|
||||
endpoint: {
|
||||
essentials: [],
|
||||
complete: [],
|
||||
},
|
||||
cloud: {
|
||||
essentials: [],
|
||||
complete: [],
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -8,25 +8,65 @@ import { getProductAppFeatures } from './pli_features';
|
|||
import * as pliConfig from './pli_config';
|
||||
|
||||
describe('getProductAppFeatures', () => {
|
||||
it('returns the union of all enabled PLIs features', () => {
|
||||
it('should return the essentials PLIs features', () => {
|
||||
// @ts-ignore reassigning readonly value for testing
|
||||
pliConfig.PLI_APP_FEATURES = { securityEssentials: ['foo'], securityComplete: ['baz'] };
|
||||
pliConfig.PLI_APP_FEATURES = {
|
||||
security: {
|
||||
essentials: ['foo'],
|
||||
complete: ['baz'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(getProductAppFeatures(['securityEssentials', 'securityComplete'])).toEqual({
|
||||
foo: true,
|
||||
baz: true,
|
||||
});
|
||||
const appFeatureKeys = getProductAppFeatures([
|
||||
{ product_line: 'security', product_tier: 'essentials' },
|
||||
]);
|
||||
|
||||
expect(appFeatureKeys).toEqual(['foo']);
|
||||
});
|
||||
|
||||
it('returns a single PLI when only one is enabled', () => {
|
||||
it('should return the complete PLIs features, which includes essentials', () => {
|
||||
// @ts-ignore reassigning readonly value for testing
|
||||
pliConfig.PLI_APP_FEATURES = { securityEssentials: [], securityComplete: ['foo'] };
|
||||
expect(getProductAppFeatures(['securityEssentials', 'securityComplete'])).toEqual({
|
||||
foo: true,
|
||||
});
|
||||
pliConfig.PLI_APP_FEATURES = {
|
||||
security: {
|
||||
essentials: ['foo'],
|
||||
complete: ['baz'],
|
||||
},
|
||||
};
|
||||
|
||||
const appFeatureKeys = getProductAppFeatures([
|
||||
{ product_line: 'security', product_tier: 'complete' },
|
||||
]);
|
||||
|
||||
expect(appFeatureKeys).toEqual(['foo', 'baz']);
|
||||
});
|
||||
|
||||
it('should return the union of all enabled PLIs features without duplicates', () => {
|
||||
// @ts-ignore reassigning readonly value for testing
|
||||
pliConfig.PLI_APP_FEATURES = {
|
||||
security: {
|
||||
essentials: ['foo'],
|
||||
complete: ['baz'],
|
||||
},
|
||||
endpoint: {
|
||||
essentials: ['bar', 'repeated'],
|
||||
complete: ['qux', 'quux'],
|
||||
},
|
||||
cloud: {
|
||||
essentials: ['corge', 'garply', 'repeated'],
|
||||
complete: ['grault'],
|
||||
},
|
||||
};
|
||||
|
||||
const appFeatureKeys = getProductAppFeatures([
|
||||
{ product_line: 'security', product_tier: 'essentials' },
|
||||
{ product_line: 'endpoint', product_tier: 'complete' },
|
||||
{ product_line: 'cloud', product_tier: 'essentials' },
|
||||
]);
|
||||
|
||||
expect(appFeatureKeys).toEqual(['foo', 'bar', 'repeated', 'qux', 'quux', 'corge', 'garply']);
|
||||
});
|
||||
|
||||
it('returns an empty object if no PLIs are enabled', () => {
|
||||
expect(getProductAppFeatures([])).toEqual({});
|
||||
expect(getProductAppFeatures([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,19 +6,24 @@
|
|||
*/
|
||||
|
||||
import type { AppFeatureKeys } from '@kbn/security-solution-plugin/common';
|
||||
import { SecurityProductLineId } from '../config';
|
||||
import uniq from 'lodash/fp/uniq';
|
||||
import type { SecurityProductTypes } from '../config';
|
||||
import { PLI_APP_FEATURES } from './pli_config';
|
||||
|
||||
/**
|
||||
* Returns the U (union) of all enabled PLIs features in a single object.
|
||||
* Returns the U (union) of all PLIs from the enabled productTypes in a single array.
|
||||
*/
|
||||
export const getProductAppFeatures = (productLineIds: SecurityProductLineId[]): AppFeatureKeys =>
|
||||
productLineIds.reduce<AppFeatureKeys>((appFeatures, productLineId) => {
|
||||
const productAppFeatures = PLI_APP_FEATURES[productLineId];
|
||||
|
||||
productAppFeatures.forEach((featureName) => {
|
||||
appFeatures[featureName] = true;
|
||||
});
|
||||
|
||||
return appFeatures;
|
||||
}, {} as AppFeatureKeys);
|
||||
export const getProductAppFeatures = (productTypes: SecurityProductTypes): AppFeatureKeys => {
|
||||
const appFeatureKeys = productTypes.reduce<AppFeatureKeys>(
|
||||
(appFeatures, { product_line: line, product_tier: tier }) => {
|
||||
if (tier === 'complete') {
|
||||
// Adding all "essentials" PLIs when tier is "complete"
|
||||
appFeatures.push(...PLI_APP_FEATURES[line].essentials);
|
||||
}
|
||||
appFeatures.push(...PLI_APP_FEATURES[line][tier]);
|
||||
return appFeatures;
|
||||
},
|
||||
[]
|
||||
);
|
||||
return uniq(appFeatureKeys);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import { PLI_APP_FEATURES } from '../../../../common/pli/pli_config';
|
||||
|
||||
export const useProductTypeByPLI = (requiredPLI: AppFeatureKey): string | null => {
|
||||
if (PLI_APP_FEATURES.security.essentials.includes(requiredPLI)) {
|
||||
return 'Security Essentials';
|
||||
}
|
||||
if (PLI_APP_FEATURES.security.complete.includes(requiredPLI)) {
|
||||
return 'Security Complete';
|
||||
}
|
||||
if (PLI_APP_FEATURES.endpoint.essentials.includes(requiredPLI)) {
|
||||
return 'Endpoint Essentials';
|
||||
}
|
||||
if (PLI_APP_FEATURES.endpoint.complete.includes(requiredPLI)) {
|
||||
return 'Endpoint Complete';
|
||||
}
|
||||
if (PLI_APP_FEATURES.cloud.essentials.includes(requiredPLI)) {
|
||||
return 'Cloud Essentials';
|
||||
}
|
||||
if (PLI_APP_FEATURES.cloud.complete.includes(requiredPLI)) {
|
||||
return 'Cloud Complete';
|
||||
}
|
||||
return null;
|
||||
};
|
|
@ -7,13 +7,12 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
|
||||
import { SecurityProductLineIds } from '../../../../common/config';
|
||||
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import { useProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
|
||||
export const GenericUpsellingPage: React.FC<{ projectPLIs: SecurityProductLineIds }> = React.memo(
|
||||
({ projectPLIs }) => {
|
||||
const upsellingPLI = projectPLIs.includes('securityComplete')
|
||||
? 'Security Complete'
|
||||
: 'Security Essential';
|
||||
export const GenericUpsellingPage: React.FC<{ requiredPLI: AppFeatureKey }> = React.memo(
|
||||
({ requiredPLI }) => {
|
||||
const productTypeRequired = useProductTypeByPLI(requiredPLI);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
|
@ -21,7 +20,7 @@ export const GenericUpsellingPage: React.FC<{ projectPLIs: SecurityProductLineId
|
|||
title={<>This is a testing component for a Serverless upselling prompt.</>}
|
||||
body={
|
||||
<>
|
||||
Get <EuiLink href="#">{upsellingPLI}</EuiLink> to enable this feature
|
||||
Get <EuiLink href="#">{productTypeRequired}</EuiLink> to enable this feature
|
||||
<br />
|
||||
<br />
|
||||
<iframe
|
||||
|
|
|
@ -7,13 +7,12 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
|
||||
import { SecurityProductLineIds } from '../../../../common/config';
|
||||
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import { useProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
|
||||
export const GenericUpsellingSection: React.FC<{ projectPLIs: SecurityProductLineIds }> =
|
||||
React.memo(({ projectPLIs }) => {
|
||||
const upsellingPLI = projectPLIs.includes('securityComplete')
|
||||
? 'Security Complete'
|
||||
: 'Security Essential';
|
||||
export const GenericUpsellingSection: React.FC<{ requiredPLI: AppFeatureKey }> = React.memo(
|
||||
({ requiredPLI }) => {
|
||||
const productTypeRequired = useProductTypeByPLI(requiredPLI);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
|
@ -21,7 +20,7 @@ export const GenericUpsellingSection: React.FC<{ projectPLIs: SecurityProductLin
|
|||
title={<>This is a testing component for a Serverless upselling prompt.</>}
|
||||
body={
|
||||
<>
|
||||
Get <EuiLink href="#">{upsellingPLI}</EuiLink> to enable this feature
|
||||
Get <EuiLink href="#">{productTypeRequired}</EuiLink> to enable this feature
|
||||
<br />
|
||||
<br />
|
||||
<iframe
|
||||
|
@ -37,7 +36,8 @@ export const GenericUpsellingSection: React.FC<{ projectPLIs: SecurityProductLin
|
|||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { GenericUpsellingSection as default };
|
||||
|
|
|
@ -6,16 +6,24 @@
|
|||
*/
|
||||
|
||||
import { UpsellingService } from '@kbn/security-solution-plugin/public';
|
||||
import { registerUpsellings } from './register_upsellings';
|
||||
import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-plugin/common';
|
||||
import { registerUpsellings, upsellingPages, upsellingSections } from './register_upsellings';
|
||||
import type { SecurityProductTypes } from '../../../common/config';
|
||||
|
||||
const mockGetProductAppFeatures = jest.fn();
|
||||
jest.mock('../../../common/pli/pli_features', () => ({
|
||||
getProductAppFeatures: () => mockGetProductAppFeatures(),
|
||||
}));
|
||||
|
||||
const allProductTypes: SecurityProductTypes = [
|
||||
{ product_line: 'security', product_tier: 'complete' },
|
||||
{ product_line: 'endpoint', product_tier: 'complete' },
|
||||
{ product_line: 'cloud', product_tier: 'complete' },
|
||||
];
|
||||
|
||||
describe('registerUpsellings', () => {
|
||||
it('registers entity analytics upsellings page and section when PLIs features are disabled', () => {
|
||||
mockGetProductAppFeatures.mockReturnValue({}); // return empty object to simulate no features enabled
|
||||
it('should not register anything when all PLIs features are enabled', () => {
|
||||
mockGetProductAppFeatures.mockReturnValue(ALL_APP_FEATURE_KEYS);
|
||||
|
||||
const registerPages = jest.fn();
|
||||
const registerSections = jest.fn();
|
||||
|
@ -24,20 +32,37 @@ describe('registerUpsellings', () => {
|
|||
registerSections,
|
||||
} as unknown as UpsellingService;
|
||||
|
||||
registerUpsellings(upselling, ['securityEssentials', 'securityComplete']);
|
||||
registerUpsellings(upselling, allProductTypes);
|
||||
|
||||
expect(registerPages).toHaveBeenCalledTimes(1);
|
||||
expect(registerPages).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
['entity-analytics']: expect.any(Function),
|
||||
})
|
||||
);
|
||||
expect(registerPages).toHaveBeenCalledWith({});
|
||||
|
||||
expect(registerSections).toHaveBeenCalledTimes(1);
|
||||
expect(registerSections).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entity_analytics_panel: expect.any(Function),
|
||||
})
|
||||
expect(registerSections).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('should register all upsellings pages and sections when PLIs features are disabled', () => {
|
||||
mockGetProductAppFeatures.mockReturnValue([]);
|
||||
|
||||
const registerPages = jest.fn();
|
||||
const registerSections = jest.fn();
|
||||
const upselling = {
|
||||
registerPages,
|
||||
registerSections,
|
||||
} as unknown as UpsellingService;
|
||||
|
||||
registerUpsellings(upselling, allProductTypes);
|
||||
|
||||
const expectedPagesObject = Object.fromEntries(
|
||||
upsellingPages.map(({ pageName }) => [pageName, expect.any(Function)])
|
||||
);
|
||||
expect(registerPages).toHaveBeenCalledTimes(1);
|
||||
expect(registerPages).toHaveBeenCalledWith(expectedPagesObject);
|
||||
|
||||
const expectedSectionsObject = Object.fromEntries(
|
||||
upsellingSections.map(({ id }) => [id, expect.any(Function)])
|
||||
);
|
||||
expect(registerSections).toHaveBeenCalledTimes(1);
|
||||
expect(registerSections).toHaveBeenCalledWith(expectedSectionsObject);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,15 +12,14 @@ import type {
|
|||
SectionUpsellings,
|
||||
UpsellingSectionId,
|
||||
} from '@kbn/security-solution-plugin/public';
|
||||
|
||||
import type { SecurityProductLineIds } from '../../../common/config';
|
||||
import type { SecurityProductTypes } from '../../../common/config';
|
||||
import { getProductAppFeatures } from '../../../common/pli/pli_features';
|
||||
|
||||
const GenericUpsellingPageLazy = lazy(() => import('./pages/generic_upselling_page'));
|
||||
const GenericUpsellingSectionLazy = lazy(() => import('./pages/generic_upselling_section'));
|
||||
|
||||
interface UpsellingsConfig {
|
||||
feature: AppFeatureKey;
|
||||
pli: AppFeatureKey;
|
||||
component: React.ComponentType;
|
||||
}
|
||||
|
||||
|
@ -29,13 +28,13 @@ type UpsellingSections = Array<UpsellingsConfig & { id: UpsellingSectionId }>;
|
|||
|
||||
export const registerUpsellings = (
|
||||
upselling: UpsellingService,
|
||||
projectPLIs: SecurityProductLineIds
|
||||
productTypes: SecurityProductTypes
|
||||
) => {
|
||||
const PLIsFeatures = getProductAppFeatures(projectPLIs);
|
||||
const enabledPLIsSet = new Set(getProductAppFeatures(productTypes));
|
||||
|
||||
const upsellingPages = getUpsellingPages(projectPLIs).reduce<PageUpsellings>(
|
||||
(pageUpsellings, { pageName, feature, component }) => {
|
||||
if (!PLIsFeatures[feature]) {
|
||||
const upsellingPagesToRegister = upsellingPages.reduce<PageUpsellings>(
|
||||
(pageUpsellings, { pageName, pli, component }) => {
|
||||
if (!enabledPLIsSet.has(pli)) {
|
||||
pageUpsellings[pageName] = component;
|
||||
}
|
||||
return pageUpsellings;
|
||||
|
@ -43,9 +42,9 @@ export const registerUpsellings = (
|
|||
{}
|
||||
);
|
||||
|
||||
const upsellingSections = getUpsellingSections(projectPLIs).reduce<SectionUpsellings>(
|
||||
(sectionUpsellings, { id, feature, component }) => {
|
||||
if (!PLIsFeatures[feature]) {
|
||||
const upsellingSectionsToRegister = upsellingSections.reduce<SectionUpsellings>(
|
||||
(sectionUpsellings, { id, pli, component }) => {
|
||||
if (!enabledPLIsSet.has(pli)) {
|
||||
sectionUpsellings[id] = component;
|
||||
}
|
||||
return sectionUpsellings;
|
||||
|
@ -53,23 +52,24 @@ export const registerUpsellings = (
|
|||
{}
|
||||
);
|
||||
|
||||
upselling.registerPages(upsellingPages);
|
||||
upselling.registerSections(upsellingSections);
|
||||
upselling.registerPages(upsellingPagesToRegister);
|
||||
upselling.registerSections(upsellingSectionsToRegister);
|
||||
};
|
||||
|
||||
// Upselling configuration for pages and sections components
|
||||
const getUpsellingPages = (projectPLIs: SecurityProductLineIds): UpsellingPages => [
|
||||
// Upsellings for entire pages, linked to a SecurityPageName
|
||||
export const upsellingPages: UpsellingPages = [
|
||||
{
|
||||
pageName: SecurityPageName.entityAnalytics,
|
||||
feature: AppFeatureKey.advancedInsights,
|
||||
component: () => <GenericUpsellingPageLazy projectPLIs={projectPLIs} />,
|
||||
pli: AppFeatureKey.advancedInsights,
|
||||
component: () => <GenericUpsellingPageLazy requiredPLI={AppFeatureKey.advancedInsights} />,
|
||||
},
|
||||
];
|
||||
|
||||
const getUpsellingSections = (projectPLIs: SecurityProductLineIds): UpsellingSections => [
|
||||
// Upsellings for sections, linked by arbitrary ids
|
||||
export const upsellingSections: UpsellingSections = [
|
||||
{
|
||||
id: 'entity_analytics_panel',
|
||||
feature: AppFeatureKey.advancedInsights,
|
||||
component: () => <GenericUpsellingSectionLazy projectPLIs={projectPLIs} />,
|
||||
pli: AppFeatureKey.advancedInsights,
|
||||
component: () => <GenericUpsellingSectionLazy requiredPLI={AppFeatureKey.advancedInsights} />,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -36,7 +36,7 @@ export class ServerlessSecurityPlugin
|
|||
_core: CoreSetup,
|
||||
setupDeps: ServerlessSecurityPluginSetupDependencies
|
||||
): ServerlessSecurityPluginSetup {
|
||||
registerUpsellings(setupDeps.securitySolution.upselling, this.config.productLineIds);
|
||||
registerUpsellings(setupDeps.securitySolution.upselling, this.config.productTypes);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
|||
PluginStart as SecuritySolutionPluginStart,
|
||||
} from '@kbn/security-solution-plugin/public';
|
||||
import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import type { SecurityProductLineIds } from '../common/config';
|
||||
import type { SecurityProductTypes } from '../common/config';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ServerlessSecurityPluginSetup {}
|
||||
|
@ -32,5 +32,5 @@ export interface ServerlessSecurityPluginStartDependencies {
|
|||
}
|
||||
|
||||
export interface ServerlessSecurityPublicConfig {
|
||||
productLineIds: SecurityProductLineIds;
|
||||
productTypes: SecurityProductTypes;
|
||||
}
|
||||
|
|
|
@ -7,17 +7,17 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
import { productLineIds } from '../common/config';
|
||||
import { productTypes } from '../common/config';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
productLineIds,
|
||||
productTypes,
|
||||
});
|
||||
export type ServerlessSecurityConfig = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<ServerlessSecurityConfig> = {
|
||||
exposeToBrowser: {
|
||||
productLineIds: true,
|
||||
productTypes: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
|
|
@ -38,9 +38,7 @@ export class ServerlessSecurityPlugin
|
|||
const shouldRegister = pluginsSetup.essSecurity == null;
|
||||
|
||||
if (shouldRegister) {
|
||||
pluginsSetup.securitySolution.setAppFeatures(
|
||||
getProductAppFeatures(this.config.productLineIds)
|
||||
);
|
||||
pluginsSetup.securitySolution.setAppFeatures(getProductAppFeatures(this.config.productTypes));
|
||||
}
|
||||
|
||||
return {};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue