mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Cloud Security] Feature Flag Support for Cloud Security Posture Plugin (#205438)
## Summary Summarize your PR. If it involves visual changes include a screenshot or gif. ## Changes * Adds `enableExperimental` to server `configSchema` * Makes feature flags configurable via `xpack.cloudSecurityPosture.enableExperimental` in `kibana.dev.yml` * Implements `ExperimentFeatureService.get()` for accessing feature flags * Add passing `initliaterContext` to plugin in order to access our plugin config ## Benefits * Avoids circular dependency with Security Solution `useIsExperimentalFeatureEnabled` and prop drilling feature flags from Fleet plugin `PackagePolicyReplaceDefineStepExtensionComponentProps` * Provides server-side configuration support * Enables pre-release feature testing * Creates centralized feature flag management This allows controlled testing of new features before release through configuration rather than code changes. --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
55390001ad
commit
473eb721bc
8 changed files with 134 additions and 7 deletions
|
@ -15,7 +15,7 @@ pageLoadAssetSize:
|
|||
cloudExperiments: 109746
|
||||
cloudFullStory: 18493
|
||||
cloudLinks: 55984
|
||||
cloudSecurityPosture: 19109
|
||||
cloudSecurityPosture: 19270
|
||||
console: 46091
|
||||
contentManagement: 16254
|
||||
controls: 60000
|
||||
|
|
|
@ -250,6 +250,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cloud.performance_url (string?)',
|
||||
'xpack.cloud.users_and_roles_url (string?)',
|
||||
'xpack.cloud.projects_url (string?|never)',
|
||||
'xpack.cloudSecurityPosture.enableExperimental (array?)',
|
||||
// can't be used to infer urls or customer id from the outside
|
||||
'xpack.cloud.serverless.project_id (string?)',
|
||||
'xpack.cloud.serverless.project_name (string?)',
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface CSPUIConfigType {
|
||||
enableExperimental: string[];
|
||||
}
|
||||
|
||||
export type ExperimentalFeatures = { [K in keyof typeof allowedExperimentalValues]: boolean };
|
||||
|
||||
/**
|
||||
* A list of allowed values that can be used in `xpack.cloud_security_posture.enableExperimental`.
|
||||
* This object is then used to validate and parse the value entered.
|
||||
*/
|
||||
export const allowedExperimentalValues = Object.freeze({
|
||||
/**
|
||||
* Enables cloud Connectors for Cloud Security Posture
|
||||
*/
|
||||
cloudConnectorsEnabled: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<ExperimentalConfigKeys>;
|
||||
|
||||
/**
|
||||
* Parses the string value used in `xpack.cloud_security_posture.enableExperimental` kibana configuration,
|
||||
* which should be a string of values delimited by a comma (`,`)
|
||||
*
|
||||
* @param configValue
|
||||
* @throws SecuritySolutionInvalidExperimentalValue
|
||||
*/
|
||||
export const parseExperimentalConfigValue = (
|
||||
configValue: string[]
|
||||
): { features: ExperimentalFeatures; invalid: string[] } => {
|
||||
const enabledFeatures: Mutable<Partial<ExperimentalFeatures>> = {};
|
||||
const invalidKeys: string[] = [];
|
||||
|
||||
for (const value of configValue) {
|
||||
if (!allowedKeys.includes(value as keyof ExperimentalFeatures)) {
|
||||
invalidKeys.push(value);
|
||||
} else {
|
||||
enabledFeatures[value as keyof ExperimentalFeatures] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
features: {
|
||||
...allowedExperimentalValues,
|
||||
...enabledFeatures,
|
||||
},
|
||||
invalid: invalidKeys,
|
||||
};
|
||||
};
|
||||
|
||||
export const getExperimentalAllowedValues = (): string[] => [...allowedKeys];
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { ExperimentalFeatures } from '../../common/experimental_features';
|
||||
|
||||
export class ExperimentalFeaturesService {
|
||||
private static experimentalFeatures?: ExperimentalFeatures;
|
||||
|
||||
public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) {
|
||||
this.experimentalFeatures = experimentalFeatures;
|
||||
}
|
||||
|
||||
public static get(): ExperimentalFeatures {
|
||||
if (!this.experimentalFeatures) {
|
||||
this.throwUninitializedError();
|
||||
}
|
||||
|
||||
return this.experimentalFeatures;
|
||||
}
|
||||
|
||||
private static throwUninitializedError(): never {
|
||||
throw new Error(
|
||||
'Technical preview features services not initialized - are you trying to import this module from outside of the Security Solution app?'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { CspPlugin } from './plugin';
|
||||
export type { CspSecuritySolutionContext } from './types';
|
||||
export type { CloudSecurityPosturePageId } from './common/navigation/types';
|
||||
|
@ -12,4 +13,5 @@ export { getSecuritySolutionLink } from './common/navigation/security_solution_l
|
|||
|
||||
export type { CspClientPluginSetup, CspClientPluginStart } from './types';
|
||||
|
||||
export const plugin = () => new CspPlugin();
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
new CspPlugin(initializerContext);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
|
@ -16,6 +16,12 @@ import type { CspRouterProps } from './application/csp_router';
|
|||
import type { CspClientPluginSetup, CspClientPluginStart, CspClientPluginSetupDeps } from './types';
|
||||
import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants';
|
||||
import { SetupContext } from './application/setup_context';
|
||||
import {
|
||||
type CSPUIConfigType,
|
||||
type ExperimentalFeatures,
|
||||
parseExperimentalConfigValue,
|
||||
} from '../common/experimental_features';
|
||||
import { ExperimentalFeaturesService } from './common/experimental_features_service';
|
||||
|
||||
const LazyCspPolicyTemplateForm = lazy(
|
||||
() => import('./components/fleet_extensions/policy_template_form')
|
||||
|
@ -42,13 +48,22 @@ export class CspPlugin
|
|||
>
|
||||
{
|
||||
private isCloudEnabled?: boolean;
|
||||
private config: CSPUIConfigType;
|
||||
private experimentalFeatures: ExperimentalFeatures;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<CSPUIConfigType>();
|
||||
|
||||
this.experimentalFeatures = parseExperimentalConfigValue(
|
||||
this.config.enableExperimental || []
|
||||
)?.features;
|
||||
}
|
||||
|
||||
public setup(
|
||||
_core: CoreSetup<CspClientPluginStartDeps, CspClientPluginStart>,
|
||||
plugins: CspClientPluginSetupDeps
|
||||
): CspClientPluginSetup {
|
||||
this.isCloudEnabled = plugins.cloud.isCloudEnabled;
|
||||
|
||||
if (plugins.usageCollection) uiMetricService.setup(plugins.usageCollection);
|
||||
|
||||
// Return methods that should be available to other plugins
|
||||
|
@ -56,6 +71,7 @@ export class CspPlugin
|
|||
}
|
||||
|
||||
public start(core: CoreStart, plugins: CspClientPluginStartDeps): CspClientPluginStart {
|
||||
ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures });
|
||||
plugins.fleet.registerExtension({
|
||||
package: CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
|
||||
view: 'package-policy-replace-define-step',
|
||||
|
|
|
@ -18,10 +18,28 @@ const configSchema = schema.object({
|
|||
options: { defaultValue: schema.contextRef('serverless') },
|
||||
}),
|
||||
}),
|
||||
/**
|
||||
* For internal use. A list of string values (comma delimited) that will enable experimental
|
||||
* type of functionality that is not yet released. Valid values for this settings need to
|
||||
* be defined in:
|
||||
* `x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts`
|
||||
* under the `allowedExperimentalValues` object
|
||||
*
|
||||
* @example
|
||||
* xpack.cloudSecurityPosture.enableExperimental:
|
||||
* - newFeatureA
|
||||
* - newFeatureB
|
||||
*/
|
||||
enableExperimental: schema.arrayOf(schema.string(), {
|
||||
defaultValue: () => [],
|
||||
}),
|
||||
});
|
||||
|
||||
export type CloudSecurityPostureConfig = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<CloudSecurityPostureConfig> = {
|
||||
schema: configSchema,
|
||||
exposeToBrowser: {
|
||||
enableExperimental: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -63,7 +63,7 @@ describe('createBenchmarkScoreIndex', () => {
|
|||
it('should create index template the correct index patter, index name and default ingest pipeline but without lifecycle in serverless', async () => {
|
||||
await createBenchmarkScoreIndex(
|
||||
mockEsClient,
|
||||
{ serverless: { enabled: true }, enabled: true },
|
||||
{ serverless: { enabled: true }, enabled: true, enableExperimental: [] },
|
||||
logger
|
||||
);
|
||||
expect(mockEsClient.indices.putIndexTemplate).toHaveBeenCalledTimes(1);
|
||||
|
@ -87,7 +87,7 @@ describe('createBenchmarkScoreIndex', () => {
|
|||
|
||||
await createBenchmarkScoreIndex(
|
||||
mockEsClient,
|
||||
{ serverless: { enabled: true }, enabled: true },
|
||||
{ serverless: { enabled: true }, enabled: true, enableExperimental: [] },
|
||||
logger
|
||||
);
|
||||
expect(mockEsClient.indices.create).toHaveBeenCalledTimes(1);
|
||||
|
@ -102,7 +102,7 @@ describe('createBenchmarkScoreIndex', () => {
|
|||
|
||||
await createBenchmarkScoreIndex(
|
||||
mockEsClient,
|
||||
{ serverless: { enabled: true }, enabled: true },
|
||||
{ serverless: { enabled: true }, enabled: true, enableExperimental: [] },
|
||||
logger
|
||||
);
|
||||
expect(mockEsClient.indices.create).toHaveBeenCalledTimes(0);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue