From dd1ae90ac1a8e12b8462b913665881e93fc44036 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 11 Aug 2023 15:48:08 +0800 Subject: [PATCH 1/3] add workspace filter into saved objects page (#76) * add workspace filter into saved objects page Signed-off-by: Hailong Cui * workspace filter Signed-off-by: Hailong Cui * managment workspace filter Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui --- src/core/public/utils/index.ts | 2 +- .../public/lib/get_saved_object_counts.ts | 4 +- .../public/lib/parse_query.ts | 7 ++ .../objects_table/saved_objects_table.tsx | 114 +++++++++++++++--- .../server/routes/scroll_count.ts | 24 +++- 5 files changed, 129 insertions(+), 22 deletions(-) diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index a6d76a87e313..32ea0ba3c101 100644 --- a/src/core/public/utils/index.ts +++ b/src/core/public/utils/index.ts @@ -32,4 +32,4 @@ export { shareWeakReplay } from './share_weak_replay'; export { Sha256 } from './crypto'; export { MountWrapper, mountReactNode } from './mount'; export { getWorkspaceIdFromUrl, WORKSPACE_TYPE } from './workspace'; -export { WORKSPACE_PATH_PREFIX, PUBLIC_WORKSPACE } from '../../utils'; +export { WORKSPACE_PATH_PREFIX, PUBLIC_WORKSPACE, MANAGEMENT_WORKSPACE } from '../../utils'; diff --git a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts index 9039dae2be53..374f2720b537 100644 --- a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts +++ b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts @@ -40,8 +40,8 @@ export interface SavedObjectCountOptions { export async function getSavedObjectCounts( http: HttpStart, options: SavedObjectCountOptions -): Promise> { - return await http.post>( +): Promise>> { + return await http.post>>( `/api/opensearch-dashboards/management/saved_objects/scroll/counts`, { body: JSON.stringify(options) } ); diff --git a/src/plugins/saved_objects_management/public/lib/parse_query.ts b/src/plugins/saved_objects_management/public/lib/parse_query.ts index 24c35d500aaa..3db3f7fcee1c 100644 --- a/src/plugins/saved_objects_management/public/lib/parse_query.ts +++ b/src/plugins/saved_objects_management/public/lib/parse_query.ts @@ -33,12 +33,15 @@ import { Query } from '@elastic/eui'; interface ParsedQuery { queryText?: string; visibleTypes?: string[]; + visibleNamespaces?: string[]; + visibleWorkspaces?: string[]; } export function parseQuery(query: Query): ParsedQuery { let queryText: string | undefined; let visibleTypes: string[] | undefined; let visibleNamespaces: string[] | undefined; + let visibleWorkspaces: string[] | undefined; if (query) { if (query.ast.getTermClauses().length) { @@ -53,11 +56,15 @@ export function parseQuery(query: Query): ParsedQuery { if (query.ast.getFieldClauses('namespaces')) { visibleNamespaces = query.ast.getFieldClauses('namespaces')[0].value as string[]; } + if (query.ast.getFieldClauses('workspaces')) { + visibleWorkspaces = query.ast.getFieldClauses('workspaces')[0].value as string[]; + } } return { queryText, visibleTypes, visibleNamespaces, + visibleWorkspaces, }; } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 517b4a069266..fb621d13f8d0 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -66,8 +66,9 @@ import { OverlayStart, NotificationsStart, ApplicationStart, - PUBLIC_WORKSPACE, -} from '../../../../../core/public'; + WorkspaceAttribute, +} from 'src/core/public'; +import { Subscription } from 'rxjs'; import { RedirectAppLinks } from '../../../../opensearch_dashboards_react/public'; import { IndexPatternsContract } from '../../../../data/public'; import { @@ -95,6 +96,7 @@ import { import { Header, Table, Flyout, Relationships } from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { SavedObjectsCopyModal } from './components/copy_modal'; +import { PUBLIC_WORKSPACE, MANAGEMENT_WORKSPACE } from '../../../../../core/public'; interface ExportAllOption { id: string; @@ -128,7 +130,7 @@ export interface SavedObjectsTableState { page: number; perPage: number; savedObjects: SavedObjectWithMetadata[]; - savedObjectCounts: Record; + savedObjectCounts: Record>; activeQuery: Query; selectedSavedObjects: SavedObjectWithMetadata[]; isShowingImportFlyout: boolean; @@ -144,23 +146,28 @@ export interface SavedObjectsTableState { exportAllSelectedOptions: Record; isIncludeReferencesDeepChecked: boolean; workspaceId: string | null; + availableWorkspace?: WorkspaceAttribute[]; } export class SavedObjectsTable extends Component { private _isMounted = false; + private currentWorkspaceIdSubscription?: Subscription; + private workspacesSubscription?: Subscription; constructor(props: SavedObjectsTableProps) { super(props); + const typeCounts = props.allowedTypes.reduce((typeToCountMap, type) => { + typeToCountMap[type] = 0; + return typeToCountMap; + }, {} as Record); + this.state = { totalCount: 0, page: 0, perPage: props.perPageConfig || 50, savedObjects: [], - savedObjectCounts: props.allowedTypes.reduce((typeToCountMap, type) => { - typeToCountMap[type] = 0; - return typeToCountMap; - }, {} as Record), + savedObjectCounts: { type: typeCounts } as Record>, activeQuery: Query.parse(''), selectedSavedObjects: [], isShowingImportFlyout: false, @@ -176,22 +183,37 @@ export class SavedObjectsTable extends Component ws.id); + } else if (workspaceId === PUBLIC_WORKSPACE) { + return [PUBLIC_WORKSPACE]; + } else { + return [workspaceId, PUBLIC_WORKSPACE]; + } + } + + private get wsNameIdLookup() { + const { availableWorkspace } = this.state; + // Assumption: workspace name is unique across the system + return availableWorkspace?.reduce((map, ws) => { + return map.set(ws.name, ws.id); + }, new Map()); } componentDidMount() { this._isMounted = true; - this.props.workspaces.currentWorkspaceId$.subscribe((workspaceId) => - this.setState({ - workspaceId, - }) - ); + + this.fetchWorkspace(); this.fetchSavedObjects(); this.fetchCounts(); } @@ -199,11 +221,15 @@ export class SavedObjectsTable extends Component { const { allowedTypes, namespaceRegistry } = this.props; - const { queryText, visibleTypes, visibleNamespaces } = parseQuery(this.state.activeQuery); + const { queryText, visibleTypes, visibleNamespaces, visibleWorkspaces } = parseQuery( + this.state.activeQuery + ); const filteredTypes = filterQuery(allowedTypes, visibleTypes); @@ -219,6 +245,11 @@ export class SavedObjectsTable extends Component this.wsNameIdLookup?.get(wsName) || PUBLIC_WORKSPACE + ); + } // These are the saved objects visible in the table. const filteredSavedObjectCounts = await getSavedObjectCounts( @@ -268,6 +299,19 @@ export class SavedObjectsTable extends Component { + const workspace = this.props.workspaces; + this.currentWorkspaceIdSubscription = workspace.currentWorkspaceId$.subscribe((workspaceId) => + this.setState({ + workspaceId, + }) + ); + + this.workspacesSubscription = workspace.workspaceList$.subscribe((workspaceList) => { + this.setState({ availableWorkspace: workspaceList }); + }); + }; + fetchSavedObject = (type: string, id: string) => { this.setState({ isSearching: true }, () => this.debouncedFetchObject(type, id)); }; @@ -275,7 +319,7 @@ export class SavedObjectsTable extends Component { const { activeQuery: query, page, perPage } = this.state; const { notifications, http, allowedTypes, namespaceRegistry } = this.props; - const { queryText, visibleTypes, visibleNamespaces } = parseQuery(query); + const { queryText, visibleTypes, visibleNamespaces, visibleWorkspaces } = parseQuery(query); const filteredTypes = filterQuery(allowedTypes, visibleTypes); // "searchFields" is missing from the "findOptions" but gets injected via the API. // The API extracts the fields from each uiExports.savedObjectsManagement "defaultSearchField" attribute @@ -294,6 +338,13 @@ export class SavedObjectsTable extends Component this.wsNameIdLookup?.get(wsName) || PUBLIC_WORKSPACE + ); + findOptions.workspaces = workspaceIds; + } + if (findOptions.type.length > 1) { findOptions.sortField = 'type'; } @@ -880,6 +931,7 @@ export class SavedObjectsTable extends Component { + return this.workspaceIdQuery?.includes(ws.id); + }) + .map((ws) => { + return { + name: ws.name, + value: ws.name, + view: `${ws.name} (${wsCounts[ws.id] || 0})`, + }; + }); + + filters.push({ + type: 'field_value_selection', + field: 'workspaces', + name: + namespaceRegistry.getAlias() || + i18n.translate('savedObjectsManagement.objectsTable.table.workspaceFilterName', { + defaultMessage: 'Workspaces', + }), + multiSelect: 'or', + options: wsFilterOptions, + }); + } + return ( { }, router.handleLegacyErrors(async (context, req, res) => { const { client } = context.core.savedObjects; - const counts = { + const counts: Record> = { type: {}, }; const findOptions: SavedObjectsFindOptions = { type: req.body.typesToInclude, perPage: 1000, - workspaces: req.body.workspaces, }; const requestHasNamespaces = Array.isArray(req.body.namespacesToInclude) && req.body.namespacesToInclude.length; + const requestHasWorkspaces = Array.isArray(req.body.workspaces) && req.body.workspaces.length; + if (requestHasNamespaces) { counts.namespaces = {}; findOptions.namespaces = req.body.namespacesToInclude; } + if (requestHasWorkspaces) { + counts.workspaces = {}; + findOptions.workspaces = req.body.workspaces; + } + if (req.body.searchString) { findOptions.search = `${req.body.searchString}*`; findOptions.searchFields = ['title']; @@ -84,6 +90,13 @@ export const registerScrollForCountRoute = (router: IRouter) => { counts.namespaces[ns]++; }); } + if (requestHasWorkspaces) { + const resultWorkspaces = result.workspaces || ['public']; + resultWorkspaces.forEach((ws) => { + counts.workspaces[ws] = counts.workspaces[ws] || 0; + counts.workspaces[ws]++; + }); + } counts.type[type] = counts.type[type] || 0; counts.type[type]++; }); @@ -101,6 +114,13 @@ export const registerScrollForCountRoute = (router: IRouter) => { } } + const workspacesToInclude = req.body.workspaces || []; + for (const ws of workspacesToInclude) { + if (!counts.workspaces[ws]) { + counts.workspaces[ws] = 0; + } + } + return res.ok({ body: counts, }); From 3b8879d9ad967c5b6e7bf6004e55b453c9fbb9ec Mon Sep 17 00:00:00 2001 From: raintygao Date: Fri, 11 Aug 2023 16:43:25 +0800 Subject: [PATCH 2/3] add permission check when updating workspace (#81) * feat: add permission check when updating workspace Signed-off-by: tygao * fix: only use management access and update bulkUpdate logic Signed-off-by: tygao * chore: update code Signed-off-by: tygao * chore: update code after rebase Signed-off-by: tygao --------- Signed-off-by: tygao --- .../workspace_saved_objects_client_wrapper.ts | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index bd1d68bd0f7b..170498fb40bb 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -19,6 +19,11 @@ import { SavedObjectsDeleteOptions, SavedObjectsFindOptions, SavedObjectsShareObjects, + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, + SavedObjectsBulkUpdateObject, + SavedObjectsBulkUpdateResponse, + SavedObjectsBulkUpdateOptions, SavedObjectsPermissionControlContract, WORKSPACE_TYPE, ACL, @@ -60,6 +65,28 @@ export class WorkspaceSavedObjectsClientWrapper { return [permission]; } + private async validateSingleWorkspacePermissions( + workspaceId: string | undefined, + request: OpenSearchDashboardsRequest, + permissionMode: WorkspacePermissionMode | WorkspacePermissionMode[] + ) { + if (!workspaceId) { + return; + } + if ( + !(await this.permissionControl.validate( + request, + { + type: WORKSPACE_TYPE, + id: workspaceId, + }, + this.formatWorkspacePermissionModeToStringArray(permissionMode) + )) + ) { + throw generateWorkspacePermissionError(); + } + } + private async validateMultiWorkspacesPermissions( workspaces: string[] | undefined, request: OpenSearchDashboardsRequest, @@ -129,6 +156,12 @@ export class WorkspaceSavedObjectsClientWrapper { id: string, options: SavedObjectsDeleteOptions = {} ) => { + if (this.isRelatedToWorkspace(type)) { + await this.validateSingleWorkspacePermissions(id, wrapperOptions.request, [ + WorkspacePermissionMode.Management, + ]); + } + const objectToDeleted = await wrapperOptions.client.get(type, id, options); await this.validateMultiWorkspacesPermissions( objectToDeleted.workspaces, @@ -138,6 +171,42 @@ export class WorkspaceSavedObjectsClientWrapper { return await wrapperOptions.client.delete(type, id, options); }; + const updateWithWorkspacePermissionControl = async ( + type: string, + id: string, + attributes: Partial, + options: SavedObjectsUpdateOptions = {} + ): Promise> => { + if (this.isRelatedToWorkspace(type)) { + await this.validateSingleWorkspacePermissions(id, wrapperOptions.request, [ + WorkspacePermissionMode.Management, + ]); + } + return await wrapperOptions.client.update(type, id, attributes, options); + }; + + const bulkUpdateWithWorkspacePermissionControl = async ( + objects: Array>, + options?: SavedObjectsBulkUpdateOptions + ): Promise> => { + const workspaceIds = objects.reduce((acc, cur) => { + if (this.isRelatedToWorkspace(cur.type)) { + acc.push(cur.id); + } + return acc; + }, []); + const permittedWorkspaceIds = + (await this.permissionControl.getPermittedWorkspaceIds(wrapperOptions.request, [ + WorkspacePermissionMode.Management, + ])) ?? []; + const workspacePermitted = workspaceIds.every((id) => permittedWorkspaceIds.includes(id)); + if (!workspacePermitted) { + throw generateWorkspacePermissionError(); + } + + return await wrapperOptions.client.bulkUpdate(objects, options); + }; + const bulkCreateWithWorkspacePermissionControl = async ( objects: Array>, options: SavedObjectsCreateOptions = {} @@ -310,8 +379,8 @@ export class WorkspaceSavedObjectsClientWrapper { create: createWithWorkspacePermissionControl, bulkCreate: bulkCreateWithWorkspacePermissionControl, delete: deleteWithWorkspacePermissionControl, - update: wrapperOptions.client.update, - bulkUpdate: wrapperOptions.client.bulkUpdate, + update: updateWithWorkspacePermissionControl, + bulkUpdate: bulkUpdateWithWorkspacePermissionControl, addToWorkspaces: addToWorkspacesWithPermissionControl, }; }; From e4b45a99e26c6bfe3c32cb83752fa64fd231a448 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 11 Aug 2023 16:50:51 +0800 Subject: [PATCH 3/3] Show objects without workspace info when no workspaces are provided in find query. (#83) * temp: modify Signed-off-by: SuZhou-Joe * feat: update Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- .../workspace_saved_objects_client_wrapper.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 170498fb40bb..5b2b45b353d7 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -327,6 +327,16 @@ export class WorkspaceSavedObjectsClientWrapper { workspaces: permittedWorkspaceIds, }, }, + // TODO: remove this child clause when home workspace proposal is finalized. + { + bool: { + must_not: { + exists: { + field: 'workspaces', + }, + }, + }, + }, ], }, },