[Security Solution] Allow disabling experimental features via config (#217363)

## Summary

This PR adds support for disabling experimental features using the
existing `xpack.securitySolution.enableExperimental` configuration.

This solves the problem of not being able to disable a feature by config
once the feature has been enabled by default.

### The Challenge 

When we start developing a feature under an experimental flag we always
follow the same steps:

1 - Create the experimental flag disabled by default + enable it via
config for testing
2 - Implement the feature
3 - Enable the experimental flag by default when we want to release the
feature.
4 - Deployments can disable the feature via config (as a safety
measure).
5 - Remove the experimental flag after some time.

We start by creating the flag disabled by default while we implement it.
In `experimental_features.ts`:
```ts
export const allowedExperimentalValues = Object.freeze({
  myFeatureEnabled: false,
  [...]
```
And enable it via config with:
```yml
xpack.securitySolution.enableExperimental:
  - myFeatureEnabled
```

Once the implementation is done and the experimental flag can be enabled
by default, we have to do a trick:
Since the `xpack.securitySolution.enableExperimental` config can only
turn flags to _true_, instead of setting `myFeatureEnabled: true`, what
we have to do is rename the flag to `myFeatureDisabled` and keep the
value as _false_:

```ts
export const allowedExperimentalValues = Object.freeze({
  myFeatureDisabled: false,
  [...]
```
Then we also need to do a code refactor to update all the places in the
code where the flag was checked: `if (myFeatureEnabled)` -> `if
(!myFeatureDisabled)`

This way, we have the option of disabling the feature via config (in
case something goes wrong):
```yml
xpack.securitySolution.enableExperimental:
  - myFeatureDisabled
```

### A solution

This PR introduces the possibility to turn a flag to _false_ using the
same `xpack.securitySolution.enableExperimental` config. This was
preferable to introducing a new config since this one is already
whitelisted in Cloud UI, can be easily overritten in deployments, and
also because people are used to it.

With these changes, the first two steps would be the same, with the
difference that we won't need to have the _Enabled_ or _Disabled_ word
at the end of the flag name. It could be just the feature name, in
`experimental_features.ts`:
```ts
export const allowedExperimentalValues = Object.freeze({
  myFeature: false,
  [...]
```

And when we need to enable the feature by default, we can just turn it
to `true`:
```ts
export const allowedExperimentalValues = Object.freeze({
  myFeature: true,
  [...]
```
No tedious refactor or confusing naming would be required. 

Then, in case we need to disable the feature in a production deployment
for some reason, we could just do this via config :
```yml
xpack.securitySolution.enableExperimental:
  - disable:myFeature
```

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2025-04-16 14:09:28 +02:00 committed by GitHub
parent b91da375a3
commit 937dbba41e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -276,9 +276,12 @@ type Mutable<T> = { -readonly [P in keyof T]: T[P] };
const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<ExperimentalConfigKeys>;
const disableExperimentalPrefix = 'disable:' as const;
/**
* Parses the string value used in `xpack.securitySolution.enableExperimental` kibana configuration,
* which should be a string of values delimited by a comma (`,`)
* which should be an array of strings corresponding to allowedExperimentalValues keys.
* Use the `disable:` prefix to disable a feature.
*
* @param configValue
* @throws SecuritySolutionInvalidExperimentalValue
@ -289,11 +292,15 @@ export const parseExperimentalConfigValue = (
const enabledFeatures: Mutable<Partial<ExperimentalFeatures>> = {};
const invalidKeys: string[] = [];
for (const value of configValue) {
for (let value of configValue) {
const isDisabled = value.startsWith(disableExperimentalPrefix);
if (isDisabled) {
value = value.replace(disableExperimentalPrefix, '');
}
if (!allowedKeys.includes(value as keyof ExperimentalFeatures)) {
invalidKeys.push(value);
} else {
enabledFeatures[value as keyof ExperimentalFeatures] = true;
enabledFeatures[value as keyof ExperimentalFeatures] = !isDisabled;
}
}