mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Fix saved object share UI bugs regarding read-only privileges (#81828)
This commit is contained in:
parent
286dbcae3a
commit
fb1c7d7048
28 changed files with 1370 additions and 940 deletions
|
@ -11,6 +11,20 @@ experimental[] Retrieve all {kib} spaces.
|
|||
|
||||
`GET <kibana host>:<port>/api/spaces/space`
|
||||
|
||||
[[spaces-api-get-all-query-params]]
|
||||
==== Query parameters
|
||||
|
||||
`purpose`::
|
||||
(Optional, string) Valid options include `any`, `copySavedObjectsIntoSpace`, and `shareSavedObjectsIntoSpace`. This determines what
|
||||
authorization checks are applied to the API call. If `purpose` is not provided in the URL, the `any` purpose is used.
|
||||
|
||||
`include_authorized_purposes`::
|
||||
(Optional, boolean) When enabled, the API will return any spaces that the user is authorized to access in any capacity, and each space
|
||||
will contain the purpose(s) for which the user is authorized. This can be useful to determine which spaces a user can read but not take a
|
||||
specific action in. If the Security plugin is not enabled, this will have no effect, as no authorization checks would take place.
|
||||
+
|
||||
NOTE: This option cannot be used in conjunction with `purpose`.
|
||||
|
||||
[[spaces-api-get-all-response-codes]]
|
||||
==== Response code
|
||||
|
||||
|
@ -18,7 +32,17 @@ experimental[] Retrieve all {kib} spaces.
|
|||
Indicates a successful call.
|
||||
|
||||
[[spaces-api-get-all-example]]
|
||||
==== Example
|
||||
==== Examples
|
||||
|
||||
[[spaces-api-get-all-example-1]]
|
||||
===== Default options
|
||||
|
||||
Retrieve all spaces without specifying any options:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X GET api/spaces/space
|
||||
--------------------------------------------------
|
||||
|
||||
The API returns the following:
|
||||
|
||||
|
@ -51,3 +75,63 @@ The API returns the following:
|
|||
}
|
||||
]
|
||||
--------------------------------------------------
|
||||
|
||||
[[spaces-api-get-all-example-2]]
|
||||
===== Custom options
|
||||
|
||||
The user has read-only access to the Sales space. Retrieve all spaces and specify options:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X GET api/spaces/space?purpose=shareSavedObjectsIntoSpace&include_authorized_purposes=true
|
||||
--------------------------------------------------
|
||||
|
||||
The API returns the following:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
[
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"description" : "This is the Default Space",
|
||||
"disabledFeatures": [],
|
||||
"imageUrl": "",
|
||||
"_reserved": true,
|
||||
"authorizedPurposes": {
|
||||
"any": true,
|
||||
"copySavedObjectsIntoSpace": true,
|
||||
"findSavedObjects": true,
|
||||
"shareSavedObjectsIntoSpace": true,
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "marketing",
|
||||
"name": "Marketing",
|
||||
"description" : "This is the Marketing Space",
|
||||
"color": "#aabbcc",
|
||||
"disabledFeatures": ["apm"],
|
||||
"initials": "MK",
|
||||
"imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSU",
|
||||
"authorizedPurposes": {
|
||||
"any": true,
|
||||
"copySavedObjectsIntoSpace": true,
|
||||
"findSavedObjects": true,
|
||||
"shareSavedObjectsIntoSpace": true,
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sales",
|
||||
"name": "Sales",
|
||||
"initials": "MK",
|
||||
"disabledFeatures": ["discover", "timelion"],
|
||||
"imageUrl": "",
|
||||
"authorizedPurposes": {
|
||||
"any": true,
|
||||
"copySavedObjectsIntoSpace": false,
|
||||
"findSavedObjects": true,
|
||||
"shareSavedObjectsIntoSpace": false,
|
||||
}
|
||||
}
|
||||
]
|
||||
--------------------------------------------------
|
||||
|
|
|
@ -266,8 +266,19 @@ exports[`SavedObjectsTable should render normally 1`] = `
|
|||
"serverBasePath": "",
|
||||
}
|
||||
}
|
||||
canDelete={false}
|
||||
canGoInApp={[Function]}
|
||||
capabilities={
|
||||
Object {
|
||||
"catalogue": Object {},
|
||||
"management": Object {},
|
||||
"navLinks": Object {},
|
||||
"savedObjectsManagement": Object {
|
||||
"delete": false,
|
||||
"edit": false,
|
||||
"read": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
columnRegistry={
|
||||
Object {
|
||||
"getAll": [MockFunction],
|
||||
|
|
|
@ -24,7 +24,6 @@ import { keys } from '@elastic/eui';
|
|||
import { httpServiceMock } from '../../../../../../core/public/mocks';
|
||||
import { actionServiceMock } from '../../../services/action_service.mock';
|
||||
import { columnServiceMock } from '../../../services/column_service.mock';
|
||||
import { SavedObjectsManagementAction } from '../../..';
|
||||
import { Table, TableProps } from './table';
|
||||
|
||||
const defaultProps: TableProps = {
|
||||
|
@ -82,7 +81,7 @@ const defaultProps: TableProps = {
|
|||
onTableChange: () => {},
|
||||
isSearching: false,
|
||||
onShowRelationships: () => {},
|
||||
canDelete: true,
|
||||
capabilities: { savedObjectsManagement: { delete: true } } as any,
|
||||
};
|
||||
|
||||
describe('Table', () => {
|
||||
|
@ -121,7 +120,11 @@ describe('Table', () => {
|
|||
{ type: 'search' },
|
||||
{ type: 'index-pattern' },
|
||||
] as any;
|
||||
const customizedProps = { ...defaultProps, selectedSavedObjects, canDelete: false };
|
||||
const customizedProps = {
|
||||
...defaultProps,
|
||||
selectedSavedObjects,
|
||||
capabilities: { savedObjectsManagement: { delete: false } } as any,
|
||||
};
|
||||
const component = shallowWithI18nProvider(<Table {...customizedProps} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
|
@ -137,7 +140,8 @@ describe('Table', () => {
|
|||
refreshOnFinish: () => true,
|
||||
euiAction: { name: 'foo', description: 'bar', icon: 'beaker', type: 'icon' },
|
||||
registerOnFinishCallback: (callback: Function) => callback(), // call the callback immediately for this test
|
||||
} as SavedObjectsManagementAction,
|
||||
setActionContext: () => null,
|
||||
} as any,
|
||||
]);
|
||||
const onActionRefresh = jest.fn();
|
||||
const customizedProps = { ...defaultProps, actionRegistry, onActionRefresh };
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { IBasePath } from 'src/core/public';
|
||||
import { ApplicationStart, IBasePath } from 'src/core/public';
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import {
|
||||
EuiSearchBar,
|
||||
|
@ -57,7 +57,7 @@ export interface TableProps {
|
|||
onSelectionChange: (selection: SavedObjectWithMetadata[]) => void;
|
||||
};
|
||||
filterOptions: any[];
|
||||
canDelete: boolean;
|
||||
capabilities: ApplicationStart['capabilities'];
|
||||
onDelete: () => void;
|
||||
onActionRefresh: (object: SavedObjectWithMetadata) => void;
|
||||
onExport: (includeReferencesDeep: boolean) => void;
|
||||
|
@ -156,6 +156,7 @@ export class Table extends PureComponent<TableProps, TableState> {
|
|||
isSearching,
|
||||
filterOptions,
|
||||
selectionConfig: selection,
|
||||
capabilities,
|
||||
onDelete,
|
||||
onActionRefresh,
|
||||
selectedSavedObjects,
|
||||
|
@ -285,6 +286,7 @@ export class Table extends PureComponent<TableProps, TableState> {
|
|||
'data-test-subj': 'savedObjectsTableAction-relationships',
|
||||
},
|
||||
...actionRegistry.getAll().map((action) => {
|
||||
action.setActionContext({ capabilities });
|
||||
return {
|
||||
...action.euiAction,
|
||||
'data-test-subj': `savedObjectsTableAction-${action.id}`,
|
||||
|
@ -354,9 +356,11 @@ export class Table extends PureComponent<TableProps, TableState> {
|
|||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={onDelete}
|
||||
isDisabled={selectedSavedObjects.length === 0 || !this.props.canDelete}
|
||||
isDisabled={
|
||||
selectedSavedObjects.length === 0 || !capabilities.savedObjectsManagement.delete
|
||||
}
|
||||
title={
|
||||
this.props.canDelete
|
||||
capabilities.savedObjectsManagement.delete
|
||||
? undefined
|
||||
: i18n.translate('savedObjectsManagement.objectsTable.table.deleteButtonTitle', {
|
||||
defaultMessage: 'Unable to delete saved objects',
|
||||
|
|
|
@ -807,7 +807,7 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
|
|||
onTableChange={this.onTableChange}
|
||||
filterOptions={filterOptions}
|
||||
onExport={this.onExport}
|
||||
canDelete={applications.capabilities.savedObjectsManagement.delete as boolean}
|
||||
capabilities={applications.capabilities}
|
||||
onDelete={this.onDelete}
|
||||
onActionRefresh={this.refreshObject}
|
||||
goInspectObject={this.props.goInspectObject}
|
||||
|
|
|
@ -18,8 +18,13 @@
|
|||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { SavedObjectsManagementRecord } from '.';
|
||||
|
||||
interface ActionContext {
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
export abstract class SavedObjectsManagementAction {
|
||||
public abstract render: () => ReactNode;
|
||||
public abstract id: string;
|
||||
|
@ -37,8 +42,13 @@ export abstract class SavedObjectsManagementAction {
|
|||
|
||||
private callbacks: Function[] = [];
|
||||
|
||||
protected actionContext: ActionContext | null = null;
|
||||
protected record: SavedObjectsManagementRecord | null = null;
|
||||
|
||||
public setActionContext(actionContext: ActionContext) {
|
||||
this.actionContext = actionContext;
|
||||
}
|
||||
|
||||
public registerOnFinishCallback(callback: Function) {
|
||||
this.callbacks.push(callback);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type GetSpacePurpose =
|
||||
import { Space } from './space';
|
||||
|
||||
export type GetAllSpacesPurpose =
|
||||
| 'any'
|
||||
| 'copySavedObjectsIntoSpace'
|
||||
| 'findSavedObjects'
|
||||
| 'shareSavedObjectsIntoSpace';
|
||||
|
||||
export interface GetSpaceResult extends Space {
|
||||
authorizedPurposes?: Record<GetAllSpacesPurpose, boolean>;
|
||||
}
|
||||
|
|
|
@ -61,13 +61,16 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
}
|
||||
);
|
||||
useEffect(() => {
|
||||
const getSpaces = spacesManager.getSpaces('copySavedObjectsIntoSpace');
|
||||
const getSpaces = spacesManager.getSpaces({ includeAuthorizedPurposes: true });
|
||||
const getActiveSpace = spacesManager.getActiveSpace();
|
||||
Promise.all([getSpaces, getActiveSpace])
|
||||
.then(([allSpaces, activeSpace]) => {
|
||||
setSpacesState({
|
||||
isLoading: false,
|
||||
spaces: allSpaces.filter((space) => space.id !== activeSpace.id),
|
||||
spaces: allSpaces.filter(
|
||||
({ id, authorizedPurposes }) =>
|
||||
id !== activeSpace.id && authorizedPurposes?.copySavedObjectsIntoSpace !== false
|
||||
),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
|
@ -8,6 +8,8 @@ import { SpacesPlugin } from './plugin';
|
|||
|
||||
export { Space } from '../common/model/space';
|
||||
|
||||
export { GetSpaceResult } from '../common/model/types';
|
||||
|
||||
export { SpaceAvatar, getSpaceColor, getSpaceImageUrl, getSpaceInitials } from './space_avatar';
|
||||
|
||||
export { SpacesPluginSetup, SpacesPluginStart } from './plugin';
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, PropsWithChildren } from 'react';
|
||||
import { StartServicesAccessor, CoreStart } from 'src/core/public';
|
||||
import { createKibanaReactContext } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { PluginsStart } from '../../plugin';
|
||||
|
||||
interface Props {
|
||||
getStartServices: StartServicesAccessor<PluginsStart>;
|
||||
}
|
||||
|
||||
export const ContextWrapper = (props: PropsWithChildren<Props>) => {
|
||||
const { getStartServices, children } = props;
|
||||
|
||||
const [coreStart, setCoreStart] = useState<CoreStart>();
|
||||
|
||||
useEffect(() => {
|
||||
getStartServices().then((startServices) => {
|
||||
const [coreStartValue] = startServices;
|
||||
setCoreStart(coreStartValue);
|
||||
});
|
||||
}, [getStartServices]);
|
||||
|
||||
if (!coreStart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { application, docLinks } = coreStart;
|
||||
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
|
||||
application,
|
||||
docLinks,
|
||||
});
|
||||
|
||||
return <KibanaReactContextProvider>{children}</KibanaReactContextProvider>;
|
||||
};
|
|
@ -4,4 +4,5 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ContextWrapper } from './context_wrapper';
|
||||
export { ShareSavedObjectsToSpaceFlyout } from './share_to_space_flyout';
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiLink,
|
||||
EuiSelectable,
|
||||
EuiSelectableOption,
|
||||
|
@ -35,6 +36,26 @@ interface Props {
|
|||
type SpaceOption = EuiSelectableOption & { ['data-space-id']: string };
|
||||
|
||||
const ROW_HEIGHT = 40;
|
||||
const partiallyAuthorizedTooltip = {
|
||||
checked: i18n.translate(
|
||||
'xpack.spaces.management.shareToSpace.partiallyAuthorizedSpaceTooltip.checked',
|
||||
{ defaultMessage: 'You need additional privileges to deselect this space.' }
|
||||
),
|
||||
unchecked: i18n.translate(
|
||||
'xpack.spaces.management.shareToSpace.partiallyAuthorizedSpaceTooltip.unchecked',
|
||||
{ defaultMessage: 'You need additional privileges to select this space.' }
|
||||
),
|
||||
};
|
||||
const partiallyAuthorizedSpaceProps = (checked: boolean) => ({
|
||||
append: (
|
||||
<EuiIconTip
|
||||
content={checked ? partiallyAuthorizedTooltip.checked : partiallyAuthorizedTooltip.unchecked}
|
||||
position="left"
|
||||
type="iInCircle"
|
||||
/>
|
||||
),
|
||||
disabled: true,
|
||||
});
|
||||
const activeSpaceProps = {
|
||||
append: <EuiBadge color="hollow">Current</EuiBadge>,
|
||||
disabled: true,
|
||||
|
@ -46,22 +67,27 @@ export const SelectableSpacesControl = (props: Props) => {
|
|||
const { services } = useKibana();
|
||||
const { application, docLinks } = services;
|
||||
|
||||
const activeSpaceId = spaces.find((space) => space.isActiveSpace)!.id;
|
||||
const isGlobalControlChecked = selectedSpaceIds.includes(ALL_SPACES_ID);
|
||||
const options = spaces
|
||||
.sort((a, b) => (a.isActiveSpace ? -1 : b.isActiveSpace ? 1 : 0))
|
||||
.map<SpaceOption>((space) => ({
|
||||
label: space.name,
|
||||
prepend: <SpaceAvatar space={space} size={'s'} />,
|
||||
checked: selectedSpaceIds.includes(space.id) ? 'on' : undefined,
|
||||
['data-space-id']: space.id,
|
||||
['data-test-subj']: `sts-space-selector-row-${space.id}`,
|
||||
...(space.isActiveSpace ? activeSpaceProps : {}),
|
||||
...(isGlobalControlChecked && { disabled: true }),
|
||||
}));
|
||||
.map<SpaceOption>((space) => {
|
||||
const checked = selectedSpaceIds.includes(space.id);
|
||||
return {
|
||||
label: space.name,
|
||||
prepend: <SpaceAvatar space={space} size={'s'} />,
|
||||
checked: checked ? 'on' : undefined,
|
||||
['data-space-id']: space.id,
|
||||
['data-test-subj']: `sts-space-selector-row-${space.id}`,
|
||||
...(isGlobalControlChecked && { disabled: true }),
|
||||
...(space.isPartiallyAuthorized && partiallyAuthorizedSpaceProps(checked)),
|
||||
...(space.isActiveSpace && activeSpaceProps),
|
||||
};
|
||||
});
|
||||
|
||||
function updateSelectedSpaces(spaceOptions: SpaceOption[]) {
|
||||
const selectedOptions = spaceOptions
|
||||
.filter(({ checked, disabled }) => checked && !disabled)
|
||||
.filter((x) => x.checked && x['data-space-id'] !== activeSpaceId)
|
||||
.map((x) => x['data-space-id']);
|
||||
const updatedSpaceIds = [
|
||||
...selectedOptions,
|
||||
|
|
|
@ -8,7 +8,7 @@ import Boom from '@hapi/boom';
|
|||
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { ShareSavedObjectsToSpaceFlyout } from './share_to_space_flyout';
|
||||
import { ShareToSpaceForm } from './share_to_space_form';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { EuiLoadingSpinner, EuiSelectable } from '@elastic/eui';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { SelectableSpacesControl } from './selectable_spaces_control';
|
||||
|
@ -21,6 +21,7 @@ import { EuiCallOut } from '@elastic/eui';
|
|||
import { CopySavedObjectsToSpaceFlyout } from '../../copy_saved_objects_to_space/components';
|
||||
import { NoSpacesAvailable } from './no_spaces_available';
|
||||
import { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public';
|
||||
import { ContextWrapper } from '.';
|
||||
|
||||
interface SetupOpts {
|
||||
mockSpaces?: Space[];
|
||||
|
@ -93,17 +94,26 @@ const setup = async (opts: SetupOpts = {}) => {
|
|||
};
|
||||
getStartServices.mockResolvedValue([startServices, , ,]);
|
||||
|
||||
// the flyout depends upon the Kibana React Context, and it cannot be used without the context wrapper
|
||||
// the context wrapper is only split into a separate component to avoid recreating the context upon every flyout state change
|
||||
const wrapper = mountWithIntl(
|
||||
<ShareSavedObjectsToSpaceFlyout
|
||||
savedObject={savedObjectToShare}
|
||||
spacesManager={(mockSpacesManager as unknown) as SpacesManager}
|
||||
toastNotifications={(mockToastNotifications as unknown) as ToastsApi}
|
||||
onClose={onClose}
|
||||
onObjectUpdated={onObjectUpdated}
|
||||
getStartServices={getStartServices}
|
||||
/>
|
||||
<ContextWrapper getStartServices={getStartServices}>
|
||||
<ShareSavedObjectsToSpaceFlyout
|
||||
savedObject={savedObjectToShare}
|
||||
spacesManager={(mockSpacesManager as unknown) as SpacesManager}
|
||||
toastNotifications={(mockToastNotifications as unknown) as ToastsApi}
|
||||
onClose={onClose}
|
||||
onObjectUpdated={onObjectUpdated}
|
||||
/>
|
||||
</ContextWrapper>
|
||||
);
|
||||
|
||||
// wait for context wrapper to rerender
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
if (!opts.returnBeforeSpacesLoad) {
|
||||
// Wait for spaces manager to complete and flyout to rerender
|
||||
await act(async () => {
|
||||
|
@ -383,4 +393,95 @@ describe('ShareToSpaceFlyout', () => {
|
|||
expect(mockToastNotifications.addError).not.toHaveBeenCalled();
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('space selection', () => {
|
||||
const mockSpaces = [
|
||||
{
|
||||
// normal "fully authorized" space selection option -- not the active space
|
||||
id: 'space-1',
|
||||
name: 'Space 1',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
// "partially authorized" space selection option -- not the active space
|
||||
id: 'space-2',
|
||||
name: 'Space 2',
|
||||
disabledFeatures: [],
|
||||
authorizedPurposes: { shareSavedObjectsIntoSpace: false },
|
||||
},
|
||||
{
|
||||
// "active space" selection option (determined by an ID that matches the result of `getActiveSpace`, mocked at top)
|
||||
id: 'my-active-space',
|
||||
name: 'my active space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
];
|
||||
|
||||
const expectActiveSpace = (option: any) => {
|
||||
expect(option.append).toMatchInlineSnapshot(`
|
||||
<EuiBadge
|
||||
color="hollow"
|
||||
>
|
||||
Current
|
||||
</EuiBadge>
|
||||
`);
|
||||
// by definition, the active space will always be checked
|
||||
expect(option.checked).toEqual('on');
|
||||
expect(option.disabled).toEqual(true);
|
||||
};
|
||||
const expectInactiveSpace = (option: any, checked: boolean) => {
|
||||
expect(option.append).toBeUndefined();
|
||||
expect(option.checked).toEqual(checked ? 'on' : undefined);
|
||||
expect(option.disabled).toBeUndefined();
|
||||
};
|
||||
const expectPartiallyAuthorizedSpace = (option: any, checked: boolean) => {
|
||||
if (checked) {
|
||||
expect(option.append).toMatchInlineSnapshot(`
|
||||
<EuiIconTip
|
||||
content="You need additional privileges to deselect this space."
|
||||
position="left"
|
||||
type="iInCircle"
|
||||
/>
|
||||
`);
|
||||
} else {
|
||||
expect(option.append).toMatchInlineSnapshot(`
|
||||
<EuiIconTip
|
||||
content="You need additional privileges to select this space."
|
||||
position="left"
|
||||
type="iInCircle"
|
||||
/>
|
||||
`);
|
||||
}
|
||||
expect(option.checked).toEqual(checked ? 'on' : undefined);
|
||||
expect(option.disabled).toEqual(true);
|
||||
};
|
||||
|
||||
it('correctly defines space selection options when spaces are not selected', async () => {
|
||||
const namespaces = ['my-active-space']; // the saved object's current namespaces; it will always exist in at least the active namespace
|
||||
const { wrapper } = await setup({ mockSpaces, namespaces });
|
||||
|
||||
const selectable = wrapper.find(SelectableSpacesControl).find(EuiSelectable);
|
||||
const selectOptions = selectable.prop('options');
|
||||
expect(selectOptions[0]['data-space-id']).toEqual('my-active-space');
|
||||
expectActiveSpace(selectOptions[0]);
|
||||
expect(selectOptions[1]['data-space-id']).toEqual('space-1');
|
||||
expectInactiveSpace(selectOptions[1], false);
|
||||
expect(selectOptions[2]['data-space-id']).toEqual('space-2');
|
||||
expectPartiallyAuthorizedSpace(selectOptions[2], false);
|
||||
});
|
||||
|
||||
it('correctly defines space selection options when spaces are selected', async () => {
|
||||
const namespaces = ['my-active-space', 'space-1', 'space-2']; // the saved object's current namespaces
|
||||
const { wrapper } = await setup({ mockSpaces, namespaces });
|
||||
|
||||
const selectable = wrapper.find(SelectableSpacesControl).find(EuiSelectable);
|
||||
const selectOptions = selectable.prop('options');
|
||||
expect(selectOptions[0]['data-space-id']).toEqual('my-active-space');
|
||||
expectActiveSpace(selectOptions[0]);
|
||||
expect(selectOptions[1]['data-space-id']).toEqual('space-1');
|
||||
expectInactiveSpace(selectOptions[1], true);
|
||||
expect(selectOptions[2]['data-space-id']).toEqual('space-2');
|
||||
expectPartiallyAuthorizedSpace(selectOptions[2], true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,16 +21,14 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ToastsStart, StartServicesAccessor, CoreStart } from 'src/core/public';
|
||||
import { ToastsStart } from 'src/core/public';
|
||||
import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public';
|
||||
import { createKibanaReactContext } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { ALL_SPACES_ID, UNKNOWN_SPACE } from '../../../common/constants';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { GetSpaceResult } from '../../../common/model/types';
|
||||
import { SpacesManager } from '../../spaces_manager';
|
||||
import { ShareToSpaceForm } from './share_to_space_form';
|
||||
import { ShareOptions, SpaceTarget } from '../types';
|
||||
import { CopySavedObjectsToSpaceFlyout } from '../../copy_saved_objects_to_space/components';
|
||||
import { PluginsStart } from '../../plugin';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
@ -38,23 +36,14 @@ interface Props {
|
|||
savedObject: SavedObjectsManagementRecord;
|
||||
spacesManager: SpacesManager;
|
||||
toastNotifications: ToastsStart;
|
||||
getStartServices: StartServicesAccessor<PluginsStart>;
|
||||
}
|
||||
|
||||
const arraysAreEqual = (a: unknown[], b: unknown[]) =>
|
||||
a.every((x) => b.includes(x)) && b.every((x) => a.includes(x));
|
||||
|
||||
export const ShareSavedObjectsToSpaceFlyout = (props: Props) => {
|
||||
const {
|
||||
getStartServices,
|
||||
onClose,
|
||||
onObjectUpdated,
|
||||
savedObject,
|
||||
spacesManager,
|
||||
toastNotifications,
|
||||
} = props;
|
||||
const { onClose, onObjectUpdated, savedObject, spacesManager, toastNotifications } = props;
|
||||
const { namespaces: currentNamespaces = [] } = savedObject;
|
||||
const [coreStart, setCoreStart] = useState<CoreStart>();
|
||||
const [shareOptions, setShareOptions] = useState<ShareOptions>({ selectedSpaceIds: [] });
|
||||
const [canShareToAllSpaces, setCanShareToAllSpaces] = useState<boolean>(false);
|
||||
const [showMakeCopy, setShowMakeCopy] = useState<boolean>(false);
|
||||
|
@ -64,20 +53,19 @@ export const ShareSavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
spaces: SpaceTarget[];
|
||||
}>({ isLoading: true, spaces: [] });
|
||||
useEffect(() => {
|
||||
const getSpaces = spacesManager.getSpaces('shareSavedObjectsIntoSpace');
|
||||
const getSpaces = spacesManager.getSpaces({ includeAuthorizedPurposes: true });
|
||||
const getActiveSpace = spacesManager.getActiveSpace();
|
||||
const getPermissions = spacesManager.getShareSavedObjectPermissions(savedObject.type);
|
||||
Promise.all([getSpaces, getActiveSpace, getPermissions, getStartServices()])
|
||||
.then(([allSpaces, activeSpace, permissions, startServices]) => {
|
||||
const [coreStartValue] = startServices;
|
||||
setCoreStart(coreStartValue);
|
||||
Promise.all([getSpaces, getActiveSpace, getPermissions])
|
||||
.then(([allSpaces, activeSpace, permissions]) => {
|
||||
setShareOptions({
|
||||
selectedSpaceIds: currentNamespaces.filter((spaceId) => spaceId !== activeSpace.id),
|
||||
});
|
||||
setCanShareToAllSpaces(permissions.shareToAllSpaces);
|
||||
const createSpaceTarget = (space: Space): SpaceTarget => ({
|
||||
const createSpaceTarget = (space: GetSpaceResult): SpaceTarget => ({
|
||||
...space,
|
||||
isActiveSpace: space.id === activeSpace.id,
|
||||
isPartiallyAuthorized: space.authorizedPurposes?.shareSavedObjectsIntoSpace === false,
|
||||
});
|
||||
setSpacesState({
|
||||
isLoading: false,
|
||||
|
@ -91,7 +79,7 @@ export const ShareSavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
}),
|
||||
});
|
||||
});
|
||||
}, [currentNamespaces, spacesManager, savedObject, toastNotifications, getStartServices]);
|
||||
}, [currentNamespaces, spacesManager, savedObject, toastNotifications]);
|
||||
|
||||
const getSelectionChanges = () => {
|
||||
const activeSpace = spaces.find((space) => space.isActiveSpace);
|
||||
|
@ -208,23 +196,16 @@ export const ShareSavedObjectsToSpaceFlyout = (props: Props) => {
|
|||
const activeSpace = spaces.find((x) => x.isActiveSpace)!;
|
||||
const showShareWarning =
|
||||
spaces.length > 1 && arraysAreEqual(currentNamespaces, [activeSpace.id]);
|
||||
const { application, docLinks } = coreStart!;
|
||||
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
|
||||
application,
|
||||
docLinks,
|
||||
});
|
||||
// Step 2: Share has not been initiated yet; User must fill out form to continue.
|
||||
return (
|
||||
<KibanaReactContextProvider>
|
||||
<ShareToSpaceForm
|
||||
spaces={spaces}
|
||||
shareOptions={shareOptions}
|
||||
onUpdate={setShareOptions}
|
||||
showShareWarning={showShareWarning}
|
||||
canShareToAllSpaces={canShareToAllSpaces}
|
||||
makeCopy={() => setShowMakeCopy(true)}
|
||||
/>
|
||||
</KibanaReactContextProvider>
|
||||
<ShareToSpaceForm
|
||||
spaces={spaces}
|
||||
shareOptions={shareOptions}
|
||||
onUpdate={setShareOptions}
|
||||
showShareWarning={showShareWarning}
|
||||
canShareToAllSpaces={canShareToAllSpaces}
|
||||
makeCopy={() => setShowMakeCopy(true)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { coreMock, notificationServiceMock } from 'src/core/public/mocks';
|
||||
import { SavedObjectsManagementRecord } from '../../../../../src/plugins/saved_objects_management/public';
|
||||
import { spacesManagerMock } from '../spaces_manager/mocks';
|
||||
import { ShareToSpaceSavedObjectsManagementAction } from './share_saved_objects_to_space_action';
|
||||
|
||||
describe('ShareToSpaceSavedObjectsManagementAction', () => {
|
||||
const createAction = () => {
|
||||
const spacesManager = spacesManagerMock.create();
|
||||
const notificationsStart = notificationServiceMock.createStartContract();
|
||||
const { getStartServices } = coreMock.createSetup();
|
||||
return new ShareToSpaceSavedObjectsManagementAction(
|
||||
spacesManager,
|
||||
notificationsStart,
|
||||
getStartServices
|
||||
);
|
||||
};
|
||||
describe('#euiAction.available', () => {
|
||||
describe('with an object type that has a namespaceType of "multiple"', () => {
|
||||
const object = { meta: { namespaceType: 'multiple' } } as SavedObjectsManagementRecord;
|
||||
|
||||
it(`is available when UI capabilities are not set`, () => {
|
||||
const action = createAction();
|
||||
expect(action.euiAction.available(object)).toBe(true);
|
||||
});
|
||||
|
||||
it(`is available when UI capabilities are set and shareIntoSpace is enabled`, () => {
|
||||
const action = createAction();
|
||||
const capabilities: any = { savedObjectsManagement: { shareIntoSpace: true } };
|
||||
action.setActionContext({ capabilities });
|
||||
expect(action.euiAction.available(object)).toBe(true);
|
||||
});
|
||||
|
||||
it(`is not available when UI capabilities are set and shareIntoSpace is disabled`, () => {
|
||||
const action = createAction();
|
||||
const capabilities: any = { savedObjectsManagement: { shareIntoSpace: false } };
|
||||
action.setActionContext({ capabilities });
|
||||
expect(action.euiAction.available(object)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an object type that does not have a namespaceType of "multiple"', () => {
|
||||
const object = { meta: { namespaceType: 'single' } } as SavedObjectsManagementRecord;
|
||||
|
||||
it(`is not available when UI capabilities are not set`, () => {
|
||||
const action = createAction();
|
||||
expect(action.euiAction.available(object)).toBe(false);
|
||||
});
|
||||
|
||||
it(`is not available when UI capabilities are set and shareIntoSpace is enabled`, () => {
|
||||
const action = createAction();
|
||||
const capabilities: any = { savedObjectsManagement: { shareIntoSpace: true } };
|
||||
action.setActionContext({ capabilities });
|
||||
expect(action.euiAction.available(object)).toBe(false);
|
||||
});
|
||||
|
||||
it(`is not available when UI capabilities are set and shareIntoSpace is disabled`, () => {
|
||||
const action = createAction();
|
||||
const capabilities: any = { savedObjectsManagement: { shareIntoSpace: false } };
|
||||
action.setActionContext({ capabilities });
|
||||
expect(action.euiAction.available(object)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,7 +10,7 @@ import {
|
|||
SavedObjectsManagementAction,
|
||||
SavedObjectsManagementRecord,
|
||||
} from '../../../../../src/plugins/saved_objects_management/public';
|
||||
import { ShareSavedObjectsToSpaceFlyout } from './components';
|
||||
import { ContextWrapper, ShareSavedObjectsToSpaceFlyout } from './components';
|
||||
import { SpacesManager } from '../spaces_manager';
|
||||
import { PluginsStart } from '../plugin';
|
||||
|
||||
|
@ -27,7 +27,10 @@ export class ShareToSpaceSavedObjectsManagementAction extends SavedObjectsManage
|
|||
icon: 'share',
|
||||
type: 'icon',
|
||||
available: (object: SavedObjectsManagementRecord) => {
|
||||
return object.meta.namespaceType === 'multiple';
|
||||
const hasCapability =
|
||||
!this.actionContext ||
|
||||
!!this.actionContext.capabilities.savedObjectsManagement.shareIntoSpace;
|
||||
return object.meta.namespaceType === 'multiple' && hasCapability;
|
||||
},
|
||||
onClick: (object: SavedObjectsManagementRecord) => {
|
||||
this.isDataChanged = false;
|
||||
|
@ -52,14 +55,15 @@ export class ShareToSpaceSavedObjectsManagementAction extends SavedObjectsManage
|
|||
}
|
||||
|
||||
return (
|
||||
<ShareSavedObjectsToSpaceFlyout
|
||||
onClose={this.onClose}
|
||||
onObjectUpdated={() => (this.isDataChanged = true)}
|
||||
savedObject={this.record}
|
||||
spacesManager={this.spacesManager}
|
||||
toastNotifications={this.notifications.toasts}
|
||||
getStartServices={this.getStartServices}
|
||||
/>
|
||||
<ContextWrapper getStartServices={this.getStartServices}>
|
||||
<ShareSavedObjectsToSpaceFlyout
|
||||
onClose={this.onClose}
|
||||
onObjectUpdated={() => (this.isDataChanged = true)}
|
||||
savedObject={this.record}
|
||||
spacesManager={this.spacesManager}
|
||||
toastNotifications={this.notifications.toasts}
|
||||
/>
|
||||
</ContextWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsImportRetry, SavedObjectsImportResponse } from 'src/core/public';
|
||||
import { Space } from '..';
|
||||
import { GetSpaceResult } from '..';
|
||||
|
||||
export interface ShareOptions {
|
||||
selectedSpaceIds: string[];
|
||||
|
@ -17,6 +17,7 @@ export interface ShareSavedObjectsToSpaceResponse {
|
|||
[spaceId: string]: SavedObjectsImportResponse;
|
||||
}
|
||||
|
||||
export interface SpaceTarget extends Omit<Space, 'disabledFeatures'> {
|
||||
export interface SpaceTarget extends Omit<GetSpaceResult, 'disabledFeatures'> {
|
||||
isActiveSpace: boolean;
|
||||
isPartiallyAuthorized?: boolean;
|
||||
}
|
||||
|
|
|
@ -8,10 +8,14 @@ import { skipWhile } from 'rxjs/operators';
|
|||
import { HttpSetup } from 'src/core/public';
|
||||
import { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { GetSpacePurpose } from '../../common/model/types';
|
||||
import { GetAllSpacesPurpose, GetSpaceResult } from '../../common/model/types';
|
||||
import { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types';
|
||||
|
||||
type SavedObject = Pick<SavedObjectsManagementRecord, 'type' | 'id'>;
|
||||
interface GetAllSpacesOptions {
|
||||
purpose?: GetAllSpacesPurpose;
|
||||
includeAuthorizedPurposes?: boolean;
|
||||
}
|
||||
|
||||
export class SpacesManager {
|
||||
private activeSpace$: BehaviorSubject<Space | null> = new BehaviorSubject<Space | null>(null);
|
||||
|
@ -30,8 +34,10 @@ export class SpacesManager {
|
|||
this.refreshActiveSpace();
|
||||
}
|
||||
|
||||
public async getSpaces(purpose?: GetSpacePurpose): Promise<Space[]> {
|
||||
return await this.http.get('/api/spaces/space', { query: { purpose } });
|
||||
public async getSpaces(options: GetAllSpacesOptions = {}): Promise<GetSpaceResult[]> {
|
||||
const { purpose, includeAuthorizedPurposes } = options;
|
||||
const query = { purpose, include_authorized_purposes: includeAuthorizedPurposes };
|
||||
return await this.http.get('/api/spaces/space', { query });
|
||||
}
|
||||
|
||||
public async getSpace(id: string): Promise<Space> {
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`#create authorization is null throws bad request when we are at the maximum number of spaces 1`] = `"Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting"`;
|
||||
|
||||
exports[`#create authorization.mode.useRbacForRequest returns false throws bad request when we're at the maximum number of spaces 1`] = `"Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting"`;
|
||||
|
||||
exports[`#create useRbacForRequest is true throws Boom.forbidden if the user isn't authorized at space 1`] = `"Unauthorized to create spaces"`;
|
||||
|
||||
exports[`#create useRbacForRequest is true throws bad request when we are at the maximum number of spaces 1`] = `"Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting"`;
|
||||
|
||||
exports[`#delete authorization is null throws bad request when the space is reserved 1`] = `"This Space cannot be deleted because it is reserved."`;
|
||||
|
||||
exports[`#delete authorization.mode.useRbacForRequest returns false throws bad request when the space is reserved 1`] = `"This Space cannot be deleted because it is reserved."`;
|
||||
|
||||
exports[`#delete authorization.mode.useRbacForRequest returns true throws Boom.forbidden if the user isn't authorized 1`] = `"Unauthorized to delete spaces"`;
|
||||
|
||||
exports[`#delete authorization.mode.useRbacForRequest returns true throws bad request if the user is authorized but the space is reserved 1`] = `"This Space cannot be deleted because it is reserved."`;
|
||||
|
||||
exports[`#get useRbacForRequest is true throws Boom.forbidden if the user isn't authorized at space 1`] = `"Unauthorized to get foo-space space"`;
|
||||
|
||||
exports[`#getAll authorization.mode.useRbacForRequest returns false throws Boom.badRequest when an invalid purpose is provided' 1`] = `"unsupported space purpose: invalid_purpose"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true throws Boom.badRequest when an invalid purpose is provided 1`] = `"unsupported space purpose: invalid_purpose"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true with purpose='any' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true with purpose='copySavedObjectsIntoSpace' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true with purpose='findSavedObjects' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true with purpose='shareSavedObjectsIntoSpace' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true with purpose='undefined' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
|
||||
exports[`#update useRbacForRequest is true throws Boom.forbidden when user isn't authorized at space 1`] = `"Unauthorized to update spaces"`;
|
File diff suppressed because it is too large
Load diff
|
@ -11,31 +11,42 @@ import { isReservedSpace } from '../../../common/is_reserved_space';
|
|||
import { Space } from '../../../common/model/space';
|
||||
import { SpacesAuditLogger } from '../audit_logger';
|
||||
import { ConfigType } from '../../config';
|
||||
import { GetSpacePurpose } from '../../../common/model/types';
|
||||
import { GetAllSpacesPurpose, GetSpaceResult } from '../../../common/model/types';
|
||||
|
||||
const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = [
|
||||
interface GetAllSpacesOptions {
|
||||
purpose?: GetAllSpacesPurpose;
|
||||
includeAuthorizedPurposes?: boolean;
|
||||
}
|
||||
|
||||
const SUPPORTED_GET_SPACE_PURPOSES: GetAllSpacesPurpose[] = [
|
||||
'any',
|
||||
'copySavedObjectsIntoSpace',
|
||||
'findSavedObjects',
|
||||
'shareSavedObjectsIntoSpace',
|
||||
];
|
||||
const DEFAULT_PURPOSE = 'any';
|
||||
|
||||
const PURPOSE_PRIVILEGE_MAP: Record<
|
||||
GetSpacePurpose,
|
||||
GetAllSpacesPurpose,
|
||||
(authorization: SecurityPluginSetup['authz']) => string[]
|
||||
> = {
|
||||
any: (authorization) => [authorization.actions.login],
|
||||
copySavedObjectsIntoSpace: (authorization) => [
|
||||
authorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'),
|
||||
],
|
||||
findSavedObjects: (authorization) => {
|
||||
return [authorization.actions.savedObject.get('config', 'find')];
|
||||
},
|
||||
findSavedObjects: (authorization) => [
|
||||
authorization.actions.login,
|
||||
authorization.actions.savedObject.get('config', 'find'),
|
||||
],
|
||||
shareSavedObjectsIntoSpace: (authorization) => [
|
||||
authorization.actions.ui.get('savedObjectsManagement', 'shareIntoSpace'),
|
||||
],
|
||||
};
|
||||
|
||||
function filterUnauthorizedSpaceResults(value: GetSpaceResult | null): value is GetSpaceResult {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
export class SpacesClient {
|
||||
constructor(
|
||||
private readonly auditLogger: SpacesAuditLogger,
|
||||
|
@ -47,14 +58,17 @@ export class SpacesClient {
|
|||
private readonly request: KibanaRequest
|
||||
) {}
|
||||
|
||||
public async getAll(purpose: GetSpacePurpose = 'any'): Promise<Space[]> {
|
||||
public async getAll(options: GetAllSpacesOptions = {}): Promise<GetSpaceResult[]> {
|
||||
const { purpose = DEFAULT_PURPOSE, includeAuthorizedPurposes = false } = options;
|
||||
if (!SUPPORTED_GET_SPACE_PURPOSES.includes(purpose)) {
|
||||
throw Boom.badRequest(`unsupported space purpose: ${purpose}`);
|
||||
}
|
||||
|
||||
if (this.useRbac()) {
|
||||
const privilegeFactory = PURPOSE_PRIVILEGE_MAP[purpose];
|
||||
if (options.purpose && includeAuthorizedPurposes) {
|
||||
throw Boom.badRequest(`'purpose' cannot be supplied with 'includeAuthorizedPurposes'`);
|
||||
}
|
||||
|
||||
if (this.useRbac()) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { saved_objects } = await this.internalSavedObjectRepository.find({
|
||||
type: 'space',
|
||||
|
@ -65,26 +79,62 @@ export class SpacesClient {
|
|||
|
||||
this.debugLogger(`SpacesClient.getAll(), using RBAC. Found ${saved_objects.length} spaces`);
|
||||
|
||||
const spaces = saved_objects.map(this.transformSavedObjectToSpace);
|
||||
|
||||
const spaces: GetSpaceResult[] = saved_objects.map(this.transformSavedObjectToSpace);
|
||||
const spaceIds = spaces.map((space: Space) => space.id);
|
||||
|
||||
const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request);
|
||||
|
||||
const privilege = privilegeFactory(this.authorization!);
|
||||
|
||||
const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, {
|
||||
kibana: privilege,
|
||||
});
|
||||
|
||||
const authorized = privileges.kibana.filter((x) => x.authorized).map((x) => x.resource);
|
||||
|
||||
this.debugLogger(
|
||||
`SpacesClient.getAll(), authorized for ${
|
||||
authorized.length
|
||||
} spaces, derived from ES privilege check: ${JSON.stringify(privileges)}`
|
||||
// Collect all privileges which need to be checked
|
||||
const allPrivileges = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce(
|
||||
(acc, [getSpacesPurpose, privilegeFactory]) =>
|
||||
!includeAuthorizedPurposes && getSpacesPurpose !== purpose
|
||||
? acc
|
||||
: { ...acc, [getSpacesPurpose]: privilegeFactory(this.authorization!) },
|
||||
{} as Record<GetAllSpacesPurpose, string[]>
|
||||
);
|
||||
|
||||
if (authorized.length === 0) {
|
||||
// Check all privileges against all spaces
|
||||
const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, {
|
||||
kibana: Object.values(allPrivileges).flat(),
|
||||
});
|
||||
|
||||
// Determine which purposes the user is authorized for within each space.
|
||||
// Remove any spaces for which user is fully unauthorized.
|
||||
const checkHasAllRequired = (space: Space, actions: string[]) =>
|
||||
actions.every((action) =>
|
||||
privileges.kibana.some(
|
||||
({ resource, privilege, authorized }) =>
|
||||
resource === space.id && privilege === action && authorized
|
||||
)
|
||||
);
|
||||
const authorizedSpaces = spaces
|
||||
.map((space: Space) => {
|
||||
if (!includeAuthorizedPurposes) {
|
||||
// Check if the user is authorized for a single purpose
|
||||
const requiredActions = PURPOSE_PRIVILEGE_MAP[purpose](this.authorization!);
|
||||
return checkHasAllRequired(space, requiredActions) ? space : null;
|
||||
}
|
||||
|
||||
// Check if the user is authorized for each purpose
|
||||
let hasAnyAuthorization = false;
|
||||
const authorizedPurposes = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce(
|
||||
(acc, [purposeKey, privilegeFactory]) => {
|
||||
const requiredActions = privilegeFactory(this.authorization!);
|
||||
const hasAllRequired = checkHasAllRequired(space, requiredActions);
|
||||
hasAnyAuthorization = hasAnyAuthorization || hasAllRequired;
|
||||
return { ...acc, [purposeKey]: hasAllRequired };
|
||||
},
|
||||
{} as Record<GetAllSpacesPurpose, boolean>
|
||||
);
|
||||
|
||||
if (!hasAnyAuthorization) {
|
||||
return null;
|
||||
}
|
||||
return { ...space, authorizedPurposes };
|
||||
})
|
||||
.filter(filterUnauthorizedSpaceResults);
|
||||
|
||||
if (authorizedSpaces.length === 0) {
|
||||
this.debugLogger(
|
||||
`SpacesClient.getAll(), using RBAC. returning 403/Forbidden. Not authorized for any spaces for ${purpose} purpose.`
|
||||
);
|
||||
|
@ -92,14 +142,12 @@ export class SpacesClient {
|
|||
throw Boom.forbidden(); // Note: there is a catch for this in `SpacesSavedObjectsClient.find`; if we get rid of this error, remove that too
|
||||
}
|
||||
|
||||
this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized as string[]);
|
||||
const filteredSpaces: Space[] = spaces.filter((space: any) => authorized.includes(space.id));
|
||||
const authorizedSpaceIds = authorizedSpaces.map((s) => s.id);
|
||||
this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorizedSpaceIds);
|
||||
this.debugLogger(
|
||||
`SpacesClient.getAll(), using RBAC. returning spaces: ${filteredSpaces
|
||||
.map((s) => s.id)
|
||||
.join(',')}`
|
||||
`SpacesClient.getAll(), using RBAC. returning spaces: ${authorizedSpaceIds.join(',')}`
|
||||
);
|
||||
return filteredSpaces;
|
||||
return authorizedSpaces;
|
||||
} else {
|
||||
this.debugLogger(`SpacesClient.getAll(), NOT USING RBAC. querying all spaces`);
|
||||
|
||||
|
|
|
@ -75,66 +75,54 @@ describe('GET /spaces/space', () => {
|
|||
};
|
||||
};
|
||||
|
||||
it(`returns all available spaces`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
[undefined, 'any', 'copySavedObjectsIntoSpace', 'shareSavedObjectsIntoSpace'].forEach(
|
||||
(purpose) => {
|
||||
describe(`with purpose='${purpose}'`, () => {
|
||||
it(`returns expected result when not specifying include_authorized_purposes`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
method: 'get',
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest({ method: 'get', query: { purpose } });
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(spaces);
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(spaces);
|
||||
});
|
||||
it(`returns expected result when specifying include_authorized_purposes=true`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
|
||||
it(`returns all available spaces with the 'any' purpose`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
method: 'get',
|
||||
query: { purpose, include_authorized_purposes: true },
|
||||
});
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
query: {
|
||||
purpose: 'any',
|
||||
},
|
||||
method: 'get',
|
||||
});
|
||||
if (purpose === undefined) {
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(spaces);
|
||||
} else {
|
||||
expect(response.status).toEqual(400);
|
||||
expect(response.payload).toEqual(
|
||||
new Error(`'purpose' cannot be supplied with 'includeAuthorizedPurposes'`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
it(`returns expected result when specifying include_authorized_purposes=false`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(spaces);
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
method: 'get',
|
||||
query: { purpose, include_authorized_purposes: false },
|
||||
});
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
it(`returns all available spaces with the 'copySavedObjectsIntoSpace' purpose`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
query: {
|
||||
purpose: 'copySavedObjectsIntoSpace',
|
||||
},
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(spaces);
|
||||
});
|
||||
|
||||
it(`returns all available spaces with the 'shareSavedObjectsIntoSpace' purpose`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
query: {
|
||||
purpose: 'shareSavedObjectsIntoSpace',
|
||||
},
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(spaces);
|
||||
});
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(spaces);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it(`returns http/403 when the license is invalid`, async () => {
|
||||
const { routeHandler } = await setup();
|
||||
|
|
|
@ -18,15 +18,18 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) {
|
|||
path: '/api/spaces/space',
|
||||
validate: {
|
||||
query: schema.object({
|
||||
purpose: schema.oneOf(
|
||||
[
|
||||
purpose: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal('any'),
|
||||
schema.literal('copySavedObjectsIntoSpace'),
|
||||
schema.literal('shareSavedObjectsIntoSpace'),
|
||||
],
|
||||
{
|
||||
defaultValue: 'any',
|
||||
}
|
||||
])
|
||||
),
|
||||
include_authorized_purposes: schema.conditional(
|
||||
schema.siblingRef('purpose'),
|
||||
schema.string(),
|
||||
schema.maybe(schema.literal(false)),
|
||||
schema.maybe(schema.boolean())
|
||||
),
|
||||
}),
|
||||
},
|
||||
|
@ -34,18 +37,24 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) {
|
|||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
log.debug(`Inside GET /api/spaces/space`);
|
||||
|
||||
const purpose = request.query.purpose;
|
||||
const { purpose, include_authorized_purposes: includeAuthorizedPurposes } = request.query;
|
||||
|
||||
const spacesClient = await spacesService.scopedClient(request);
|
||||
|
||||
let spaces: Space[];
|
||||
|
||||
try {
|
||||
log.debug(`Attempting to retrieve all spaces for ${purpose} purpose`);
|
||||
spaces = await spacesClient.getAll(purpose);
|
||||
log.debug(`Retrieved ${spaces.length} spaces for ${purpose} purpose`);
|
||||
log.debug(
|
||||
`Attempting to retrieve all spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}`
|
||||
);
|
||||
spaces = await spacesClient.getAll({ purpose, includeAuthorizedPurposes });
|
||||
log.debug(
|
||||
`Retrieved ${spaces.length} spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}`
|
||||
);
|
||||
} catch (error) {
|
||||
log.debug(`Error retrieving spaces for ${purpose} purpose: ${error}`);
|
||||
log.debug(
|
||||
`Error retrieving spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}: ${error}`
|
||||
);
|
||||
return response.customError(wrapError(error));
|
||||
}
|
||||
|
||||
|
|
|
@ -227,7 +227,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
type: ['foo', 'bar'],
|
||||
namespaces: ['ns-1', 'ns-2'],
|
||||
});
|
||||
expect(spacesClient.getAll).toHaveBeenCalledWith('findSavedObjects');
|
||||
expect(spacesClient.getAll).toHaveBeenCalledWith({ purpose: 'findSavedObjects' });
|
||||
});
|
||||
|
||||
test(`filters options.namespaces based on authorization`, async () => {
|
||||
|
@ -258,7 +258,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
type: ['foo', 'bar'],
|
||||
namespaces: ['ns-1'],
|
||||
});
|
||||
expect(spacesClient.getAll).toHaveBeenCalledWith('findSavedObjects');
|
||||
expect(spacesClient.getAll).toHaveBeenCalledWith({ purpose: 'findSavedObjects' });
|
||||
});
|
||||
|
||||
test(`translates options.namespace: ['*']`, async () => {
|
||||
|
@ -289,7 +289,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
type: ['foo', 'bar'],
|
||||
namespaces: ['ns-1', 'ns-2'],
|
||||
});
|
||||
expect(spacesClient.getAll).toHaveBeenCalledWith('findSavedObjects');
|
||||
expect(spacesClient.getAll).toHaveBeenCalledWith({ purpose: 'findSavedObjects' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -170,7 +170,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
|
|||
const spacesClient = await this.getSpacesClient;
|
||||
|
||||
try {
|
||||
const availableSpaces = await spacesClient.getAll('findSavedObjects');
|
||||
const availableSpaces = await spacesClient.getAll({ purpose: 'findSavedObjects' });
|
||||
if (namespaces.includes(ALL_SPACES_ID)) {
|
||||
namespaces = availableSpaces.map((space) => space.id);
|
||||
} else {
|
||||
|
|
|
@ -17,6 +17,7 @@ interface GetAllTests {
|
|||
exists: GetAllTest;
|
||||
copySavedObjectsPurpose: GetAllTest;
|
||||
shareSavedObjectsPurpose: GetAllTest;
|
||||
includeAuthorizedPurposes: GetAllTest;
|
||||
}
|
||||
|
||||
interface GetAllTestDefinition {
|
||||
|
@ -25,29 +26,48 @@ interface GetAllTestDefinition {
|
|||
tests: GetAllTests;
|
||||
}
|
||||
|
||||
interface AuthorizedPurposes {
|
||||
any: boolean;
|
||||
copySavedObjectsIntoSpace: boolean;
|
||||
findSavedObjects: boolean;
|
||||
shareSavedObjectsIntoSpace: boolean;
|
||||
}
|
||||
|
||||
const ALL_SPACE_RESULTS = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default Space',
|
||||
description: 'This is the default space',
|
||||
_reserved: true,
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space_1',
|
||||
name: 'Space 1',
|
||||
description: 'This is the first test space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space_2',
|
||||
name: 'Space 2',
|
||||
description: 'This is the second test space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
];
|
||||
|
||||
export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>) {
|
||||
const createExpectResults = (...spaceIds: string[]) => (resp: { [key: string]: any }) => {
|
||||
const expectedBody = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default Space',
|
||||
description: 'This is the default space',
|
||||
_reserved: true,
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space_1',
|
||||
name: 'Space 1',
|
||||
description: 'This is the first test space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space_2',
|
||||
name: 'Space 2',
|
||||
description: 'This is the second test space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
].filter((entry) => spaceIds.includes(entry.id));
|
||||
const expectedBody = ALL_SPACE_RESULTS.filter((entry) => spaceIds.includes(entry.id));
|
||||
expect(resp.body).to.eql(expectedBody);
|
||||
};
|
||||
|
||||
const createExpectAllPurposesResults = (
|
||||
authorizedPurposes: AuthorizedPurposes,
|
||||
...spaceIds: string[]
|
||||
) => (resp: { [key: string]: any }) => {
|
||||
const expectedBody = ALL_SPACE_RESULTS.filter((entry) =>
|
||||
spaceIds.includes(entry.id)
|
||||
).map((x) => ({ ...x, authorizedPurposes }));
|
||||
expect(resp.body).to.eql(expectedBody);
|
||||
};
|
||||
|
||||
|
@ -72,15 +92,17 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
after(() => esArchiver.unload('saved_objects/spaces'));
|
||||
|
||||
getTestScenariosForSpace(spaceId).forEach(({ scenario, urlPrefix }) => {
|
||||
it(`should return ${tests.exists.statusCode} ${scenario}`, async () => {
|
||||
return supertest
|
||||
.get(`${urlPrefix}/api/spaces/space`)
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.exists.statusCode)
|
||||
.then(tests.exists.response);
|
||||
describe('undefined purpose', () => {
|
||||
it(`should return ${tests.exists.statusCode} ${scenario}`, async () => {
|
||||
return supertest
|
||||
.get(`${urlPrefix}/api/spaces/space`)
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.exists.statusCode)
|
||||
.then(tests.exists.response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copySavedObjects purpose', () => {
|
||||
describe('copySavedObjectsIntoSpace purpose', () => {
|
||||
it(`should return ${tests.copySavedObjectsPurpose.statusCode} ${scenario}`, async () => {
|
||||
return supertest
|
||||
.get(`${urlPrefix}/api/spaces/space`)
|
||||
|
@ -91,7 +113,7 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
});
|
||||
});
|
||||
|
||||
describe('copySavedObjects purpose', () => {
|
||||
describe('shareSavedObjectsIntoSpace purpose', () => {
|
||||
it(`should return ${tests.shareSavedObjectsPurpose.statusCode} ${scenario}`, async () => {
|
||||
return supertest
|
||||
.get(`${urlPrefix}/api/spaces/space`)
|
||||
|
@ -101,6 +123,17 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
.then(tests.copySavedObjectsPurpose.response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('include_authorized_purposes=true', () => {
|
||||
it(`should return ${tests.includeAuthorizedPurposes.statusCode} ${scenario}`, async () => {
|
||||
return supertest
|
||||
.get(`${urlPrefix}/api/spaces/space`)
|
||||
.query({ include_authorized_purposes: true })
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.includeAuthorizedPurposes.statusCode)
|
||||
.then(tests.includeAuthorizedPurposes.response);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -111,6 +144,7 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
|
||||
return {
|
||||
createExpectResults,
|
||||
createExpectAllPurposesResults,
|
||||
expectRbacForbidden,
|
||||
getAllTest,
|
||||
expectEmptyResult,
|
||||
|
|
|
@ -14,10 +14,26 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const { getAllTest, createExpectResults, expectRbacForbidden } = getAllTestSuiteFactory(
|
||||
esArchiver,
|
||||
supertestWithoutAuth
|
||||
);
|
||||
const {
|
||||
getAllTest,
|
||||
createExpectResults,
|
||||
createExpectAllPurposesResults,
|
||||
expectRbacForbidden,
|
||||
} = getAllTestSuiteFactory(esArchiver, supertestWithoutAuth);
|
||||
|
||||
// these are used to determine expected results for tests where the `include_authorized_purposes` option is enabled
|
||||
const authorizedAll = {
|
||||
any: true,
|
||||
copySavedObjectsIntoSpace: true,
|
||||
findSavedObjects: true,
|
||||
shareSavedObjectsIntoSpace: true,
|
||||
};
|
||||
const authorizedRead = {
|
||||
any: true,
|
||||
copySavedObjectsIntoSpace: false,
|
||||
findSavedObjects: true,
|
||||
shareSavedObjectsIntoSpace: false,
|
||||
};
|
||||
|
||||
describe('get all', () => {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
@ -92,6 +108,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -111,6 +131,15 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(
|
||||
authorizedAll,
|
||||
'default',
|
||||
'space_1',
|
||||
'space_2'
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -130,6 +159,15 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(
|
||||
authorizedAll,
|
||||
'default',
|
||||
'space_1',
|
||||
'space_2'
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -149,6 +187,15 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(
|
||||
authorizedAll,
|
||||
'default',
|
||||
'space_1',
|
||||
'space_2'
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -168,6 +215,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -187,6 +238,15 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(
|
||||
authorizedRead,
|
||||
'default',
|
||||
'space_1',
|
||||
'space_2'
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -206,6 +266,15 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(
|
||||
authorizedRead,
|
||||
'default',
|
||||
'space_1',
|
||||
'space_2'
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -225,6 +294,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('space_1'),
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(authorizedAll, 'space_1'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -244,6 +317,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(authorizedRead, 'space_1'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -265,6 +342,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default'),
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(authorizedAll, 'default'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -287,6 +368,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(authorizedRead, 'default'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -309,6 +394,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default'),
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(authorizedAll, 'default'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -331,6 +420,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(authorizedRead, 'default'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -353,6 +446,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('space_1'),
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(authorizedAll, 'space_1'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -375,6 +472,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectAllPurposesResults(authorizedRead, 'space_1'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -395,6 +496,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -414,6 +519,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -433,6 +542,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -452,6 +565,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,6 +42,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
includeAuthorizedPurposes: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue