[8.16] [Spaces] Read Security license to infer eligibility for sub feature customization (#195389) (#196924)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[Spaces] Read Security license to infer eligibility for sub feature
customization (#195389)](https://github.com/elastic/kibana/pull/195389)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Eyo O.
Eyo","email":"7893459+eokoneyo@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-16T10:02:51Z","message":"[Spaces]
Read Security license to infer eligibility for sub feature customization
(#195389)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/195549\r\n\r\nThis PR adds
implementation such that eligibility to allow for the\r\ntoggling of the
switch for customization of sub features whilst defining\r\nprivileges
that would be assigned to a space is determined from
security\r\nlicense.\r\n\r\n\r\n###
Before\r\n![ScreenRecording2024-10-09at10 09
33-ezgif\r\ncom-video-to-gif-converter](https://github.com/user-attachments/assets/c80761c9-a45e-4784-835e-e6895d2fbed5)\r\n\r\n###
After\r\n\r\n![ScreenRecording2024-10-09at10 05
53-ezgif\r\ncom-video-to-gif-converter](https://github.com/user-attachments/assets/4e7d5724-42b0-4495-8fae-b47e7a97957c)\r\n\r\n<!--
### Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n-->","sha":"e6e30c20215ce7cbb8bd25d6646edc5d0a8bc33e","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","backport
missing","v9.0.0","backport:prev-major"],"number":195389,"url":"https://github.com/elastic/kibana/pull/195389","mergeCommit":{"message":"[Spaces]
Read Security license to infer eligibility for sub feature customization
(#195389)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/195549\r\n\r\nThis PR adds
implementation such that eligibility to allow for the\r\ntoggling of the
switch for customization of sub features whilst defining\r\nprivileges
that would be assigned to a space is determined from
security\r\nlicense.\r\n\r\n\r\n###
Before\r\n![ScreenRecording2024-10-09at10 09
33-ezgif\r\ncom-video-to-gif-converter](https://github.com/user-attachments/assets/c80761c9-a45e-4784-835e-e6895d2fbed5)\r\n\r\n###
After\r\n\r\n![ScreenRecording2024-10-09at10 05
53-ezgif\r\ncom-video-to-gif-converter](https://github.com/user-attachments/assets/4e7d5724-42b0-4495-8fae-b47e7a97957c)\r\n\r\n<!--
### Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n-->","sha":"e6e30c20215ce7cbb8bd25d6646edc5d0a8bc33e"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195389","number":195389,"mergeCommit":{"message":"[Spaces]
Read Security license to infer eligibility for sub feature customization
(#195389)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/195549\r\n\r\nThis PR adds
implementation such that eligibility to allow for the\r\ntoggling of the
switch for customization of sub features whilst defining\r\nprivileges
that would be assigned to a space is determined from
security\r\nlicense.\r\n\r\n\r\n###
Before\r\n![ScreenRecording2024-10-09at10 09
33-ezgif\r\ncom-video-to-gif-converter](https://github.com/user-attachments/assets/c80761c9-a45e-4784-835e-e6895d2fbed5)\r\n\r\n###
After\r\n\r\n![ScreenRecording2024-10-09at10 05
53-ezgif\r\ncom-video-to-gif-converter](https://github.com/user-attachments/assets/4e7d5724-42b0-4495-8fae-b47e7a97957c)\r\n\r\n<!--
### Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n-->","sha":"e6e30c20215ce7cbb8bd25d6646edc5d0a8bc33e"}}]}]
BACKPORT-->

Co-authored-by: Eyo O. Eyo <7893459+eokoneyo@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2024-10-18 11:31:46 -07:00 committed by GitHub
parent 9be2beb69b
commit e0d0ff9d7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 336 additions and 97 deletions

View file

@ -24,3 +24,4 @@ export type {
} from './src/roles';
export { PrivilegesAPIClientPublicContract } from './src/privileges';
export type { PrivilegesAPIClientGetAllArgs } from './src/privileges';
export type { SecurityLicense } from './src/license';

View file

@ -0,0 +1,10 @@
/*
* 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 { SecurityPluginSetup } from '../plugin';
export type SecurityLicense = SecurityPluginSetup['license'];

View file

@ -19,12 +19,13 @@ import {
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { EditSpaceContentTab } from './edit_space_content_tab';
import { EditSpaceProvider } from './provider';
import { EditSpaceProviderRoot } from './provider';
import type { Space } from '../../../common';
import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock';
import type { SpaceContentTypeSummaryItem } from '../../types';
import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock';
import { getRolesAPIClientMock } from '../roles_api_client.mock';
import { getSecurityLicenseMock } from '../security_license.mock';
const getUrlForApp = (appId: string) => appId;
const navigateToUrl = jest.fn();
@ -42,7 +43,7 @@ const logger = loggingSystemMock.createLogger();
const TestComponent: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<IntlProvider locale="en">
<EditSpaceProvider
<EditSpaceProviderRoot
capabilities={{
navLinks: {},
management: {},
@ -58,12 +59,13 @@ const TestComponent: React.FC<React.PropsWithChildren> = ({ children }) => {
notifications={notifications}
overlays={overlays}
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
theme={theme}
i18n={i18n}
logger={logger}
>
{children}
</EditSpaceProvider>
</EditSpaceProviderRoot>
</IntlProvider>
);
};

View file

@ -23,12 +23,13 @@ import { KibanaFeature } from '@kbn/features-plugin/common';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { EditSpaceSettingsTab } from './edit_space_general_tab';
import { EditSpaceProvider } from './provider/edit_space_provider';
import { EditSpaceProviderRoot } from './provider/edit_space_provider';
import type { SolutionView } from '../../../common';
import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants';
import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock';
import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock';
import { getRolesAPIClientMock } from '../roles_api_client.mock';
import { getSecurityLicenseMock } from '../security_license.mock';
const space = { id: 'default', name: 'Default', disabledFeatures: [], _reserved: true };
const history = scopedHistoryMock.create();
@ -64,7 +65,7 @@ describe('EditSpaceSettings', () => {
const TestComponent: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<IntlProvider locale="en">
<EditSpaceProvider
<EditSpaceProviderRoot
capabilities={{
navLinks: {},
management: {},
@ -80,12 +81,13 @@ describe('EditSpaceSettings', () => {
notifications={notifications}
overlays={overlays}
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
theme={theme}
i18n={i18n}
logger={logger}
>
{children}
</EditSpaceProvider>
</EditSpaceProviderRoot>
</IntlProvider>
);
};

View file

@ -9,9 +9,9 @@ import React from 'react';
import type { ComponentProps, PropsWithChildren } from 'react';
import { EditSpace } from './edit_space';
import { EditSpaceProvider, type EditSpaceProviderProps } from './provider';
import { EditSpaceProviderRoot, type EditSpaceProviderRootProps } from './provider';
type EditSpacePageProps = ComponentProps<typeof EditSpace> & EditSpaceProviderProps;
type EditSpacePageProps = ComponentProps<typeof EditSpace> & EditSpaceProviderRootProps;
export function EditSpacePage({
spaceId,
@ -25,7 +25,7 @@ export function EditSpacePage({
...editSpaceServicesProps
}: PropsWithChildren<EditSpacePageProps>) {
return (
<EditSpaceProvider {...editSpaceServicesProps}>
<EditSpaceProviderRoot {...editSpaceServicesProps}>
<EditSpace
spaceId={spaceId}
getFeatures={getFeatures}
@ -35,6 +35,6 @@ export function EditSpacePage({
allowFeatureVisibility={allowFeatureVisibility}
allowSolutionVisibility={allowSolutionVisibility}
/>
</EditSpaceProvider>
</EditSpaceProviderRoot>
);
}

View file

@ -19,10 +19,11 @@ import {
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { EditSpaceAssignedRolesTab } from './edit_space_roles_tab';
import { EditSpaceProvider } from './provider';
import { EditSpaceProviderRoot } from './provider';
import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock';
import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock';
import { getRolesAPIClientMock } from '../roles_api_client.mock';
import { getSecurityLicenseMock } from '../security_license.mock';
const getUrlForApp = (appId: string) => appId;
const navigateToUrl = jest.fn();
@ -51,7 +52,7 @@ describe('EditSpaceAssignedRolesTab', () => {
const TestComponent: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<IntlProvider locale="en">
<EditSpaceProvider
<EditSpaceProviderRoot
capabilities={{
navLinks: {},
management: {},
@ -67,12 +68,13 @@ describe('EditSpaceAssignedRolesTab', () => {
notifications={notifications}
overlays={overlays}
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
theme={theme}
i18n={i18n}
logger={logger}
>
{children}
</EditSpaceProvider>
</EditSpaceProviderRoot>
</IntlProvider>
);
};

View file

@ -62,7 +62,7 @@ export const EditSpaceAssignedRolesTab: FC<Props> = ({ space, features, isReadOn
(defaultSelected?: Role[]) => {
const overlayRef = overlays.openFlyout(
toMountPoint(
<EditSpaceProvider {...services}>
<EditSpaceProvider {...services} dispatch={dispatch} state={state}>
<PrivilegesRolesForm
{...{
space,
@ -109,9 +109,10 @@ export const EditSpaceAssignedRolesTab: FC<Props> = ({ space, features, isReadOn
[
overlays,
services,
dispatch,
state,
space,
features,
dispatch,
invokeClient,
getUrlForApp,
theme,

View file

@ -20,10 +20,15 @@ import {
import type { ApplicationStart } from '@kbn/core-application-browser';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './edit_space_provider';
import {
EditSpaceProviderRoot,
useEditSpaceServices,
useEditSpaceStore,
} from './edit_space_provider';
import { spacesManagerMock } from '../../../spaces_manager/spaces_manager.mock';
import { getPrivilegeAPIClientMock } from '../../privilege_api_client.mock';
import { getRolesAPIClientMock } from '../../roles_api_client.mock';
import { getSecurityLicenseMock } from '../../security_license.mock';
const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
@ -45,7 +50,7 @@ const SUTProvider = ({
}: PropsWithChildren<Partial<Pick<ApplicationStart, 'capabilities'>>>) => {
return (
<IntlProvider locale="en">
<EditSpaceProvider
<EditSpaceProviderRoot
{...{
logger,
i18n,
@ -58,12 +63,13 @@ const SUTProvider = ({
getUrlForApp: (_) => _,
getRolesAPIClient: getRolesAPIClientMock,
getPrivilegesAPIClient: getPrivilegeAPIClientMock,
getSecurityLicense: getSecurityLicenseMock,
navigateToUrl: jest.fn(),
capabilities,
}}
>
{children}
</EditSpaceProvider>
</EditSpaceProviderRoot>
</IntlProvider>
);
};

View file

@ -23,6 +23,7 @@ import type { Logger } from '@kbn/logging';
import type {
PrivilegesAPIClientPublicContract,
RolesAPIClient,
SecurityLicense,
} from '@kbn/security-plugin-types-public';
import {
@ -32,7 +33,7 @@ import {
} from './reducers';
import type { SpacesManager } from '../../../spaces_manager';
export interface EditSpaceProviderProps
export interface EditSpaceProviderRootProps
extends Pick<CoreStart, 'theme' | 'i18n' | 'overlays' | 'http' | 'notifications'> {
logger: Logger;
capabilities: ApplicationStart['capabilities'];
@ -42,10 +43,7 @@ export interface EditSpaceProviderProps
spacesManager: SpacesManager;
getRolesAPIClient: () => Promise<RolesAPIClient>;
getPrivilegesAPIClient: () => Promise<PrivilegesAPIClientPublicContract>;
}
export interface EditSpaceServices extends EditSpaceProviderProps {
invokeClient<R extends unknown>(arg: (clients: EditSpaceClients) => Promise<R>): Promise<R>;
getSecurityLicense: () => Promise<SecurityLicense>;
}
interface EditSpaceClients {
@ -54,6 +52,15 @@ interface EditSpaceClients {
privilegesClient: PrivilegesAPIClientPublicContract;
}
export interface EditSpaceServices
extends Omit<
EditSpaceProviderRootProps,
'getRolesAPIClient' | 'getPrivilegesAPIClient' | 'getSecurityLicense'
> {
invokeClient<R extends unknown>(arg: (clients: EditSpaceClients) => Promise<R>): Promise<R>;
license?: SecurityLicense;
}
export interface EditSpaceStore {
state: IEditSpaceStoreState;
dispatch: Dispatch<IDispatchAction>;
@ -63,16 +70,43 @@ const createSpaceRolesContext = once(() => createContext<EditSpaceStore | null>(
const createEditSpaceServicesContext = once(() => createContext<EditSpaceServices | null>(null));
/**
*
* @description EditSpaceProvider is a provider component that wraps the children components with the necessary context providers for the Edit Space feature. It provides the necessary services and state management for the feature,
* this is provided as an export for use with out of band renders within the spaces app
*/
export const EditSpaceProvider = ({
children,
state,
dispatch,
...services
}: PropsWithChildren<EditSpaceProviderProps>) => {
}: PropsWithChildren<EditSpaceServices & EditSpaceStore>) => {
const EditSpaceStoreContext = createSpaceRolesContext();
const EditSpaceServicesContext = createEditSpaceServicesContext();
const clients = useRef(
Promise.all([services.getRolesAPIClient(), services.getPrivilegesAPIClient()])
return (
<EditSpaceServicesContext.Provider value={services}>
<EditSpaceStoreContext.Provider value={{ state, dispatch }}>
{children}
</EditSpaceStoreContext.Provider>
</EditSpaceServicesContext.Provider>
);
};
/**
* @description EditSpaceProviderRoot is the root provider for the Edit Space feature. It instantiates the necessary services and state management for the feature. It ideally
* should only be rendered once
*/
export const EditSpaceProviderRoot = ({
children,
...services
}: PropsWithChildren<EditSpaceProviderRootProps>) => {
const { logger, getRolesAPIClient, getPrivilegesAPIClient, getSecurityLicense } = services;
const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()]));
const license = useRef(getSecurityLicense);
const licenseRef = useRef<SecurityLicense>();
const rolesAPIClientRef = useRef<RolesAPIClient>();
const privilegesClientRef = useRef<PrivilegesAPIClientPublicContract>();
@ -81,7 +115,14 @@ export const EditSpaceProvider = ({
fetchRolesError: false,
});
const { logger } = services;
const resolveSecurityLicense = useCallback(async () => {
try {
licenseRef.current = await license.current();
} catch (err) {
logger.error('Could not resolve Security License!', err);
}
}, [logger]);
const resolveAPIClients = useCallback(async () => {
try {
[rolesAPIClientRef.current, privilegesClientRef.current] = await clients.current;
@ -94,6 +135,10 @@ export const EditSpaceProvider = ({
resolveAPIClients();
}, [resolveAPIClients]);
useEffect(() => {
resolveSecurityLicense();
}, [resolveSecurityLicense]);
const createInitialState = useCallback((state: IEditSpaceStoreState) => {
return state;
}, []);
@ -118,11 +163,11 @@ export const EditSpaceProvider = ({
);
return (
<EditSpaceServicesContext.Provider value={{ ...services, invokeClient }}>
<EditSpaceStoreContext.Provider value={{ state, dispatch }}>
{children}
</EditSpaceStoreContext.Provider>
</EditSpaceServicesContext.Provider>
<EditSpaceProvider
{...{ ...services, invokeClient, state, dispatch, license: licenseRef.current }}
>
{children}
</EditSpaceProvider>
);
};

View file

@ -5,9 +5,14 @@
* 2.0.
*/
export { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './edit_space_provider';
export {
EditSpaceProviderRoot,
EditSpaceProvider,
useEditSpaceServices,
useEditSpaceStore,
} from './edit_space_provider';
export type {
EditSpaceProviderProps,
EditSpaceProviderRootProps,
EditSpaceServices,
EditSpaceStore,
} from './edit_space_provider';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import crypto from 'crypto';
import React from 'react';
@ -19,7 +19,7 @@ import {
themeServiceMock,
} from '@kbn/core/public/mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import type { Role } from '@kbn/security-plugin-types-common';
import type { Role, SecurityLicense } from '@kbn/security-plugin-types-common';
import {
createRawKibanaPrivileges,
kibanaFeatures,
@ -33,11 +33,8 @@ import {
FEATURE_PRIVILEGES_READ,
} from '../../../../../common/constants';
import { spacesManagerMock } from '../../../../spaces_manager/spaces_manager.mock';
import {
createPrivilegeAPIClientMock,
getPrivilegeAPIClientMock,
} from '../../../privilege_api_client.mock';
import { createRolesAPIClientMock, getRolesAPIClientMock } from '../../../roles_api_client.mock';
import { createPrivilegeAPIClientMock } from '../../../privilege_api_client.mock';
import { createRolesAPIClientMock } from '../../../roles_api_client.mock';
import { EditSpaceProvider } from '../../provider';
const rolesAPIClient = createRolesAPIClientMock();
@ -74,6 +71,9 @@ const spacesClientsInvocatorMock = jest.fn((fn) =>
const dispatchMock = jest.fn();
const onSaveCompleted = jest.fn();
const closeFlyout = jest.fn();
const licenseMock = {
getFeatures: jest.fn(() => ({})),
} as unknown as SecurityLicense;
const renderPrivilegeRolesForm = ({
preSelectedRoles,
@ -93,15 +93,20 @@ const renderPrivilegeRolesForm = ({
spacesManager,
serverBasePath: '',
getUrlForApp: jest.fn((_) => _),
getRolesAPIClient: getRolesAPIClientMock,
getPrivilegesAPIClient: getPrivilegeAPIClientMock,
navigateToUrl: jest.fn(),
license: licenseMock,
capabilities: {
navLinks: {},
management: {},
catalogue: {},
spaces: { manage: true },
},
dispatch: dispatchMock,
state: {
roles: new Map(),
fetchRolesError: false,
},
invokeClient: spacesClientsInvocatorMock,
}}
>
<PrivilegesRolesForm
@ -111,9 +116,6 @@ const renderPrivilegeRolesForm = ({
closeFlyout,
defaultSelected: preSelectedRoles,
onSaveCompleted,
storeDispatch: dispatchMock,
spacesClientsInvocator: spacesClientsInvocatorMock,
getUrlForApp: jest.fn((_) => _),
}}
/>
</EditSpaceProvider>
@ -358,11 +360,11 @@ describe('PrivilegesRolesForm', () => {
preSelectedRoles: roles,
});
await waitFor(() => null);
expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute(
'aria-pressed',
String(true)
await waitFor(() =>
expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute(
'aria-pressed',
String(true)
)
);
await user.click(screen.getByTestId('custom-privilege-button'));
@ -408,5 +410,116 @@ describe('PrivilegesRolesForm', () => {
String(true)
);
});
it('prevents customization up to sub privilege level by default', async () => {
const user = userEvent.setup();
const roles: Role[] = [
createRole('test_role_1', [
{ base: [FEATURE_PRIVILEGES_READ], feature: {}, spaces: [space.id] },
]),
];
getRolesSpy.mockResolvedValue([]);
getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures));
const featuresWithSubFeatures = kibanaFeatures.filter((kibanaFeature) =>
Boolean(kibanaFeature.subFeatures.length)
);
renderPrivilegeRolesForm({
preSelectedRoles: roles,
});
await user.click(screen.getByTestId('custom-privilege-button'));
expect(
screen.getByTestId('space-assign-role-privilege-customization-form')
).toBeInTheDocument();
const featureUT = featuresWithSubFeatures[0];
// change a single feature with sub features to read from default privilege "none"
await user.click(screen.getByTestId(`${featureUT.id}_${FEATURE_PRIVILEGES_READ}`));
// click on the accordion toggle to show sub features
await user.click(
screen.getByTestId(
`featurePrivilegeControls_${featureUT.category.id}_${featureUT.id}_accordionToggle`
)
);
// sub feature table renders
expect(
screen.getByTestId(`${featureUT.category.id}_${featureUT.id}_subFeaturesTable`)
).toBeInTheDocument();
// assert switch to customize sub feature can toggled
expect(
within(
screen.getByTestId(
`${featureUT.category.id}_${featureUT.id}_customizeSubFeaturesSwitchContainer`
)
).getByTestId('customizeSubFeaturePrivileges')
).toBeDisabled();
});
it('supports customization up to sub privilege level only when security license allows', async () => {
const user = userEvent.setup();
const roles: Role[] = [
createRole('test_role_1', [
{ base: [FEATURE_PRIVILEGES_READ], feature: {}, spaces: [space.id] },
]),
];
// enable sub feature privileges
(licenseMock.getFeatures as jest.Mock).mockReturnValue({
allowSubFeaturePrivileges: true,
});
getRolesSpy.mockResolvedValue([]);
getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures));
const featuresWithSubFeatures = kibanaFeatures.filter((kibanaFeature) =>
Boolean(kibanaFeature.subFeatures.length)
);
renderPrivilegeRolesForm({
preSelectedRoles: roles,
});
await user.click(screen.getByTestId('custom-privilege-button'));
expect(
screen.getByTestId('space-assign-role-privilege-customization-form')
).toBeInTheDocument();
const featureUT = featuresWithSubFeatures[0];
// change a single feature with sub features to read from default privilege "none"
await user.click(screen.getByTestId(`${featureUT.id}_${FEATURE_PRIVILEGES_READ}`));
// click on the accordion toggle to show sub features
await user.click(
screen.getByTestId(
`featurePrivilegeControls_${featureUT.category.id}_${featureUT.id}_accordionToggle`
)
);
// sub feature table renders
expect(
screen.getByTestId(`${featureUT.category.id}_${featureUT.id}_subFeaturesTable`)
).toBeInTheDocument();
// assert switch to customize sub feature can toggled
expect(
within(
screen.getByTestId(
`${featureUT.category.id}_${featureUT.id}_customizeSubFeaturesSwitchContainer`
)
).getByTestId('customizeSubFeaturePrivileges')
).not.toBeDisabled();
});
});
});

View file

@ -46,7 +46,7 @@ import {
FEATURE_PRIVILEGES_CUSTOM,
FEATURE_PRIVILEGES_READ,
} from '../../../../../common/constants';
import { type EditSpaceServices, type EditSpaceStore, useEditSpaceServices } from '../../provider';
import { useEditSpaceServices, useEditSpaceStore } from '../../provider';
type KibanaRolePrivilege =
| keyof NonNullable<KibanaFeatureConfig['privileges']>
@ -62,9 +62,6 @@ interface PrivilegesRolesFormProps {
* this is useful when the form is opened in edit mode
*/
defaultSelected?: Role[];
storeDispatch: EditSpaceStore['dispatch'];
spacesClientsInvocator: EditSpaceServices['invokeClient'];
getUrlForApp: EditSpaceServices['getUrlForApp'];
}
const createRolesComboBoxOptions = (roles: Role[]): Array<EuiComboBoxOptionOption<Role>> =>
@ -74,17 +71,9 @@ const createRolesComboBoxOptions = (roles: Role[]): Array<EuiComboBoxOptionOptio
}));
export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
const {
space,
onSaveCompleted,
closeFlyout,
features,
defaultSelected = [],
spacesClientsInvocator,
storeDispatch,
getUrlForApp,
} = props;
const { logger, notifications } = useEditSpaceServices();
const { space, onSaveCompleted, closeFlyout, features, defaultSelected = [] } = props;
const { logger, notifications, license, invokeClient, getUrlForApp } = useEditSpaceServices();
const { dispatch: storeDispatch } = useEditSpaceStore();
const [assigningToRole, setAssigningToRole] = useState(false);
const [fetchingDataDeps, setFetchingDataDeps] = useState(false);
const [kibanaPrivileges, setKibanaPrivileges] = useState<RawKibanaPrivileges | null>(null);
@ -98,7 +87,7 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
async function fetchRequiredData(spaceId: string) {
setFetchingDataDeps(true);
const [systemRoles, _kibanaPrivileges] = await spacesClientsInvocator((clients) =>
const [systemRoles, _kibanaPrivileges] = await invokeClient((clients) =>
Promise.all([
clients.rolesClient.getRoles(),
clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }),
@ -123,7 +112,7 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
}
fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false));
}, [space.id, spacesClientsInvocator]);
}, [invokeClient, space.id]);
const selectedRolesCombinedPrivileges = useMemo(() => {
const combinedPrivilege = new Set(
@ -315,7 +304,7 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
return selectedRole.value!;
});
await spacesClientsInvocator((clients) =>
await invokeClient((clients) =>
clients.rolesClient.bulkUpdateRoles({ rolesUpdate: updatedRoles }).then((response) => {
setAssigningToRole(false);
onSaveCompleted(response);
@ -338,13 +327,14 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
});
}
}, [
selectedRoles,
spacesClientsInvocator,
storeDispatch,
onSaveCompleted,
space.id,
roleSpacePrivilege,
roleCustomizationAnchor,
roleCustomizationAnchor.value?.kibana,
roleCustomizationAnchor.privilegeIndex,
selectedRoles,
invokeClient,
storeDispatch,
space.id,
onSaveCompleted,
logger,
notifications.toasts,
]);
@ -571,7 +561,9 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
)
}
allSpacesSelected={false}
canCustomizeSubFeaturePrivileges={false}
canCustomizeSubFeaturePrivileges={
license?.getFeatures().allowSubFeaturePrivileges ?? false
}
/>
)}
</React.Fragment>

