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 babd25dd3ec4b2..797d7fd1bdcc4e 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -59,6 +59,27 @@ const features = ([ }, }, }, + { + // feature 4 intentionally delcares the same items as feature 3 + id: 'feature_4', + name: 'Feature 4', + navLinkId: 'feature3', + app: ['feature3', 'feature3_app'], + catalogue: ['feature3Entry'], + management: { + kibana: ['indices'], + }, + privileges: { + all: { + app: [], + ui: [], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, ] as unknown) as Feature[]; const buildCapabilities = () => @@ -73,6 +94,7 @@ const buildCapabilities = () => catalogue: { discover: true, visualize: false, + feature3Entry: true, }, management: { kibana: { @@ -217,11 +239,38 @@ describe('capabilitiesSwitcher', () => { expect(result).toEqual(expectedCapabilities); }); + it('does not disable catalogue, management, or app entries when they are shared with an enabled feature', async () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_3'], + }; + + const capabilities = buildCapabilities(); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); + + const expectedCapabilities = buildCapabilities(); + + // These capabilities are shared by feature_4, which is enabled + expectedCapabilities.navLinks.feature3 = true; + expectedCapabilities.navLinks.feature3_app = true; + expectedCapabilities.catalogue.feature3Entry = true; + expectedCapabilities.management.kibana.indices = true; + // These capabilities are only exposed by feature_3, which is disabled + expectedCapabilities.feature_3.bar = false; + expectedCapabilities.feature_3.foo = false; + + expect(result).toEqual(expectedCapabilities); + }); + it('can disable everything', async () => { const space: Space = { id: 'space', name: '', - disabledFeatures: ['feature_1', 'feature_2', 'feature_3'], + disabledFeatures: ['feature_1', 'feature_2', 'feature_3', 'feature_4'], }; const capabilities = buildCapabilities(); diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 05d04295964892..00e2419136f488 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -54,22 +54,38 @@ function toggleDisabledFeatures( ) { const disabledFeatureKeys = activeSpace.disabledFeatures; - const disabledFeatures = disabledFeatureKeys - .map((key) => features.find((feature) => feature.id === key)) - .filter((feature) => typeof feature !== 'undefined') as Feature[]; + const [enabledFeatures, disabledFeatures] = features.reduce( + (acc, feature) => { + if (disabledFeatureKeys.includes(feature.id)) { + return [acc[0], [...acc[1], feature]]; + } + return [[...acc[0], feature], acc[1]]; + }, + [[], []] as [Feature[], Feature[]] + ); const navLinks = capabilities.navLinks; const catalogueEntries = capabilities.catalogue; const managementItems = capabilities.management; + const enabledAppEntries = new Set(enabledFeatures.flatMap((ef) => ef.app ?? [])); + const enabledCatalogueEntries = new Set(enabledFeatures.flatMap((ef) => ef.catalogue ?? [])); + const enabledManagementEntries = enabledFeatures.reduce((acc, feature) => { + const sections = Object.entries(feature.management ?? {}); + sections.forEach((section) => { + if (!acc.has(section[0])) { + acc.set(section[0], []); + } + acc.get(section[0])!.push(...section[1]); + }); + return acc; + }, new Map()); + for (const feature of disabledFeatures) { // Disable associated navLink, if one exists - if (feature.navLinkId && navLinks.hasOwnProperty(feature.navLinkId)) { - navLinks[feature.navLinkId] = false; - } - - feature.app.forEach((app) => { - if (navLinks.hasOwnProperty(app)) { + const featureNavLinks = feature.navLinkId ? [feature.navLinkId, ...feature.app] : feature.app; + featureNavLinks.forEach((app) => { + if (navLinks.hasOwnProperty(app) && !enabledAppEntries.has(app)) { navLinks[app] = false; } }); @@ -77,18 +93,24 @@ function toggleDisabledFeatures( // Disable associated catalogue entries const privilegeCatalogueEntries = feature.catalogue || []; privilegeCatalogueEntries.forEach((catalogueEntryId) => { - catalogueEntries[catalogueEntryId] = false; + if (!enabledCatalogueEntries.has(catalogueEntryId)) { + catalogueEntries[catalogueEntryId] = false; + } }); // Disable associated management items const privilegeManagementSections = feature.management || {}; Object.entries(privilegeManagementSections).forEach(([sectionId, sectionItems]) => { sectionItems.forEach((item) => { + const enabledManagementEntriesSection = enabledManagementEntries.get(sectionId); if ( managementItems.hasOwnProperty(sectionId) && managementItems[sectionId].hasOwnProperty(item) ) { - managementItems[sectionId][item] = false; + const isEnabledElsewhere = (enabledManagementEntriesSection ?? []).includes(item); + if (!isEnabledElsewhere) { + managementItems[sectionId][item] = false; + } } }); });