mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Make spaces plugin optional (#149044)
## Summary The purpose of this PR is to make the spaces plugin an optional dependency so that it can be disabled in future offerings. In order to achieve this we are reintroducing the following config option to disable spaces: ```yaml xpack.spaces.enabled: false ``` This config option is only available in development mode while we coordinate updating the rest of our plugins. ## Scope In order to keep the scope manageable, only the following plugins have been updated as part of this PR: - `x-pack/plugins/alerting` - `x-pack/plugins/security` - `x-pack/plugins/spaces` The following plugins will need to be updated separately, by working with the corresponding teams: - `x-pack/plugins/cases` - `x-pack/plugins/enterprise_search` - `x-pack/plugins/fleet` - `x-pack/plugins/infra` - `x-pack/plugins/lens` - `x-pack/plugins/ml` - `x-pack/plugins/observability` - `x-pack/plugins/osquery` - `x-pack/plugins/synthetics` ## Screenshots ### Kibana chrome without spaces selector <img width="1073" alt="212935199-dd3bb035-b2f5-4fb6-96e9-5d2093c7992c" src="https://user-images.githubusercontent.com/190132/213432556-18e1c159-1fb1-4808-ad12-47814de575ed.png"> ### Simplified role management screen <img width="1136" alt="Screenshot 2023-01-19 at 11 21 03" src="https://user-images.githubusercontent.com/190132/213432595-0ae33abb-85e7-4044-82a2-6cc44304af0e.png">
This commit is contained in:
parent
f7bac2d971
commit
c2e538189e
20 changed files with 1205 additions and 25 deletions
|
@ -8,7 +8,10 @@
|
|||
},
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "alerting"],
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"alerting"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"actions",
|
||||
"data",
|
||||
|
@ -19,9 +22,16 @@
|
|||
"features",
|
||||
"kibanaUtils",
|
||||
"licensing",
|
||||
"spaces",
|
||||
"taskManager"
|
||||
],
|
||||
"optionalPlugins": ["usageCollection", "security", "monitoringCollection"],
|
||||
"extraPublicDirs": ["common", "common/parse_duration"]
|
||||
}
|
||||
"optionalPlugins": [
|
||||
"usageCollection",
|
||||
"security",
|
||||
"monitoringCollection",
|
||||
"spaces"
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
"common",
|
||||
"common/parse_duration"
|
||||
]
|
||||
}
|
|
@ -16,7 +16,7 @@ export interface AlertingAuthorizationClientFactoryOpts {
|
|||
ruleTypeRegistry: RuleTypeRegistry;
|
||||
securityPluginSetup?: SecurityPluginSetup;
|
||||
securityPluginStart?: SecurityPluginStart;
|
||||
getSpace: (request: KibanaRequest) => Promise<Space>;
|
||||
getSpace: (request: KibanaRequest) => Promise<Space | undefined>;
|
||||
getSpaceId: (request: KibanaRequest) => string;
|
||||
features: FeaturesPluginStart;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export class AlertingAuthorizationClientFactory {
|
|||
private ruleTypeRegistry!: RuleTypeRegistry;
|
||||
private securityPluginStart?: SecurityPluginStart;
|
||||
private features!: FeaturesPluginStart;
|
||||
private getSpace!: (request: KibanaRequest) => Promise<Space>;
|
||||
private getSpace!: (request: KibanaRequest) => Promise<Space | undefined>;
|
||||
private getSpaceId!: (request: KibanaRequest) => string;
|
||||
|
||||
public initialize(options: AlertingAuthorizationClientFactoryOpts) {
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
||||
import {
|
||||
KibanaRequest,
|
||||
|
@ -162,7 +163,7 @@ export interface AlertingPluginsStart {
|
|||
features: FeaturesPluginStart;
|
||||
eventLog: IEventLogClientService;
|
||||
licensing: LicensingPluginStart;
|
||||
spaces: SpacesPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
security?: SecurityPluginStart;
|
||||
data: DataPluginStart;
|
||||
dataViews: DataViewsPluginStart;
|
||||
|
@ -417,10 +418,10 @@ export class AlertingPlugin {
|
|||
securityPluginSetup: security,
|
||||
securityPluginStart: plugins.security,
|
||||
async getSpace(request: KibanaRequest) {
|
||||
return plugins.spaces.spacesService.getActiveSpace(request);
|
||||
return plugins.spaces?.spacesService.getActiveSpace(request);
|
||||
},
|
||||
getSpaceId(request: KibanaRequest) {
|
||||
return plugins.spaces.spacesService.getSpaceId(request);
|
||||
return plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
|
||||
},
|
||||
features: plugins.features,
|
||||
});
|
||||
|
@ -434,7 +435,7 @@ export class AlertingPlugin {
|
|||
encryptedSavedObjectsClient,
|
||||
spaceIdToNamespace,
|
||||
getSpaceId(request: KibanaRequest) {
|
||||
return plugins.spaces?.spacesService.getSpaceId(request);
|
||||
return plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
|
||||
},
|
||||
actions: plugins.actions,
|
||||
eventLog: plugins.eventLog,
|
||||
|
|
|
@ -30,8 +30,7 @@ export const registerSettings = (uiSettings: UiSettingsServiceSetup, config: Ban
|
|||
defaultMessage: 'Banner placement',
|
||||
}),
|
||||
description: i18n.translate('xpack.banners.settings.placement.description', {
|
||||
defaultMessage:
|
||||
'Display a top banner for this space, above the Elastic header. {subscriptionLink}',
|
||||
defaultMessage: 'Display a top banner above the Elastic header. {subscriptionLink}',
|
||||
values: {
|
||||
subscriptionLink,
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
@ -23,9 +25,14 @@ import { userAPIClientMock } from '../../users/index.mock';
|
|||
import { createRawKibanaPrivileges } from '../__fixtures__/kibana_privileges';
|
||||
import { indicesAPIClientMock, privilegesAPIClientMock, rolesAPIClientMock } from '../index.mock';
|
||||
import { EditRolePage } from './edit_role_page';
|
||||
import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section';
|
||||
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({
|
||||
|
@ -132,10 +139,12 @@ function getProps({
|
|||
action,
|
||||
role,
|
||||
canManageSpaces = true,
|
||||
spacesEnabled = true,
|
||||
}: {
|
||||
action: 'edit' | 'clone';
|
||||
role?: Role;
|
||||
canManageSpaces?: boolean;
|
||||
spacesEnabled?: boolean;
|
||||
}) {
|
||||
const rolesAPIClient = rolesAPIClientMock.create();
|
||||
rolesAPIClient.getRole.mockResolvedValue(role);
|
||||
|
@ -162,6 +171,9 @@ function getProps({
|
|||
const { fatalErrors } = coreMock.createSetup();
|
||||
const { http, docLinks, notifications } = coreMock.createStart();
|
||||
http.get.mockImplementation(async (path: any) => {
|
||||
if (!spacesEnabled) {
|
||||
throw { response: { status: 404 } }; // eslint-disable-line no-throw-literal
|
||||
}
|
||||
if (path === '/api/spaces/space') {
|
||||
return buildSpaces();
|
||||
}
|
||||
|
@ -183,6 +195,7 @@ function getProps({
|
|||
fatalErrors,
|
||||
uiCapabilities: buildUICapabilities(canManageSpaces),
|
||||
history: scopedHistoryMock.create(),
|
||||
spacesApiUi,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -411,6 +424,194 @@ describe('<EditRolePage />', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with spaces disabled', () => {
|
||||
it('can render readonly view when not enough privileges', async () => {
|
||||
coreStart.application.capabilities = {
|
||||
...coreStart.application.capabilities,
|
||||
roles: {
|
||||
save: false,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<EditRolePage
|
||||
{...getProps({
|
||||
action: 'edit',
|
||||
spacesEnabled: false,
|
||||
role: {
|
||||
name: 'my custom role',
|
||||
metadata: {},
|
||||
elasticsearch: { cluster: ['all'], indices: [], run_as: ['*'] },
|
||||
kibana: [{ spaces: ['*'], base: ['all'], feature: {} }],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true);
|
||||
expectReadOnlyFormButtons(wrapper);
|
||||
});
|
||||
|
||||
it('can render a reserved role', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<EditRolePage
|
||||
{...getProps({
|
||||
action: 'edit',
|
||||
spacesEnabled: false,
|
||||
role: {
|
||||
name: 'superuser',
|
||||
metadata: { _reserved: true },
|
||||
elasticsearch: { cluster: ['all'], indices: [], run_as: ['*'] },
|
||||
kibana: [{ spaces: ['*'], base: ['all'], feature: {} }],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1);
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0);
|
||||
expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true);
|
||||
expectReadOnlyFormButtons(wrapper);
|
||||
});
|
||||
|
||||
it('can render a user defined role', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<EditRolePage
|
||||
{...getProps({
|
||||
action: 'edit',
|
||||
spacesEnabled: false,
|
||||
role: {
|
||||
name: 'my custom role',
|
||||
metadata: {},
|
||||
elasticsearch: { cluster: ['all'], indices: [], run_as: ['*'] },
|
||||
kibana: [{ spaces: ['*'], base: ['all'], feature: {} }],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0);
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0);
|
||||
expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true);
|
||||
expectSaveFormButtons(wrapper);
|
||||
});
|
||||
|
||||
it('can render when creating a new role', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<EditRolePage {...getProps({ action: 'edit', spacesEnabled: false })} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0);
|
||||
expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(
|
||||
false
|
||||
);
|
||||
expectSaveFormButtons(wrapper);
|
||||
});
|
||||
|
||||
it('redirects back to roles when creating a new role without privileges', async () => {
|
||||
coreStart.application.capabilities = {
|
||||
...coreStart.application.capabilities,
|
||||
roles: {
|
||||
save: false,
|
||||
},
|
||||
};
|
||||
|
||||
const props = getProps({ action: 'edit', spacesEnabled: false });
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<EditRolePage {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(props.history.push).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
it('can render when cloning an existing role', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<EditRolePage
|
||||
{...getProps({
|
||||
action: 'edit',
|
||||
spacesEnabled: false,
|
||||
role: {
|
||||
name: '',
|
||||
metadata: { _reserved: false },
|
||||
elasticsearch: {
|
||||
cluster: ['all', 'manage'],
|
||||
indices: [
|
||||
{
|
||||
names: ['foo*'],
|
||||
privileges: ['all'],
|
||||
field_security: { except: ['f'], grant: ['b*'] },
|
||||
},
|
||||
],
|
||||
run_as: ['elastic'],
|
||||
},
|
||||
kibana: [{ spaces: ['*'], base: ['all'], feature: {} }],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0);
|
||||
expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(
|
||||
false
|
||||
);
|
||||
expectSaveFormButtons(wrapper);
|
||||
});
|
||||
|
||||
it('renders a partial read-only view when there is a transform error', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<EditRolePage
|
||||
{...getProps({
|
||||
action: 'edit',
|
||||
spacesEnabled: false,
|
||||
canManageSpaces: false,
|
||||
role: {
|
||||
name: 'my custom role',
|
||||
metadata: {},
|
||||
elasticsearch: { cluster: ['all'], indices: [], run_as: ['*'] },
|
||||
kibana: [],
|
||||
_transform_error: ['kibana'],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(TransformErrorSection)).toHaveLength(1);
|
||||
expectReadOnlyFormButtons(wrapper);
|
||||
});
|
||||
});
|
||||
|
||||
it('registers fatal error if features endpoint fails unexpectedly', async () => {
|
||||
const error = { response: { status: 500 } };
|
||||
const getFeatures = jest.fn().mockRejectedValue(error);
|
||||
|
|
|
@ -470,6 +470,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
<KibanaPrivilegesRegion
|
||||
kibanaPrivileges={new KibanaPrivileges(kibanaPrivileges, features)}
|
||||
spaces={spaces.list}
|
||||
spacesEnabled={spaces.enabled}
|
||||
uiCapabilities={uiCapabilities}
|
||||
canCustomizeSubFeaturePrivileges={license.getFeatures().allowSubFeaturePrivileges}
|
||||
editable={!isRoleReadOnly}
|
||||
|
|
|
@ -42,6 +42,21 @@ exports[`<KibanaPrivileges> 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 {},
|
||||
|
|
|
@ -8,13 +8,22 @@
|
|||
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';
|
||||
import { KibanaPrivilegesRegion } from './kibana_privileges_region';
|
||||
import { SimplePrivilegeSection } from './simple_privilege_section';
|
||||
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,12 +71,15 @@ const buildProps = () => {
|
|||
onChange: jest.fn(),
|
||||
validator: new RoleValidator(),
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
spacesEnabled: true,
|
||||
spacesApiUi,
|
||||
};
|
||||
};
|
||||
|
||||
describe('<KibanaPrivileges>', () => {
|
||||
it('renders without crashing', () => {
|
||||
expect(shallow(<KibanaPrivilegesRegion {...buildProps()} />)).toMatchSnapshot();
|
||||
const props = buildProps();
|
||||
expect(shallow(<KibanaPrivilegesRegion {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders the space-aware privilege form', () => {
|
||||
|
@ -76,6 +88,12 @@ describe('<KibanaPrivileges>', () => {
|
|||
expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders simple privilege form when spaces is disabled', () => {
|
||||
const props = buildProps();
|
||||
const wrapper = shallow(<KibanaPrivilegesRegion {...props} spacesEnabled={false} />);
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders the transform error section when the role has a transform error', () => {
|
||||
const props = buildProps();
|
||||
(props.role as Role)._transform_error = ['kibana'];
|
||||
|
|
|
@ -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<Props, {}> {
|
|||
const {
|
||||
kibanaPrivileges,
|
||||
role,
|
||||
spacesEnabled,
|
||||
canCustomizeSubFeaturePrivileges,
|
||||
spaces = [],
|
||||
uiCapabilities,
|
||||
|
@ -55,17 +58,29 @@ export class KibanaPrivilegesRegion extends Component<Props, {}> {
|
|||
return <TransformErrorSection />;
|
||||
}
|
||||
|
||||
if (spacesApiUi && spacesEnabled) {
|
||||
return (
|
||||
<SpaceAwarePrivilegeSection
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
spaces={spaces}
|
||||
uiCapabilities={uiCapabilities}
|
||||
onChange={onChange}
|
||||
editable={editable}
|
||||
canCustomizeSubFeaturePrivileges={canCustomizeSubFeaturePrivileges}
|
||||
validator={validator}
|
||||
spacesApiUi={spacesApiUi}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SpaceAwarePrivilegeSection
|
||||
<SimplePrivilegeSection
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
spaces={spaces}
|
||||
uiCapabilities={uiCapabilities}
|
||||
onChange={onChange}
|
||||
editable={editable}
|
||||
canCustomizeSubFeaturePrivileges={canCustomizeSubFeaturePrivileges}
|
||||
validator={validator}
|
||||
spacesApiUi={spacesApiUi!}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SimplePrivilegeForm> renders without crashing 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Specifies the Kibana privilege for this role."
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Kibana privileges"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"dropdownDisplay": <React.Fragment>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="None"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdown"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="No access to Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</React.Fragment>,
|
||||
"inputDisplay": <FormattedMessage
|
||||
defaultMessage="None"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"value": "none",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <React.Fragment>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Read"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdown"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grants read-only access to all of Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</React.Fragment>,
|
||||
"inputDisplay": <FormattedMessage
|
||||
defaultMessage="Read"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"value": "read",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <React.Fragment>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="All"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdown"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grants full access to all of Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</React.Fragment>,
|
||||
"inputDisplay": <FormattedMessage
|
||||
defaultMessage="All"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"value": "all",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <React.Fragment>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Custom"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdown"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Customize access to Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</React.Fragment>,
|
||||
"inputDisplay": <FormattedMessage
|
||||
defaultMessage="Custom"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"value": "custom",
|
||||
},
|
||||
]
|
||||
}
|
||||
valueOfSelected="none"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
|
@ -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';
|
|
@ -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<Props, {}> {
|
||||
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 (
|
||||
<EuiSelect
|
||||
data-test-subj={this.props['data-test-subj']}
|
||||
options={options}
|
||||
hasNoInitialSelection={!allowNone && !value}
|
||||
value={value || undefined}
|
||||
onChange={this.onChange}
|
||||
disabled={disabled}
|
||||
compressed={compressed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public onChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
}
|
|
@ -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('<SimplePrivilegeForm>', () => {
|
||||
it('renders without crashing', () => {
|
||||
expect(shallowWithIntl(<SimplePrivilegeSection {...buildProps()} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays "none" when no privilege is selected', () => {
|
||||
const props = buildProps();
|
||||
const wrapper = shallowWithIntl(<SimplePrivilegeSection {...props} />);
|
||||
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(<SimplePrivilegeSection {...props} />);
|
||||
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(<SimplePrivilegeSection {...props} />);
|
||||
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(<SimplePrivilegeSection {...props} />);
|
||||
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(<SimplePrivilegeSection {...props} />);
|
||||
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(<SimplePrivilegeSection {...props} />);
|
||||
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(<SimplePrivilegeSection {...props} />);
|
||||
expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -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<Props, State> {
|
||||
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 = (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle"
|
||||
defaultMessage="Kibana privileges"
|
||||
/>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription"
|
||||
defaultMessage="Specifies the Kibana privilege for this role."
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
{description}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label={title}>
|
||||
{reservedPrivileges.length > 0 ? (
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={reservedPrivileges.map((rp) => ({ label: rp }))}
|
||||
isDisabled
|
||||
/>
|
||||
) : (
|
||||
<EuiSuperSelect
|
||||
disabled={!this.props.editable}
|
||||
onChange={this.onKibanaPrivilegeChange}
|
||||
options={[
|
||||
{
|
||||
value: NO_PRIVILEGE_VALUE,
|
||||
inputDisplay: (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeInput"
|
||||
defaultMessage="None"
|
||||
/>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdown"
|
||||
defaultMessage="None"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdownDescription"
|
||||
defaultMessage="No access to Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'read',
|
||||
inputDisplay: (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeInput"
|
||||
defaultMessage="Read"
|
||||
/>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdown"
|
||||
defaultMessage="Read"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdownDescription"
|
||||
defaultMessage="Grants read-only access to all of Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'all',
|
||||
inputDisplay: (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeInput"
|
||||
defaultMessage="All"
|
||||
/>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdown"
|
||||
defaultMessage="All"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdownDescription"
|
||||
defaultMessage="Grants full access to all of Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: CUSTOM_PRIVILEGE_VALUE,
|
||||
inputDisplay: (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeInput"
|
||||
defaultMessage="Custom"
|
||||
/>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdown"
|
||||
defaultMessage="Custom"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdownDescription"
|
||||
defaultMessage="Customize access to Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
hasDividers
|
||||
valueOfSelected={kibanaPrivilege}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
{this.state.isCustomizingGlobalPrivilege && (
|
||||
<EuiFormRow fullWidth>
|
||||
<FeatureTable
|
||||
role={this.props.role}
|
||||
kibanaPrivileges={this.props.kibanaPrivileges}
|
||||
privilegeCalculator={
|
||||
new PrivilegeFormCalculator(this.props.kibanaPrivileges, this.props.role)
|
||||
}
|
||||
onChange={this.onFeaturePrivilegeChange}
|
||||
onChangeAll={this.onChangeAllFeaturePrivileges}
|
||||
privilegeIndex={this.props.role.kibana.findIndex((k) =>
|
||||
isGlobalPrivilegeDefinition(k)
|
||||
)}
|
||||
canCustomizeSubFeaturePrivileges={this.props.canCustomizeSubFeaturePrivileges}
|
||||
allSpacesSelected
|
||||
disabled={!this.props.editable}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{this.maybeRenderSpacePrivilegeWarning()}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<EuiFormRow fullWidth>
|
||||
<UnsupportedSpacePrivilegesWarning />
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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 <EuiCallOut iconType="alert" color="warning" title={this.getMessage()} />;
|
||||
}
|
||||
|
||||
private getMessage = () => {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.unsupportedSpacePrivilegesWarning"
|
||||
defaultMessage="This role contains privilege definitions for spaces, but spaces are not enabled in Kibana. Saving this role will remove these privileges."
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
|
@ -78,7 +78,7 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
|
|||
public render() {
|
||||
const { uiCapabilities } = this.props;
|
||||
|
||||
if (!uiCapabilities.spaces.manage) {
|
||||
if (!uiCapabilities.spaces?.manage) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
export { isReservedSpace } from './is_reserved_space';
|
||||
export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD, ENTER_SPACE_PATH } from './constants';
|
||||
export {
|
||||
MAX_SPACE_INITIALS,
|
||||
SPACE_SEARCH_COUNT_THRESHOLD,
|
||||
ENTER_SPACE_PATH,
|
||||
DEFAULT_SPACE_ID,
|
||||
} from './constants';
|
||||
export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser';
|
||||
export type {
|
||||
Space,
|
||||
|
|
56
x-pack/plugins/spaces/server/config.test.ts
Normal file
56
x-pack/plugins/spaces/server/config.test.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('crypto', () => ({
|
||||
randomBytes: jest.fn(),
|
||||
constants: jest.requireActual('crypto').constants,
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/utils', () => ({
|
||||
getLogsPath: () => '/mock/kibana/logs/path',
|
||||
}));
|
||||
|
||||
import { ConfigSchema } from './config';
|
||||
|
||||
describe('config schema', () => {
|
||||
it('generates proper defaults', () => {
|
||||
expect(ConfigSchema.validate({})).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"enabled": true,
|
||||
"maxSpaces": 1000,
|
||||
}
|
||||
`);
|
||||
|
||||
expect(ConfigSchema.validate({}, { dev: false })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"enabled": true,
|
||||
"maxSpaces": 1000,
|
||||
}
|
||||
`);
|
||||
|
||||
expect(ConfigSchema.validate({}, { dev: true })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"enabled": true,
|
||||
"maxSpaces": 1000,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw error if spaces is disabled', () => {
|
||||
expect(() => ConfigSchema.validate({ enabled: false })).toThrow(
|
||||
'[enabled]: Spaces can only be disabled in development mode'
|
||||
);
|
||||
|
||||
expect(() => ConfigSchema.validate({ enabled: false }, { dev: false })).toThrow(
|
||||
'[enabled]: Spaces can only be disabled in development mode'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw error if spaces is disabled in development mode', () => {
|
||||
expect(() => ConfigSchema.validate({ enabled: false }, { dev: true })).not.toThrow();
|
||||
});
|
||||
});
|
|
@ -12,6 +12,19 @@ import { schema } from '@kbn/config-schema';
|
|||
import type { PluginInitializerContext } from '@kbn/core/server';
|
||||
|
||||
export const ConfigSchema = schema.object({
|
||||
enabled: schema.conditional(
|
||||
schema.contextRef('dev'),
|
||||
true,
|
||||
schema.boolean({ defaultValue: true }),
|
||||
schema.boolean({
|
||||
validate: (rawValue) => {
|
||||
if (rawValue === false) {
|
||||
return 'Spaces can only be disabled in development mode';
|
||||
}
|
||||
},
|
||||
defaultValue: true,
|
||||
})
|
||||
),
|
||||
maxSpaces: schema.number({ defaultValue: 1000 }),
|
||||
});
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const createMockDebugLogger = () => {
|
|||
return jest.fn();
|
||||
};
|
||||
|
||||
const createMockConfig = (mockConfig: ConfigType = { maxSpaces: 1000 }) => {
|
||||
const createMockConfig = (mockConfig: ConfigType = { enabled: true, maxSpaces: 1000 }) => {
|
||||
return ConfigSchema.validate(mockConfig);
|
||||
};
|
||||
|
||||
|
@ -75,7 +75,7 @@ describe('#getAll', () => {
|
|||
mockCallWithRequestRepository.find.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
} as any);
|
||||
const mockConfig = createMockConfig({ maxSpaces: 1234 });
|
||||
const mockConfig = createMockConfig({ enabled: true, maxSpaces: 1234 });
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
|
||||
const actualSpaces = await client.getAll();
|
||||
|
@ -182,7 +182,7 @@ describe('#create', () => {
|
|||
total: maxSpaces - 1,
|
||||
} as any);
|
||||
|
||||
const mockConfig = createMockConfig({ maxSpaces });
|
||||
const mockConfig = createMockConfig({ enabled: true, maxSpaces });
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
|
||||
|
||||
|
@ -208,7 +208,7 @@ describe('#create', () => {
|
|||
total: maxSpaces,
|
||||
} as any);
|
||||
|
||||
const mockConfig = createMockConfig({ maxSpaces });
|
||||
const mockConfig = createMockConfig({ enabled: true, maxSpaces });
|
||||
|
||||
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue