diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index ac909b62dee..7b98c544daa 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -75,7 +75,7 @@ export class WorkspacesClient { /** * Initialize workspace list */ - init() { + public init() { this.updateWorkspaceListAndNotify(); } diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index fc2f111f94e..2d4cf2aa6b0 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -119,7 +119,8 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt * * @public */ -export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions { +export interface SavedObjectsDeleteByNamespaceOptions + extends Omit { /** The OpenSearch supports only boolean flag for this operation */ refresh?: boolean; } @@ -891,7 +892,7 @@ export class SavedObjectsRepository { */ async bulkGet( objects: SavedObjectsBulkGetObject[] = [], - options: SavedObjectsBaseOptions = {} + options: Omit = {} ): Promise> { const namespace = normalizeNamespace(options.namespace); @@ -979,7 +980,7 @@ export class SavedObjectsRepository { async get( type: string, id: string, - options: SavedObjectsBaseOptions = {} + options: Omit = {} ): Promise> { if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 5f92dacacf3..4e1f1a7b456 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -233,7 +233,7 @@ export interface SavedObjectsBulkUpdateOptions extends SavedObjectsBaseOptions { * * @public */ -export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { +export interface SavedObjectsDeleteOptions extends Omit { /** The OpenSearch Refresh setting for this operation */ refresh?: MutatingOperationRefreshSetting; /** Force deletion of an object that exists in multiple namespaces */ diff --git a/src/core/server/workspaces/saved_objects/index.ts b/src/core/server/workspaces/saved_objects/index.ts index 51653c50681..e47be61b0cd 100644 --- a/src/core/server/workspaces/saved_objects/index.ts +++ b/src/core/server/workspaces/saved_objects/index.ts @@ -4,3 +4,4 @@ */ export { workspace } from './workspace'; +export { WorkspaceSavedObjectsClientWrapper } from './workspace_saved_objects_client_wrapper'; diff --git a/src/core/server/workspaces/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/core/server/workspaces/saved_objects/workspace_saved_objects_client_wrapper.ts new file mode 100644 index 00000000000..25c3aa157d9 --- /dev/null +++ b/src/core/server/workspaces/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -0,0 +1,188 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import Boom from '@hapi/boom'; + +import { + OpenSearchDashboardsRequest, + SavedObject, + SavedObjectsBaseOptions, + SavedObjectsBulkCreateObject, + SavedObjectsBulkGetObject, + SavedObjectsBulkResponse, + SavedObjectsClientWrapperFactory, + SavedObjectsCreateOptions, + SavedObjectsDeleteOptions, + SavedObjectsFindOptions, +} from 'opensearch-dashboards/server'; +import { + WorkspacePermissionControl, + WorkspacePermissionMode, +} from '../workspace_permission_control'; + +// Can't throw unauthorized for now, the page will be refreshed if unauthorized +const generateWorkspacePermissionError = () => + Boom.illegal( + i18n.translate('workspace.permission.invalidate', { + defaultMessage: 'Invalidate workspace permission', + }) + ); + +interface AttributesWithWorkspaces { + workspaces: string[]; +} + +const isWorkspacesLikeAttributes = (attributes: unknown): attributes is AttributesWithWorkspaces => + typeof attributes === 'object' && + !!attributes && + attributes.hasOwnProperty('workspaces') && + Array.isArray((attributes as { workspaces: unknown }).workspaces); + +export class WorkspaceSavedObjectsClientWrapper { + private async validateMultiWorkspacesPermissions( + workspaces: string[] | undefined, + request: OpenSearchDashboardsRequest, + permissionMode: WorkspacePermissionMode | WorkspacePermissionMode[] + ) { + if (!workspaces) { + return; + } + for (const workspaceId of workspaces) { + if (!(await this.permissionControl.validate(workspaceId, permissionMode, request))) { + throw generateWorkspacePermissionError(); + } + } + } + + private async validateAtLeastOnePermittedWorkspaces( + workspaces: string[] | undefined, + request: OpenSearchDashboardsRequest, + permissionMode: WorkspacePermissionMode | WorkspacePermissionMode[] + ) { + if (!workspaces) { + return; + } + let permitted = false; + for (const workspaceId of workspaces) { + if (await this.permissionControl.validate(workspaceId, permissionMode, request)) { + permitted = true; + break; + } + } + if (!permitted) { + throw generateWorkspacePermissionError(); + } + } + + public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { + const deleteWithWorkspacePermissionControl = async ( + type: string, + id: string, + options: SavedObjectsDeleteOptions = {} + ) => { + const objectToDeleted = await wrapperOptions.client.get(type, id, options); + await this.validateMultiWorkspacesPermissions( + objectToDeleted.workspaces, + wrapperOptions.request, + WorkspacePermissionMode.Admin + ); + return await wrapperOptions.client.delete(type, id, options); + }; + + const bulkCreateWithWorkspacePermissionControl = async ( + objects: Array>, + options: SavedObjectsCreateOptions = {} + ): Promise> => { + return await wrapperOptions.client.bulkCreate(objects, options); + }; + + const createWithWorkspacePermissionControl = async ( + type: string, + attributes: T, + options?: SavedObjectsCreateOptions + ) => { + if (isWorkspacesLikeAttributes(attributes)) { + await this.validateMultiWorkspacesPermissions( + attributes.workspaces, + wrapperOptions.request, + WorkspacePermissionMode.Admin + ); + } + return await wrapperOptions.client.create(type, attributes, options); + }; + + const getWithWorkspacePermissionControl = async ( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ): Promise> => { + const objectToGet = await wrapperOptions.client.get(type, id, options); + await this.validateAtLeastOnePermittedWorkspaces( + objectToGet.workspaces, + wrapperOptions.request, + WorkspacePermissionMode.Read + ); + return objectToGet; + }; + + const bulkGetWithWorkspacePermissionControl = async ( + objects: SavedObjectsBulkGetObject[] = [], + options: SavedObjectsBaseOptions = {} + ): Promise> => { + const objectToBulkGet = await wrapperOptions.client.bulkGet(objects, options); + for (const object of objectToBulkGet.saved_objects) { + await this.validateAtLeastOnePermittedWorkspaces( + object.workspaces, + wrapperOptions.request, + WorkspacePermissionMode.Read + ); + } + return objectToBulkGet; + }; + + const findWithWorkspacePermissionControl = async ( + options: SavedObjectsFindOptions + ) => { + if (options.workspaces) { + options.workspaces = options.workspaces.filter( + async (workspaceId) => + await this.permissionControl.validate( + workspaceId, + WorkspacePermissionMode.Read, + wrapperOptions.request + ) + ); + } else { + options.workspaces = [ + 'public', + ...(await this.permissionControl.getPermittedWorkspaceIds( + WorkspacePermissionMode.Read, + wrapperOptions.request + )), + ]; + } + return await wrapperOptions.client.find(options); + }; + + return { + ...wrapperOptions.client, + get: getWithWorkspacePermissionControl, + checkConflicts: wrapperOptions.client.checkConflicts, + find: findWithWorkspacePermissionControl, + bulkGet: bulkGetWithWorkspacePermissionControl, + errors: wrapperOptions.client.errors, + addToNamespaces: wrapperOptions.client.addToNamespaces, + deleteFromNamespaces: wrapperOptions.client.deleteFromNamespaces, + create: createWithWorkspacePermissionControl, + bulkCreate: bulkCreateWithWorkspacePermissionControl, + delete: deleteWithWorkspacePermissionControl, + update: wrapperOptions.client.update, + bulkUpdate: wrapperOptions.client.bulkUpdate, + }; + }; + + constructor(private readonly permissionControl: WorkspacePermissionControl) {} +} diff --git a/src/core/server/workspaces/workspace_permission_control.ts b/src/core/server/workspaces/workspace_permission_control.ts index bf85562c466..203ce354561 100644 --- a/src/core/server/workspaces/workspace_permission_control.ts +++ b/src/core/server/workspaces/workspace_permission_control.ts @@ -19,5 +19,12 @@ export class WorkspacePermissionControl { return true; } + public async getPermittedWorkspaceIds( + permissionModeOrModes: WorkspacePermissionMode | WorkspacePermissionMode[], + request: OpenSearchDashboardsRequest + ) { + return []; + } + public async setup() {} } diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index b25d1e9e102..30be3b9d752 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -16,6 +16,7 @@ import { IWorkspaceDBImpl } from './types'; import { WorkspacesClientWithSavedObject } from './workspaces_client'; import { WorkspacePermissionControl } from './workspace_permission_control'; import { UiSettingsServiceStart } from '../ui_settings/types'; +import { WorkspaceSavedObjectsClientWrapper } from './saved_objects'; export interface WorkspacesServiceSetup { client: IWorkspaceDBImpl; @@ -90,6 +91,15 @@ export class WorkspacesService await this.client.setup(setupDeps); await this.permissionControl.setup(); + const workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper( + this.permissionControl + ); + + setupDeps.savedObject.addClientWrapper( + 0, + 'workspace', + workspaceSavedObjectsClientWrapper.wrapperFactory + ); this.proxyWorkspaceTrafficToRealHandler(setupDeps);