Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] [Spaces] Dynamically set the space disabled feature based on the space solution view (#191927) #193299

Merged
merged 1 commit into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]));
}