From ea3934dc1d51052d08d948e4ea42460dde131f88 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Thu, 19 Jan 2023 11:37:29 +0000 Subject: [PATCH] Make spaces optional on the front-end --- .../infra/public/hooks/use_kibana_space.ts | 6 +- x-pack/plugins/infra/public/types.ts | 2 +- .../lens/public/app_plugin/app.test.tsx | 2 +- .../plugins/lens/public/app_plugin/types.ts | 2 +- x-pack/plugins/lens/public/plugin.ts | 2 +- .../state_management/load_initial.test.tsx | 2 +- .../contexts/kibana/kibana_context.ts | 2 +- .../public/hooks/use_kibana_space.tsx | 3 +- x-pack/plugins/observability/public/plugin.ts | 2 +- x-pack/plugins/osquery/public/types.ts | 2 +- .../roles/edit_role/edit_role_page.test.tsx | 7 + .../roles/edit_role/edit_role_page.tsx | 1 + .../kibana_privileges_region.test.tsx.snap | 15 + .../kibana/kibana_privileges_region.test.tsx | 10 + .../kibana/kibana_privileges_region.tsx | 25 +- .../simple_privilege_section.test.tsx.snap | 171 +++++++++ .../kibana/simple_privilege_section/index.ts | 8 + .../privilege_selector.tsx | 59 ++++ .../simple_privilege_section.test.tsx | 248 +++++++++++++ .../simple_privilege_section.tsx | 334 ++++++++++++++++++ .../unsupported_space_privileges_warning.tsx | 26 ++ .../space_aware_privilege_section.tsx | 2 +- .../public/hooks/use_kibana_space.tsx | 3 +- x-pack/plugins/synthetics/public/plugin.ts | 2 +- 24 files changed, 916 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_space.ts b/x-pack/plugins/infra/public/hooks/use_kibana_space.ts index 5a90327ff05816..3315357c51131d 100644 --- a/x-pack/plugins/infra/public/hooks/use_kibana_space.ts +++ b/x-pack/plugins/infra/public/hooks/use_kibana_space.ts @@ -17,7 +17,11 @@ export type ActiveSpace = export const useActiveKibanaSpace = (): ActiveSpace => { const kibana = useKibanaContextForPlugin(); - const asyncActiveSpace = useAsync(kibana.services.spaces.getActiveSpace); + const asyncActiveSpace = useAsync( + kibana.services.spaces + ? kibana.services.spaces.getActiveSpace + : () => Promise.resolve(undefined) + ); if (asyncActiveSpace.loading) { return { diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index be8c4d877c61cd..d0368d0b67a199 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -67,7 +67,7 @@ export interface InfraClientStartDeps { unifiedSearch: UnifiedSearchPublicPluginStart; dataViews: DataViewsPublicPluginStart; observability: ObservabilityPublicStart; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; usageCollection: UsageCollectionStart; ml: MlPluginStart; diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 648fd61203943a..eba9c383040d70 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -1563,7 +1563,7 @@ describe('Lens App', () => { }, }, }); - expect(services.spaces.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + expect(services.spaces?.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ currentObjectId: '1234', objectNoun: 'Lens visualization', otherObjectId: '2', diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 831b7ce54da39f..7e106a9183eea8 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -151,7 +151,7 @@ export interface LensAppServices { savedObjectsTagging?: SavedObjectTaggingPluginStart; getOriginatingAppName: () => string | undefined; presentationUtil: PresentationUtilPluginStart; - spaces: SpacesApi; + spaces?: SpacesApi; charts: ChartsPluginSetup; share?: SharePluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index f0c09a9fe31a7e..71324733a17ed6 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -144,7 +144,7 @@ export interface LensPluginStartDependencies { dataViewFieldEditor: IndexPatternFieldEditorStart; dataViewEditor: DataViewEditorStart; inspector: InspectorStartContract; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; usageCollection?: UsageCollectionStart; docLinks: DocLinksStart; share?: SharePluginStart; diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx index 2d8ce9405f12ff..ecf519382ff6ca 100644 --- a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx @@ -309,7 +309,7 @@ describe('Initializing the store', () => { expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ savedObjectId: defaultSavedObjectId, }); - expect(deps.lensServices.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith({ + expect(deps.lensServices.spaces?.ui.redirectLegacyUrl).toHaveBeenCalledWith({ path: '#/edit/id2?search', aliasPurpose: 'savedObjectConversion', objectNoun: 'Lens visualization', diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index d88d9abb24a87c..aebe71477c7bce 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -40,7 +40,7 @@ interface StartPlugins { usageCollection?: UsageCollectionSetup; fieldFormats: FieldFormatsRegistry; dashboard: DashboardSetup; - spacesApi: SpacesPluginStart; + spacesApi?: SpacesPluginStart; charts: ChartsPluginStart; cases?: CasesUiStart; unifiedSearch: UnifiedSearchPublicPluginStart; diff --git a/x-pack/plugins/observability/public/hooks/use_kibana_space.tsx b/x-pack/plugins/observability/public/hooks/use_kibana_space.tsx index 11716e948d8552..911971ad877b0e 100644 --- a/x-pack/plugins/observability/public/hooks/use_kibana_space.tsx +++ b/x-pack/plugins/observability/public/hooks/use_kibana_space.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Space } from '@kbn/spaces-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ObservabilityPublicPluginsStart, useFetcher } from '..'; @@ -15,7 +14,7 @@ export const useKibanaSpace = () => { data: space, loading, error, - } = useFetcher>(() => { + } = useFetcher(() => { return services.spaces?.getActiveSpace(); }, [services.spaces]); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 0cb24f0495cde4..85a94c4d0b857c 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -103,7 +103,7 @@ export interface ObservabilityPublicPluginsStart { actionTypeRegistry: ActionTypeRegistryContract; security: SecurityPluginStart; guidedOnboarding: GuidedOnboardingPluginStart; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; } export type ObservabilityPublicStart = ReturnType; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts index f6d05a3d459963..88d0b07ec22c6c 100644 --- a/x-pack/plugins/osquery/public/types.ts +++ b/x-pack/plugins/osquery/public/types.ts @@ -50,7 +50,7 @@ export interface StartPlugins { fleet: FleetStart; lens?: LensPublicStart; security: SecurityPluginStart; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; cases: CasesUiStart; timelines: TimelinesUIStart; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index e6d6403460e504..0eeabaf3b243ab 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -15,6 +15,8 @@ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { KibanaFeature } from '@kbn/features-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { Space } from '@kbn/spaces-plugin/public'; +import { spacesManagerMock } from '@kbn/spaces-plugin/public/spaces_manager/mocks'; +import { getUiApi } from '@kbn/spaces-plugin/public/ui_api'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { licenseMock } from '../../../../common/licensing/index.mock'; @@ -26,6 +28,10 @@ import { EditRolePage } from './edit_role_page'; import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; import { TransformErrorSection } from './privileges/kibana/transform_error_section'; +const spacesManager = spacesManagerMock.create(); +const { getStartServices } = coreMock.createSetup(); +const spacesApiUi = getUiApi({ spacesManager, getStartServices }); + const buildFeatures = () => { return [ new KibanaFeature({ @@ -183,6 +189,7 @@ function getProps({ fatalErrors, uiCapabilities: buildUICapabilities(canManageSpaces), history: scopedHistoryMock.create(), + spacesApiUi, }; } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 36d6815493c98c..c0459cd157e09f 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -470,6 +470,7 @@ export const EditRolePage: FunctionComponent = ({ renders without crashing 1`] = ` }, ] } + spacesApiUi={ + Object { + "components": Object { + "getCopyToSpaceFlyout": [Function], + "getEmbeddableLegacyUrlConflict": [Function], + "getLegacyUrlConflict": [Function], + "getShareToSpaceFlyout": [Function], + "getSpaceAvatar": [Function], + "getSpaceList": [Function], + "getSpacesContextProvider": [Function], + }, + "redirectLegacyUrl": [Function], + "useSpaces": [Function], + } + } uiCapabilities={ Object { "catalogue": Object {}, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.test.tsx index 5b1d06a741ad2b..c61ea2f7229889 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.test.tsx @@ -8,6 +8,10 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { coreMock } from '@kbn/core/public/mocks'; +import { spacesManagerMock } from '@kbn/spaces-plugin/public/spaces_manager/mocks'; +import { getUiApi } from '@kbn/spaces-plugin/public/ui_api'; + import type { Role } from '../../../../../../common/model'; import { KibanaPrivileges } from '../../../model'; import { RoleValidator } from '../../validate_role'; @@ -15,6 +19,10 @@ import { KibanaPrivilegesRegion } from './kibana_privileges_region'; import { SpaceAwarePrivilegeSection } from './space_aware_privilege_section'; import { TransformErrorSection } from './transform_error_section'; +const spacesManager = spacesManagerMock.create(); +const { getStartServices } = coreMock.createSetup(); +const spacesApiUi = getUiApi({ spacesManager, getStartServices }); + const buildProps = () => { return { role: { @@ -62,6 +70,8 @@ const buildProps = () => { onChange: jest.fn(), validator: new RoleValidator(), canCustomizeSubFeaturePrivileges: true, + spacesEnabled: true, + spacesApiUi, }; }; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx index 612bcf05aab568..62a1a021c0aaeb 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx @@ -14,11 +14,13 @@ import type { Role } from '../../../../../../common/model'; import type { KibanaPrivileges } from '../../../model'; import { CollapsiblePanel } from '../../collapsible_panel'; import type { RoleValidator } from '../../validate_role'; +import { SimplePrivilegeSection } from './simple_privilege_section'; import { SpaceAwarePrivilegeSection } from './space_aware_privilege_section'; import { TransformErrorSection } from './transform_error_section'; interface Props { role: Role; + spacesEnabled: boolean; canCustomizeSubFeaturePrivileges: boolean; spaces?: Space[]; uiCapabilities: Capabilities; @@ -42,6 +44,7 @@ export class KibanaPrivilegesRegion extends Component { const { kibanaPrivileges, role, + spacesEnabled, canCustomizeSubFeaturePrivileges, spaces = [], uiCapabilities, @@ -55,17 +58,29 @@ export class KibanaPrivilegesRegion extends Component { return ; } + if (spacesApiUi && spacesEnabled) { + return ( + + ); + } + return ( - ); }; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap new file mode 100644 index 00000000000000..b490dc7cefe268 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders without crashing 1`] = ` + + + + +

+ +

+
+
+ + + } + labelType="label" + > + + + + + +

+ +

+
+ , + "inputDisplay": , + "value": "none", + }, + Object { + "dropdownDisplay": + + + + +

+ +

+
+
, + "inputDisplay": , + "value": "read", + }, + Object { + "dropdownDisplay": + + + + +

+ +

+
+
, + "inputDisplay": , + "value": "all", + }, + Object { + "dropdownDisplay": + + + + +

+ +

+
+
, + "inputDisplay": , + "value": "custom", + }, + ] + } + valueOfSelected="none" + /> +
+
+
+
+`; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts new file mode 100644 index 00000000000000..bea5a3d2d592f1 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { SimplePrivilegeSection } from './simple_privilege_section'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx new file mode 100644 index 00000000000000..72061958ecc357 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiSelect } from '@elastic/eui'; +import type { ChangeEvent } from 'react'; +import React, { Component } from 'react'; + +import { NO_PRIVILEGE_VALUE } from '../constants'; + +interface Props { + ['data-test-subj']: string; + availablePrivileges: string[]; + onChange: (privilege: string) => void; + value: string | null; + allowNone?: boolean; + disabled?: boolean; + compressed?: boolean; +} + +export class PrivilegeSelector extends Component { + public state = {}; + + public render() { + const { availablePrivileges, value, disabled, allowNone, compressed } = this.props; + + const options = []; + + if (allowNone) { + options.push({ value: NO_PRIVILEGE_VALUE, text: 'none' }); + } + + options.push( + ...availablePrivileges.map((p) => ({ + value: p, + text: p, + })) + ); + + return ( + + ); + } + + public onChange = (e: ChangeEvent) => { + this.props.onChange(e.target.value); + }; +} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx new file mode 100644 index 00000000000000..8f5efff64aadd7 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -0,0 +1,248 @@ +/* + * 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 { EuiButtonGroupProps } from '@elastic/eui'; +import { EuiButtonGroup, EuiComboBox, EuiSuperSelect } from '@elastic/eui'; +import React from 'react'; + +import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers'; + +import type { Role } from '../../../../../../../common/model'; +import { KibanaPrivileges, SecuredFeature } from '../../../../model'; +import { SimplePrivilegeSection } from './simple_privilege_section'; +import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; + +const buildProps = (customProps: any = {}) => { + const features = [ + new SecuredFeature({ + id: 'feature1', + name: 'Feature 1', + app: ['app'], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: { + app: ['app'], + savedObject: { + all: ['foo'], + read: [], + }, + ui: ['app-ui'], + }, + read: { + app: ['app'], + savedObject: { + all: [], + read: [], + }, + ui: ['app-ui'], + }, + }, + }), + ] as SecuredFeature[]; + + const kibanaPrivileges = new KibanaPrivileges( + { + features: { + feature1: { + all: ['*'], + read: ['read'], + }, + }, + global: {}, + space: {}, + reserved: {}, + }, + features + ); + + const role = { + name: '', + elasticsearch: { + cluster: ['manage'], + indices: [], + run_as: [], + }, + kibana: [], + ...customProps.role, + }; + + return { + editable: true, + kibanaPrivileges, + features, + onChange: jest.fn(), + canCustomizeSubFeaturePrivileges: true, + ...customProps, + role, + }; +}; + +describe('', () => { + it('renders without crashing', () => { + expect(shallowWithIntl()).toMatchSnapshot(); + }); + + it('displays "none" when no privilege is selected', () => { + const props = buildProps(); + const wrapper = shallowWithIntl(); + const selector = wrapper.find(EuiSuperSelect); + expect(selector.props()).toMatchObject({ + valueOfSelected: 'none', + }); + expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); + }); + + it('displays "custom" when feature privileges are customized', () => { + const props = buildProps({ + role: { + elasticsearch: {}, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['foo'], + }, + }, + ], + }, + }); + const wrapper = shallowWithIntl(); + const selector = wrapper.find(EuiSuperSelect); + expect(selector.props()).toMatchObject({ + valueOfSelected: 'custom', + }); + expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); + }); + + it('displays the selected privilege', () => { + const props = buildProps({ + role: { + elasticsearch: {}, + kibana: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + ], + }, + }); + const wrapper = shallowWithIntl(); + const selector = wrapper.find(EuiSuperSelect); + expect(selector.props()).toMatchObject({ + valueOfSelected: 'read', + }); + expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); + }); + + it('displays the reserved privilege', () => { + const props = buildProps({ + role: { + elasticsearch: {}, + kibana: [ + { + spaces: ['*'], + base: [], + feature: {}, + _reserved: ['foo'], + }, + ], + }, + }); + const wrapper = shallowWithIntl(); + const selector = wrapper.find(EuiComboBox); + expect(selector.props()).toMatchObject({ + isDisabled: true, + selectedOptions: [{ label: 'foo' }], + }); + expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); + }); + + it('fires its onChange callback when the privilege changes', () => { + const props = buildProps(); + const wrapper = mountWithIntl(); + const selector = wrapper.find(EuiSuperSelect); + (selector.props() as any).onChange('all'); + + expect(props.onChange).toHaveBeenCalledWith({ + name: '', + elasticsearch: { + cluster: ['manage'], + indices: [], + run_as: [], + }, + kibana: [{ feature: {}, base: ['all'], spaces: ['*'] }], + }); + expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); + }); + + it('allows feature privileges to be customized', () => { + const props = buildProps({ + onChange: (role: Role) => { + wrapper.setProps({ + role, + }); + }, + }); + const wrapper = mountWithIntl(); + const selector = wrapper.find(EuiSuperSelect); + (selector.props() as any).onChange('custom'); + + wrapper.update(); + + const featurePrivilegeToggles = wrapper.find(EuiButtonGroup); + expect(featurePrivilegeToggles).toHaveLength(1); + expect(featurePrivilegeToggles.find('input')).toHaveLength(3); + + (featurePrivilegeToggles.props() as EuiButtonGroupProps).onChange('feature1_all', null); + + wrapper.update(); + + expect(wrapper.props().role).toEqual({ + elasticsearch: { + cluster: ['manage'], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + feature: { + feature1: ['all'], + }, + spaces: ['*'], + }, + ], + name: '', + }); + + expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); + }); + + it('renders a warning when space privileges are found', () => { + const props = buildProps({ + role: { + elasticsearch: {}, + kibana: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: ['read'], + feature: {}, + }, + ], + }, + }); + const wrapper = mountWithIntl(); + expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx new file mode 100644 index 00000000000000..f886de819e1445 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx @@ -0,0 +1,334 @@ +/* + * 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 { + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import React, { Component, Fragment } from 'react'; + +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; +import { copyRole } from '../../../../../../../common/model'; +import type { KibanaPrivileges } from '../../../../model'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; +import { FeatureTable } from '../feature_table'; +import { PrivilegeFormCalculator } from '../privilege_form_calculator'; +import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; + +interface Props { + role: Role; + kibanaPrivileges: KibanaPrivileges; + onChange: (role: Role) => void; + editable: boolean; + canCustomizeSubFeaturePrivileges: boolean; +} + +interface State { + isCustomizingGlobalPrivilege: boolean; + globalPrivsIndex: number; +} + +export class SimplePrivilegeSection extends Component { + constructor(props: Props) { + super(props); + + const globalPrivs = this.locateGlobalPrivilege(props.role); + const globalPrivsIndex = this.locateGlobalPrivilegeIndex(props.role); + + this.state = { + isCustomizingGlobalPrivilege: Boolean( + globalPrivs && Object.keys(globalPrivs.feature).length > 0 + ), + globalPrivsIndex, + }; + } + public render() { + const kibanaPrivilege = this.getDisplayedBasePrivilege(); + + const reservedPrivileges = this.props.role.kibana[this.state.globalPrivsIndex]?._reserved ?? []; + + const title = ( + + ); + + const description = ( +

+ +

+ ); + + return ( + + + + + {description} + + + + + {reservedPrivileges.length > 0 ? ( + ({ label: rp }))} + isDisabled + /> + ) : ( + + ), + dropdownDisplay: ( + <> + + + + +

+ +

+
+ + ), + }, + { + value: 'read', + inputDisplay: ( + + ), + dropdownDisplay: ( + <> + + + + +

+ +

+
+ + ), + }, + { + value: 'all', + inputDisplay: ( + + ), + dropdownDisplay: ( + <> + + + + +

+ +

+
+ + ), + }, + { + value: CUSTOM_PRIVILEGE_VALUE, + inputDisplay: ( + + ), + dropdownDisplay: ( + <> + + + + +

+ +

+
+ + ), + }, + ]} + hasDividers + valueOfSelected={kibanaPrivilege} + /> + )} +
+ {this.state.isCustomizingGlobalPrivilege && ( + + + isGlobalPrivilegeDefinition(k) + )} + canCustomizeSubFeaturePrivileges={this.props.canCustomizeSubFeaturePrivileges} + allSpacesSelected + disabled={!this.props.editable} + /> + + )} + {this.maybeRenderSpacePrivilegeWarning()} +
+
+
+ ); + } + + public getDisplayedBasePrivilege = () => { + if (this.state.isCustomizingGlobalPrivilege) { + return CUSTOM_PRIVILEGE_VALUE; + } + + const { role } = this.props; + + const form = this.locateGlobalPrivilege(role); + + return form && form.base.length > 0 ? form.base[0] : NO_PRIVILEGE_VALUE; + }; + + public onKibanaPrivilegeChange = (privilege: string) => { + const role = copyRole(this.props.role); + + const form = this.locateGlobalPrivilege(role) || this.createGlobalPrivilegeEntry(role); + + if (privilege === NO_PRIVILEGE_VALUE) { + // Remove global entry if no privilege value + role.kibana = role.kibana.filter((entry) => !isGlobalPrivilegeDefinition(entry)); + } else if (privilege === CUSTOM_PRIVILEGE_VALUE) { + // Remove base privilege if customizing feature privileges + form.base = []; + } else { + form.base = [privilege]; + form.feature = {}; + } + + this.props.onChange(role); + this.setState({ + isCustomizingGlobalPrivilege: privilege === CUSTOM_PRIVILEGE_VALUE, + globalPrivsIndex: role.kibana.indexOf(form), + }); + }; + + public onFeaturePrivilegeChange = (featureId: string, privileges: string[]) => { + const role = copyRole(this.props.role); + const form = this.locateGlobalPrivilege(role) || this.createGlobalPrivilegeEntry(role); + if (privileges.length > 0) { + form.feature[featureId] = [...privileges]; + } else { + delete form.feature[featureId]; + } + this.props.onChange(role); + }; + + private onChangeAllFeaturePrivileges = (privileges: string[]) => { + const role = copyRole(this.props.role); + + const form = this.locateGlobalPrivilege(role) || this.createGlobalPrivilegeEntry(role); + if (privileges.length > 0) { + this.props.kibanaPrivileges.getSecuredFeatures().forEach((feature) => { + form.feature[feature.id] = [...privileges]; + }); + } else { + form.feature = {}; + } + this.props.onChange(role); + }; + + private maybeRenderSpacePrivilegeWarning = () => { + const kibanaPrivileges = this.props.role.kibana; + const hasSpacePrivileges = kibanaPrivileges.some( + (privilege) => !isGlobalPrivilegeDefinition(privilege) + ); + + if (hasSpacePrivileges) { + return ( + + + + ); + } + return null; + }; + + private locateGlobalPrivilegeIndex = (role: Role) => { + return role.kibana.findIndex((privileges) => isGlobalPrivilegeDefinition(privileges)); + }; + + private locateGlobalPrivilege = (role: Role) => { + const spacePrivileges = role.kibana; + return spacePrivileges.find((privileges) => isGlobalPrivilegeDefinition(privileges)); + }; + + private createGlobalPrivilegeEntry(role: Role): RoleKibanaPrivilege { + const newEntry = { + spaces: ['*'], + base: [], + feature: {}, + }; + + role.kibana.push(newEntry); + + return newEntry; + } +} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx new file mode 100644 index 00000000000000..b3350a314349a9 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiCallOut } from '@elastic/eui'; +import React, { Component } from 'react'; + +import { FormattedMessage } from '@kbn/i18n-react'; + +export class UnsupportedSpacePrivilegesWarning extends Component<{}, {}> { + public render() { + return ; + } + + private getMessage = () => { + return ( + + ); + }; +} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index cc96446a33f2df..60887dee56d97f 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -78,7 +78,7 @@ export class SpaceAwarePrivilegeSection extends Component { public render() { const { uiCapabilities } = this.props; - if (!uiCapabilities.spaces.manage) { + if (!uiCapabilities.spaces?.manage) { return ( { data: space, loading, error, - } = useFetcher>(() => { + } = useFetcher(() => { return services.spaces?.getActiveSpace(); }, [services.spaces]); diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index e82daf4bd78139..8331b1645f2bda 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -81,7 +81,7 @@ export interface ClientPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; cases: CasesUiStart; dataViews: DataViewsPublicPluginStart; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; cloud?: CloudStart; appName: string; storage: IStorageWrapper;