Skip to content

Commit

Permalink
Fix saved object share UI bugs regarding read-only privileges (#81828)
Browse files Browse the repository at this point in the history
  • Loading branch information
jportner authored Nov 4, 2020
1 parent 286dbca commit fb1c7d7
Show file tree
Hide file tree
Showing 28 changed files with 1,371 additions and 941 deletions.
86 changes: 85 additions & 1 deletion docs/api/spaces-management/get_all.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,38 @@ 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

`200`::
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:

Expand Down Expand Up @@ -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,
}
}
]
--------------------------------------------------

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -82,7 +81,7 @@ const defaultProps: TableProps = {
onTableChange: () => {},
isSearching: false,
onShowRelationships: () => {},
canDelete: true,
capabilities: { savedObjectsManagement: { delete: true } } as any,
};

describe('Table', () => {
Expand Down Expand Up @@ -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();
Expand All @@ -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 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -156,6 +156,7 @@ export class Table extends PureComponent<TableProps, TableState> {
isSearching,
filterOptions,
selectionConfig: selection,
capabilities,
onDelete,
onActionRefresh,
selectedSavedObjects,
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down
8 changes: 7 additions & 1 deletion x-pack/plugins/spaces/common/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/spaces/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading

0 comments on commit fb1c7d7

Please sign in to comment.