Skip to content

Commit

Permalink
[8.x] [Spaces] Dynamically set the space disabled feature based on th…
Browse files Browse the repository at this point in the history
…e space solution view (#191927) (#193299)
  • Loading branch information
sebelga authored Sep 19, 2024
1 parent 3a62655 commit dc13a02
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const features = [
id: 'feature_1',
name: 'Feature 1',
app: [],
category: { id: 'enterpriseSearch' },
scope: ['spaces', 'security'],
},
{
id: 'feature_2',
Expand All @@ -39,6 +41,7 @@ const features = [
},
},
},
category: { id: 'observability' },
},
{
id: 'feature_3',
Expand All @@ -58,6 +61,7 @@ const features = [
},
},
},
category: { id: 'securitySolution' },
},
{
// feature 4 intentionally delcares the same items as feature 3
Expand All @@ -78,6 +82,7 @@ const features = [
},
},
},
category: { id: 'observability' },
},
] as unknown as KibanaFeature[];

Expand Down Expand Up @@ -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);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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
});
});
});
Original file line number Diff line number Diff line change
@@ -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]));
}

0 comments on commit dc13a02

Please sign in to comment.