View file

@ -13,6 +13,7 @@ import { managementPluginMock } from '@kbn/management-plugin/public/mocks';
import { ManagementService } from './management_service';
import { getRolesAPIClientMock } from './roles_api_client.mock';
import { getSecurityLicenseMock } from './security_license.mock';
import { EventTracker } from '../analytics';
import type { ConfigType } from '../config';
import type { PluginsStart } from '../plugin';
@ -49,6 +50,7 @@ describe('ManagementService', () => {
logger,
getRolesAPIClient: getRolesAPIClientMock,
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: getSecurityLicenseMock,
eventTracker,
});
@ -72,6 +74,7 @@ describe('ManagementService', () => {
logger,
getRolesAPIClient: getRolesAPIClientMock,
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: getSecurityLicenseMock,
eventTracker,
});
});
@ -96,6 +99,7 @@ describe('ManagementService', () => {
logger,
getRolesAPIClient: jest.fn(),
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: getSecurityLicenseMock,
eventTracker,
});

View file

@ -5,29 +5,15 @@
* 2.0.
*/
import type { StartServicesAccessor } from '@kbn/core/public';
import type { Logger } from '@kbn/logging';
import type { ManagementApp, ManagementSetup } from '@kbn/management-plugin/public';
import type {
PrivilegesAPIClientPublicContract,
RolesAPIClient,
} from '@kbn/security-plugin-types-public';
import { spacesManagementApp } from './spaces_management_app';
import type { EventTracker } from '../analytics';
import type { ConfigType } from '../config';
import type { PluginsStart } from '../plugin';
import type { SpacesManager } from '../spaces_manager';
import {
spacesManagementApp,
type CreateParams as SpacesManagementAppCreateParams,
} from './spaces_management_app';
interface SetupDeps {
interface SetupDeps extends SpacesManagementAppCreateParams {
management: ManagementSetup;
getStartServices: StartServicesAccessor<PluginsStart>;
spacesManager: SpacesManager;
config: ConfigType;
getRolesAPIClient: () => Promise<RolesAPIClient>;
eventTracker: EventTracker;
getPrivilegesAPIClient: () => Promise<PrivilegesAPIClientPublicContract>;
logger: Logger;
}
export class ManagementService {
@ -42,6 +28,7 @@ export class ManagementService {
getRolesAPIClient,
eventTracker,
getPrivilegesAPIClient,
getSecurityLicense,
}: SetupDeps) {
this.registeredSpacesManagementApp = management.sections.section.kibana.registerApp(
spacesManagementApp.create({
@ -52,6 +39,7 @@ export class ManagementService {
getRolesAPIClient,
eventTracker,
getPrivilegesAPIClient,
getSecurityLicense,
})
);
}

View file

@ -0,0 +1,49 @@
/*
* 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 { BehaviorSubject, type Observable } from 'rxjs';
import type { SecurityLicense } from '@kbn/security-plugin-types-public';
type SecurityLicenseFeatures = SecurityLicense['features$'] extends Observable<infer P> ? P : never;
export const createSecurityLicenseMock = ({
securityFeaturesConfig,
}: {
securityFeaturesConfig: SecurityLicenseFeatures;
}): SecurityLicense => {
return {
isLicenseAvailable: jest.fn(),
isEnabled: jest.fn(),
getFeatures: jest.fn(),
getUnavailableReason: jest.fn(),
hasAtLeast: jest.fn(),
getLicenseType: jest.fn(),
features$: new BehaviorSubject<SecurityLicenseFeatures>(securityFeaturesConfig),
};
};
export const getSecurityLicenseMock = jest.fn().mockResolvedValue(
createSecurityLicenseMock({
securityFeaturesConfig: {
showLinks: true,
showLogin: true,
allowLogin: true,
allowRbac: true,
allowFips: true,
showRoleMappingsManagement: true,
allowAccessAgreement: true,
allowAuditLogging: true,
allowSubFeaturePrivileges: true,
allowRoleFieldLevelSecurity: true,
allowRoleDocumentLevelSecurity: true,
allowRoleRemoteIndexPrivileges: true,
allowRemoteClusterPrivileges: true,
allowUserProfileCollaboration: true,
},
})
);

View file

@ -77,6 +77,7 @@ async function mountApp(basePath: string, pathname: string, spaceId?: string) {
logger,
getRolesAPIClient: jest.fn(),
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: jest.fn(),
eventTracker,
})
.mount({
@ -101,6 +102,7 @@ describe('spacesManagementApp', () => {
logger,
getRolesAPIClient: jest.fn(),
getPrivilegesAPIClient: jest.fn(),
getSecurityLicense: jest.fn(),
eventTracker,
})
).toMatchInlineSnapshot(`

View file

@ -18,6 +18,7 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import type {
PrivilegesAPIClientPublicContract,
RolesAPIClient,
SecurityLicense,
} from '@kbn/security-plugin-types-public';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { Route, Router, Routes } from '@kbn/shared-ux-router';
@ -28,7 +29,7 @@ import type { ConfigType } from '../config';
import type { PluginsStart } from '../plugin';
import type { SpacesManager } from '../spaces_manager';
interface CreateParams {
export interface CreateParams {
getStartServices: StartServicesAccessor<PluginsStart>;
spacesManager: SpacesManager;
config: ConfigType;
@ -36,6 +37,7 @@ interface CreateParams {
getRolesAPIClient: () => Promise<RolesAPIClient>;
eventTracker: EventTracker;
getPrivilegesAPIClient: () => Promise<PrivilegesAPIClientPublicContract>;
getSecurityLicense: () => Promise<SecurityLicense>;
}
export const spacesManagementApp = Object.freeze({
@ -48,6 +50,7 @@ export const spacesManagementApp = Object.freeze({
eventTracker,
getRolesAPIClient,
getPrivilegesAPIClient,
getSecurityLicense,
}: CreateParams) {
const title = i18n.translate('xpack.spaces.displayName', {
defaultMessage: 'Spaces',
@ -146,6 +149,7 @@ export const spacesManagementApp = Object.freeze({
capabilities={application.capabilities}
getUrlForApp={application.getUrlForApp}
navigateToUrl={application.navigateToUrl}
getSecurityLicense={getSecurityLicense}
serverBasePath={http.basePath.serverBasePath}
getFeatures={features.getFeatures}
http={http}

View file

@ -10,7 +10,7 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb
import type { FeaturesPluginStart } from '@kbn/features-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
import type { SecurityPluginStart } from '@kbn/security-plugin-types-public';
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin-types-public';
import { EventTracker, registerAnalyticsContext, registerSpacesEventTypes } from './analytics';
import type { ConfigType } from './config';
@ -114,6 +114,18 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
return security.contract.authz.privileges;
};
const getSecurityLicense = async () => {
const { security } = await core.plugins.onSetup<{ security: SecurityPluginSetup }>(
'security'
);
if (!security.found) {
throw new Error('Security plugin is not available as runtime dependency.');
}
return security.contract.license;
};
if (plugins.home) {
plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry());
}
@ -129,6 +141,7 @@ export class SpacesPlugin implements Plugin<SpacesPluginSetup, SpacesPluginStart
getRolesAPIClient,
eventTracker: this.eventTracker,
getPrivilegesAPIClient,
getSecurityLicense,
});
}