From 89ed7813c4a6a27dbd44eed656263ed1558bc78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 17 Sep 2024 17:52:28 +0100 Subject: [PATCH] [Spaces] Dynamically set the space disabled feature based on the space solution view (#191927) (cherry picked from commit 735380a02860dadbeab00105adf94c8c731d1fc0) # Conflicts: # x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts --- .../capabilities_switcher.test.ts | 82 +++++++++++++++++ .../capabilities/capabilities_switcher.ts | 7 +- .../on_post_auth_interceptor.ts | 14 ++- .../space_solution_disabled_features.test.ts | 89 +++++++++++++++++++ .../utils/space_solution_disabled_features.ts | 64 +++++++++++++ 5 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts create mode 100644 x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index b7bb839a752c6..31df41beae3cf 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -20,6 +20,8 @@ const features = [ id: 'feature_1', name: 'Feature 1', app: [], + category: { id: 'enterpriseSearch' }, + scope: ['spaces', 'security'], }, { id: 'feature_2', @@ -39,6 +41,7 @@ const features = [ }, }, }, + category: { id: 'observability' }, }, { id: 'feature_3', @@ -58,6 +61,7 @@ const features = [ }, }, }, + category: { id: 'securitySolution' }, }, { // feature 4 intentionally delcares the same items as feature 3 @@ -78,6 +82,7 @@ const features = [ }, }, }, + category: { id: 'observability' }, }, ] as unknown as KibanaFeature[]; @@ -317,4 +322,81 @@ describe('capabilitiesSwitcher', () => { expect(result).toEqual(expectedCapabilities); }); + + describe('when the space has a solution set', () => { + it('does toggles capabilities of the solutions different from the space one even when the space has no disabled features', async () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: [], + }; + + const capabilities = buildCapabilities(); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + + { + space.solution = 'es'; + + // It should disable observability and securitySolution features + // which correspond to feature_2 and feature_3 + const result = await switcher(request, capabilities, false); + + const expectedCapabilities = buildCapabilities(); + + expectedCapabilities.navLinks.feature2 = false; + expectedCapabilities.catalogue.feature2Entry = false; + expectedCapabilities.navLinks.feature3 = false; + expectedCapabilities.catalogue.feature3Entry = false; + expectedCapabilities.navLinks.feature3_app = false; + expectedCapabilities.management.kibana.indices = false; + expectedCapabilities.management.kibana.somethingElse = false; + expectedCapabilities.feature_2.bar = false; + expectedCapabilities.feature_2.foo = false; + expectedCapabilities.feature_3.bar = false; + expectedCapabilities.feature_3.foo = false; + + expect(result).toEqual(expectedCapabilities); + } + + { + space.solution = 'oblt'; + + // It should disable enterpriseSearch and securitySolution features + // which correspond to feature_1 and feature_3 + const result = await switcher(request, capabilities, false); + + const expectedCapabilities = buildCapabilities(); + + expectedCapabilities.feature_1.bar = false; + expectedCapabilities.feature_1.foo = false; + expectedCapabilities.feature_3.bar = false; + expectedCapabilities.feature_3.foo = false; + + expect(result).toEqual(expectedCapabilities); + } + + { + space.solution = 'security'; + + // It should disable enterpriseSearch and observability features + // which correspond to feature_1 and feature_2 + const result = await switcher(request, capabilities, false); + + const expectedCapabilities = buildCapabilities(); + + expectedCapabilities.navLinks.feature2 = false; + expectedCapabilities.catalogue.feature2Entry = false; + expectedCapabilities.navLinks.feature3 = false; + expectedCapabilities.management.kibana.somethingElse = false; + expectedCapabilities.feature_1.bar = false; + expectedCapabilities.feature_1.foo = false; + expectedCapabilities.feature_2.bar = false; + expectedCapabilities.feature_2.foo = false; + + expect(result).toEqual(expectedCapabilities); + } + }); + }); }); diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index d37337852b6b9..90ee85fece486 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -11,6 +11,7 @@ import type { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from '@kbn import type { KibanaFeature } from '@kbn/features-plugin/server'; import type { Space } from '../../common'; +import { withSpaceSolutionDisabledFeatures } from '../lib/utils/space_solution_disabled_features'; import type { PluginsStart } from '../plugin'; import type { SpacesServiceStart } from '../spaces_service'; @@ -61,7 +62,11 @@ function toggleDisabledFeatures( capabilities: Capabilities, activeSpace: Space ) { - const disabledFeatureKeys = activeSpace.disabledFeatures; + const disabledFeatureKeys = withSpaceSolutionDisabledFeatures( + features, + activeSpace.disabledFeatures, + activeSpace.solution + ); const { enabledFeatures, disabledFeatures } = features.reduce( (acc, feature) => { diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 99f0bc8f1e5ce..67617185ad0f2 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -14,6 +14,7 @@ import type { PluginsSetup } from '../../plugin'; import type { SpacesServiceStart } from '../../spaces_service/spaces_service'; import { wrapError } from '../errors'; import { getSpaceSelectorUrl } from '../get_space_selector_url'; +import { withSpaceSolutionDisabledFeatures } from '../utils/space_solution_disabled_features'; export interface OnPostAuthInterceptorDeps { http: CoreSetup['http']; @@ -105,18 +106,23 @@ export function initSpacesOnPostAuthRequestInterceptor({ } } + const allFeatures = features.getKibanaFeatures(); + const disabledFeatureKeys = withSpaceSolutionDisabledFeatures( + allFeatures, + space.disabledFeatures, + space.solution + ); + // Verify application is available in this space // The management page is always visible, so we shouldn't be restricting access to the kibana application in any situation. const appId = path.split('/', 3)[2]; - if (appId !== 'kibana' && space && space.disabledFeatures.length > 0) { + if (appId !== 'kibana' && space && disabledFeatureKeys.length > 0) { log.debug(`Verifying application is available: "${appId}"`); - const allFeatures = features.getKibanaFeatures(); - const isRegisteredApp = allFeatures.some((feature) => feature.app.includes(appId)); if (isRegisteredApp) { const enabledFeatures = allFeatures.filter( - (feature) => !space.disabledFeatures.includes(feature.id) + (feature) => !disabledFeatureKeys.includes(feature.id) ); const isAvailableInSpace = enabledFeatures.some((feature) => feature.app.includes(appId)); diff --git a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts new file mode 100644 index 0000000000000..908a4ee2ced57 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts @@ -0,0 +1,89 @@ +/* + * 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 { KibanaFeature } from '@kbn/features-plugin/server'; + +import { withSpaceSolutionDisabledFeatures } from './space_solution_disabled_features'; + +const features = [ + { id: 'feature1', category: { id: 'observability' } }, + { id: 'feature2', category: { id: 'enterpriseSearch' } }, + { id: 'feature3', category: { id: 'securitySolution' } }, + { id: 'feature4', category: { id: 'should_not_be_returned' } }, // not a solution, it should never appeared in the disabled features +] as KibanaFeature[]; + +describe('#withSpaceSolutionDisabledFeatures', () => { + describe('when the space solution is not set (undefined)', () => { + test('it does not remove any features', () => { + const spaceDisabledFeatures: string[] = ['foo']; + + const result = withSpaceSolutionDisabledFeatures(features, spaceDisabledFeatures); + + expect(result).toEqual(['foo']); + }); + }); + + describe('when the space solution is "classic"', () => { + test('it does not remove any features', () => { + const spaceDisabledFeatures: string[] = ['foo']; + const spaceSolution = 'classic'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + expect(result).toEqual(['foo']); + }); + }); + + describe('when the space solution is "es"', () => { + test('it removes the "oblt" and "security" features', () => { + const spaceDisabledFeatures: string[] = ['foo']; + const spaceSolution = 'es'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + // merges the spaceDisabledFeatures with the disabledFeatureKeysFromSolution + expect(result).toEqual(['feature1', 'feature3']); // "foo" from the spaceDisabledFeatures should not be removed + }); + }); + + describe('when the space solution is "oblt"', () => { + test('it removes the "search" and "security" features', () => { + const spaceDisabledFeatures: string[] = []; + const spaceSolution = 'oblt'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + expect(result).toEqual(['feature2', 'feature3']); + }); + }); + + describe('when the space solution is "security"', () => { + test('it removes the "observability" and "enterpriseSearch" features', () => { + const spaceDisabledFeatures: string[] = ['baz']; + const spaceSolution = 'security'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + expect(result).toEqual(['feature1', 'feature2']); // "baz" from the spaceDisabledFeatures should not be removed + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts new file mode 100644 index 0000000000000..4e66260f3d057 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts @@ -0,0 +1,64 @@ +/* + * 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 { KibanaFeature } from '@kbn/features-plugin/server'; + +import type { SolutionView } from '../../../common'; + +const getFeatureIdsForCategories = ( + features: KibanaFeature[], + categories: Array<'observability' | 'enterpriseSearch' | 'securitySolution'> +) => { + return features + .filter((feature) => + feature.category + ? categories.includes( + feature.category.id as 'observability' | 'enterpriseSearch' | 'securitySolution' + ) + : false + ) + .map((feature) => feature.id); +}; + +/** + * When a space has a `solution` defined, we want to disable features that are not part of that solution. + * This function takes the current space's disabled features and the space solution and returns + * the updated array of disabled features. + * + * @param spaceDisabledFeatures The current space's disabled features + * @param spaceSolution The current space's solution (es, oblt, security or classic) + * @returns The updated array of disabled features + */ +export function withSpaceSolutionDisabledFeatures( + features: KibanaFeature[], + spaceDisabledFeatures: string[] = [], + spaceSolution: SolutionView = 'classic' +): string[] { + if (spaceSolution === 'classic') { + return spaceDisabledFeatures; + } + + let disabledFeatureKeysFromSolution: string[] = []; + + if (spaceSolution === 'es') { + disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ + 'observability', + 'securitySolution', + ]); + } else if (spaceSolution === 'oblt') { + disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ + 'enterpriseSearch', + 'securitySolution', + ]); + } else if (spaceSolution === 'security') { + disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ + 'observability', + 'enterpriseSearch', + ]); + } + + return Array.from(new Set([...disabledFeatureKeysFromSolution])); +}