Skip to content

Commit deeb9fe

Browse files
fix(security, features): do not expose UI capabilities of the deprecated features (elastic#198656)
## Summary This PR ensures that we don’t expose UI capabilities for deprecated features since they’re unnecessary, and the code should rely on the UI capabilities of the replacement features instead. Additionally, this PR transforms the `disabledFeatures` property of Space objects returned from our programmatic and HTTP APIs to replace any deprecated feature IDs with the IDs of their replacement features, ensuring that feature visibility toggles work for deprecated features as well. ## How to test 1. Run Kibana FTR server with the following config (registers test deprecated features): ```shell node scripts/functional_tests_server.js --config x-pack/test/security_api_integration/features.config.ts ``` 2. Once server is up and running create Space with the `case_1_feature_a` **deprecated** feature disabled: ```shell curl 'http://localhost:5620/api/spaces/space' -u elastic:changeme \ -X POST -H 'Content-Type: application/json' -H 'kbn-version: 9.0.0' \ --data-raw '{"name":"space-alpha","id":"space-alpha","initials":"s","color":"#D6BF57","disabledFeatures":["case_1_feature_a"],"imageUrl":""}' ``` 3. Log in to Kibana and [navigate to a Space `space-alpha`](http://localhost:5620/app/management/kibana/spaces/edit/space-alpha) you've just created. Observe that deprecated `Case #1 feature A` (`case_1_feature_a`) isn't displayed, and instead you should see that replaces deprecated one - `Case #1 feature B` (`case_1_feature_b`): ![Screen Shot 2024-11-01 at 17 40 59](https://github.com/user-attachments/assets/5b91e71c-7d46-4ff1-bf73-d148622e8ec4) Co-authored-by: Elastic Machine <[email protected]>
1 parent de46e7f commit deeb9fe

File tree

15 files changed

+301
-42
lines changed

15 files changed

+301
-42
lines changed

x-pack/plugins/features/server/mocks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ const createSetup = (): jest.Mocked<FeaturesPluginSetup> => {
2525

2626
const createStart = (): jest.Mocked<FeaturesPluginStart> => {
2727
return {
28-
getKibanaFeatures: jest.fn(),
29-
getElasticsearchFeatures: jest.fn(),
28+
getKibanaFeatures: jest.fn().mockReturnValue([]),
29+
getElasticsearchFeatures: jest.fn().mockReturnValue([]),
3030
};
3131
};
3232

x-pack/plugins/features/server/plugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ export class FeaturesPlugin
138138
this.featureRegistry.validateFeatures();
139139

140140
this.capabilities = uiCapabilitiesForFeatures(
141-
this.featureRegistry.getAllKibanaFeatures(),
141+
// Don't expose capabilities of the deprecated features.
142+
this.featureRegistry.getAllKibanaFeatures({ omitDeprecated: true }),
142143
this.featureRegistry.getAllElasticsearchFeatures()
143144
);
144145

x-pack/plugins/security/server/authorization/authorization_service.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,9 @@ describe('#start', () => {
145145
customBranding: mockCoreSetup.customBranding,
146146
});
147147

148-
const featuresStart = featuresPluginMock.createStart();
149-
featuresStart.getKibanaFeatures.mockReturnValue([]);
150-
151148
authorizationService.start({
152149
clusterClient: mockClusterClient,
153-
features: featuresStart,
150+
features: featuresPluginMock.createStart(),
154151
online$: statusSubject.asObservable(),
155152
});
156153

@@ -217,12 +214,9 @@ it('#stop unsubscribes from license and ES updates.', async () => {
217214
customBranding: mockCoreSetup.customBranding,
218215
});
219216

220-
const featuresStart = featuresPluginMock.createStart();
221-
featuresStart.getKibanaFeatures.mockReturnValue([]);
222-
223217
authorizationService.start({
224218
clusterClient: mockClusterClient,
225-
features: featuresStart,
219+
features: featuresPluginMock.createStart(),
226220
online$: statusSubject.asObservable(),
227221
});
228222

x-pack/plugins/security/server/plugin.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,8 @@ describe('Security Plugin', () => {
6464

6565
mockCoreStart = coreMock.createStart();
6666

67-
const mockFeaturesStart = featuresPluginMock.createStart();
68-
mockFeaturesStart.getKibanaFeatures.mockReturnValue([]);
6967
mockStartDependencies = {
70-
features: mockFeaturesStart,
68+
features: featuresPluginMock.createStart(),
7169
licensing: licensingMock.createStart(),
7270
taskManager: taskManagerMock.createStart(),
7371
};

x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const features = [
6666
category: { id: 'securitySolution' },
6767
},
6868
{
69-
// feature 4 intentionally delcares the same items as feature 3
69+
// feature 4 intentionally declares the same items as feature 3
7070
id: 'feature_4',
7171
name: 'Feature 4',
7272
app: ['feature3', 'feature3_app'],
@@ -87,6 +87,32 @@ const features = [
8787
},
8888
category: { id: 'observability' },
8989
},
90+
{
91+
deprecated: { notice: 'It was a mistake.' },
92+
id: 'deprecated_feature',
93+
name: 'Deprecated Feature',
94+
// Expose the same `app` and `catalogue` entries as `feature_2` to make sure they are disabled
95+
// when `feature_2` is disabled even if the deprecated feature isn't explicitly disabled.
96+
app: ['feature2'],
97+
catalogue: ['feature2Entry'],
98+
category: { id: 'deprecated', label: 'deprecated' },
99+
privileges: {
100+
all: {
101+
savedObject: { all: [], read: [] },
102+
ui: ['ui_deprecated_all'],
103+
app: ['feature2'],
104+
catalogue: ['feature2Entry'],
105+
replacedBy: [{ feature: 'feature_2', privileges: ['all'] }],
106+
},
107+
read: {
108+
savedObject: { all: [], read: [] },
109+
ui: ['ui_deprecated_read'],
110+
app: ['feature2'],
111+
catalogue: ['feature2Entry'],
112+
replacedBy: [{ feature: 'feature_2', privileges: ['all'] }],
113+
},
114+
},
115+
},
90116
] as unknown as KibanaFeature[];
91117

92118
const buildCapabilities = () =>

x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function toggleDisabledFeatures(
7272
(acc, feature) => {
7373
if (disabledFeatureKeys.includes(feature.id)) {
7474
acc.disabledFeatures.push(feature);
75-
} else {
75+
} else if (!feature.deprecated) {
7676
acc.enabledFeatures.push(feature);
7777
}
7878
return acc;

x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const enabledFeaturesPerSolution: Record<SolutionId, string[]> = {
3939
* This function takes the current space's disabled features and the space solution and returns
4040
* the updated array of disabled features.
4141
*
42+
* @param features The list of all Kibana registered features.
4243
* @param spaceDisabledFeatures The current space's disabled features
4344
* @param spaceSolution The current space's solution (es, oblt, security or classic)
4445
* @returns The updated array of disabled features

x-pack/plugins/spaces/server/routes/api/external/post.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,9 @@ describe('Spaces Public API', () => {
5656
basePath: httpService.basePath,
5757
});
5858

59-
const featuresPluginMockStart = featuresPluginMock.createStart();
60-
61-
featuresPluginMockStart.getKibanaFeatures.mockReturnValue([]);
62-
6359
const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract());
6460

65-
const clientServiceStart = clientService.start(coreStart, featuresPluginMockStart);
61+
const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart());
6662

6763
const spacesServiceStart = service.start({
6864
basePath: coreStart.http.basePath,

x-pack/plugins/spaces/server/routes/api/external/put.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,9 @@ describe('PUT /api/spaces/space', () => {
5656
basePath: httpService.basePath,
5757
});
5858

59-
const featuresPluginMockStart = featuresPluginMock.createStart();
60-
61-
featuresPluginMockStart.getKibanaFeatures.mockReturnValue([]);
62-
6359
const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract());
6460

65-
const clientServiceStart = clientService.start(coreStart, featuresPluginMockStart);
61+
const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart());
6662

6763
const spacesServiceStart = service.start({
6864
basePath: coreStart.http.basePath,

x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,37 @@ const features = [
5555
catalogue: ['feature3Entry'],
5656
category: { id: 'securitySolution' },
5757
},
58+
{
59+
deprecated: { notice: 'It was a mistake.' },
60+
id: 'feature_4_deprecated',
61+
name: 'Deprecated Feature',
62+
app: ['feature2', 'feature3'],
63+
catalogue: ['feature2Entry', 'feature3Entry'],
64+
category: { id: 'deprecated', label: 'deprecated' },
65+
scope: ['spaces', 'security'],
66+
privileges: {
67+
all: {
68+
savedObject: { all: [], read: [] },
69+
ui: [],
70+
app: ['feature2', 'feature3'],
71+
catalogue: ['feature2Entry', 'feature3Entry'],
72+
replacedBy: [
73+
{ feature: 'feature_2', privileges: ['all'] },
74+
{ feature: 'feature_3', privileges: ['all'] },
75+
],
76+
},
77+
read: {
78+
savedObject: { all: [], read: [] },
79+
ui: [],
80+
app: ['feature2', 'feature3'],
81+
catalogue: ['feature2Entry', 'feature3Entry'],
82+
replacedBy: [
83+
{ feature: 'feature_2', privileges: ['read'] },
84+
{ feature: 'feature_3', privileges: ['read'] },
85+
],
86+
},
87+
},
88+
},
5889
] as unknown as KibanaFeature[];
5990
const featuresStart = featuresPluginMock.createStart();
6091

@@ -103,6 +134,17 @@ describe('#getAll', () => {
103134
bar: 'baz-bar', // an extra attribute that will be ignored during conversion
104135
},
105136
},
137+
{
138+
// alpha only has deprecated disabled features
139+
id: 'alpha',
140+
type: 'space',
141+
references: [],
142+
attributes: {
143+
name: 'alpha-name',
144+
description: 'alpha-description',
145+
disabledFeatures: ['feature_1', 'feature_4_deprecated'],
146+
},
147+
},
106148
];
107149

108150
const expectedSpaces: Space[] = [
@@ -130,6 +172,12 @@ describe('#getAll', () => {
130172
description: 'baz-description',
131173
disabledFeatures: [],
132174
},
175+
{
176+
id: 'alpha',
177+
name: 'alpha-name',
178+
description: 'alpha-description',
179+
disabledFeatures: ['feature_1', 'feature_2', 'feature_3'],
180+
},
133181
];
134182

135183
test(`finds spaces using callWithRequestRepository`, async () => {

0 commit comments

Comments
 (0)