diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index f415e9c58596..d40b91188cfc 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -38,6 +38,7 @@ import { SavedObjectsClientContract as SavedObjectsApi, SavedObjectsFindOptions as SavedObjectFindOptionsServer, SavedObjectsMigrationVersion, + SavedObjectsBaseOptions, } from '../../server'; import { SimpleSavedObject } from './simple_saved_object'; @@ -65,7 +66,7 @@ export interface SavedObjectsCreateOptions { /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; - workspaces?: string[]; + workspaces?: SavedObjectsBaseOptions['workspaces']; } /** @@ -83,7 +84,7 @@ export interface SavedObjectsBulkCreateObject extends SavedObjectsC export interface SavedObjectsBulkCreateOptions { /** If a document with the given `id` already exists, overwrite it's contents (default=false). */ overwrite?: boolean; - workspaces?: string[]; + workspaces?: SavedObjectsCreateOptions['workspaces']; } /** @public */ diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index 0336fc702973..44595b0c33ff 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -30,7 +30,7 @@ import Boom from '@hapi/boom'; import { createListStream } from '../../utils/streams'; -import { SavedObjectsClientContract, SavedObject } from '../types'; +import { SavedObjectsClientContract, SavedObject, SavedObjectsBaseOptions } from '../types'; import { fetchNestedDependencies } from './inject_nested_depdendencies'; import { sortObjects } from './sort_objects'; @@ -61,7 +61,7 @@ export interface SavedObjectsExportOptions { /** optional namespace to override the namespace used by the savedObjectsClient. */ namespace?: string; /** optional workspaces to override the workspaces used by the savedObjectsClient. */ - workspaces?: string[]; + workspaces?: SavedObjectsBaseOptions['workspaces']; } /** @@ -97,7 +97,7 @@ async function fetchObjectsToExport({ exportSizeLimit: number; savedObjectsClient: SavedObjectsClientContract; namespace?: string; - workspaces?: string[]; + workspaces?: SavedObjectsExportOptions['workspaces']; }) { if ((types?.length ?? 0) > 0 && (objects?.length ?? 0) > 0) { throw Boom.badRequest(`Can't specify both "types" and "objects" properties when exporting`); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index f36bcf3a8a92..7afab618b1ff 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -35,6 +35,7 @@ import { SavedObjectsImportError, SavedObjectError, SavedObjectsImportRetry, + SavedObjectsBaseOptions, } from '../types'; interface CheckConflictsParams { @@ -44,7 +45,7 @@ interface CheckConflictsParams { ignoreRegularConflicts?: boolean; retries?: SavedObjectsImportRetry[]; createNewCopies?: boolean; - workspaces?: string[]; + workspaces?: SavedObjectsBaseOptions['workspaces']; } const isUnresolvableConflict = (error: SavedObjectError) => @@ -79,7 +80,7 @@ export async function checkConflicts({ }); const checkConflictsResult = await savedObjectsClient.checkConflicts(objectsToCheck, { namespace, - workspaces, + ...(workspaces ? { workspaces } : {}), }); const errorMap = checkConflictsResult.errors.reduce( (acc, { type, id, error }) => acc.set(`${type}:${id}`, error), diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index 6b0015851baf..35d0ddd349fa 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -28,7 +28,12 @@ * under the License. */ -import { SavedObject, SavedObjectsClientContract, SavedObjectsImportError } from '../types'; +import { + SavedObject, + SavedObjectsBaseOptions, + SavedObjectsClientContract, + SavedObjectsImportError, +} from '../types'; import { extractErrors } from './extract_errors'; import { CreatedObject } from './types'; @@ -41,7 +46,7 @@ interface CreateSavedObjectsParams { overwrite?: boolean; dataSourceId?: string; dataSourceTitle?: string; - workspaces?: string[]; + workspaces?: SavedObjectsBaseOptions['workspaces']; } interface CreateSavedObjectsResult { createdObjects: Array>; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 994b7e627189..a243e08f83e0 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -29,7 +29,7 @@ */ import { Readable } from 'stream'; -import { SavedObjectsClientContract, SavedObject } from '../types'; +import { SavedObjectsClientContract, SavedObject, SavedObjectsBaseOptions } from '../types'; import { ISavedObjectTypeRegistry } from '..'; /** @@ -190,7 +190,7 @@ export interface SavedObjectsImportOptions { dataSourceId?: string; dataSourceTitle?: string; /** if specified, will import in given workspaces */ - workspaces?: string[]; + workspaces?: SavedObjectsBaseOptions['workspaces']; } /** @@ -215,7 +215,7 @@ export interface SavedObjectsResolveImportErrorsOptions { dataSourceId?: string; dataSourceTitle?: string; /** if specified, will import in given workspaces */ - workspaces?: string[]; + workspaces?: SavedObjectsBaseOptions['workspaces']; } export type CreatedObject = SavedObject & { destinationId?: string }; diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 056b1b795550..b8a020b4ea24 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -70,7 +70,7 @@ export const registerBulkCreateRoute = (router: IRouter) => { : undefined; const result = await context.core.savedObjects.client.bulkCreate(req.body, { overwrite, - workspaces, + ...(workspaces ? { workspaces } : {}), }); return res.ok({ body: result }); }) diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index 4d22bd244a03..2a7958aa9b78 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -71,7 +71,7 @@ export const registerCreateRoute = (router: IRouter) => { migrationVersion, references, initialNamespaces, - workspaces, + ...(workspaces ? { workspaces } : {}), }; const result = await context.core.savedObjects.client.create(type, attributes, options); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 9325b632e40f..4d9d5b7e8ec3 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -106,7 +106,7 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) exportSizeLimit: maxImportExportSize, includeReferencesDeep, excludeExportDetails, - workspaces, + ...(workspaces ? { workspaces } : {}), }); const docsToExport: string[] = await createPromiseFromStreams([ diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index 36fa7c2cd9f5..42b47cf950fc 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -85,7 +85,7 @@ export const registerFindRoute = (router: IRouter) => { fields: typeof query.fields === 'string' ? [query.fields] : query.fields, filter: query.filter, namespaces, - workspaces, + ...(workspaces ? { workspaces } : {}), }); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index f882596ce529..6b1f025e856d 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -29,7 +29,7 @@ */ import { Permissions } from '../permission_control'; -import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types'; +import { SavedObjectsMigrationVersion, SavedObjectReference, SavedObject } from '../types'; /** * A raw document as represented directly in the saved object index. @@ -53,7 +53,7 @@ export interface SavedObjectsRawDocSource { updated_at?: string; references?: SavedObjectReference[]; originId?: string; - workspaces?: string[]; + workspaces?: SavedObject['workspaces']; [typeMapping: string]: any; } @@ -71,7 +71,7 @@ interface SavedObjectDoc { version?: string; updated_at?: string; originId?: string; - workspaces?: string[]; + workspaces?: SavedObject['workspaces']; permissions?: Permissions; } diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index abbef0850dba..318768fd83c2 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -167,7 +167,7 @@ interface QueryParams { defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; kueryNode?: KueryNode; - workspaces?: string[]; + workspaces?: SavedObjectsFindOptions['workspaces']; workspacesSearchOperator?: 'AND' | 'OR'; ACLSearchParams?: SavedObjectsFindOptions['ACLSearchParams']; } diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index fa4311576638..626fc7efba3e 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -53,7 +53,7 @@ interface GetSearchDslOptions { id: string; }; kueryNode?: KueryNode; - workspaces?: string[]; + workspaces?: SavedObjectsFindOptions['workspaces']; workspacesSearchOperator?: 'AND' | 'OR'; ACLSearchParams?: SavedObjectsFindOptions['ACLSearchParams']; } 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 e1c3d16a9258..be02ef247d04 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -69,10 +69,6 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { * Note: this can only be used for multi-namespace object types. */ initialNamespaces?: string[]; - /** - * workspaces the new created objects belong to - */ - workspaces?: string[]; /** permission control describe by ACL object */ permissions?: Permissions; } @@ -101,7 +97,7 @@ export interface SavedObjectsBulkCreateObject { /** * workspaces the objects belong to, will only be used when overwrite is enabled. */ - workspaces?: string[]; + workspaces?: SavedObject['workspaces']; } /** diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index d21421dbe253..369cbfd53bf4 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -112,7 +112,7 @@ export interface SavedObjectsFindOptions { /** An optional OpenSearch preference value to be used for the query **/ preference?: string; /** If specified, will only retrieve objects that are in the workspaces */ - workspaces?: string[]; + workspaces?: SavedObjectsBaseOptions['workspaces']; /** By default the operator will be 'AND' */ workspacesSearchOperator?: 'AND' | 'OR'; /** @@ -132,7 +132,7 @@ export interface SavedObjectsBaseOptions { /** Specify the namespace for this operation */ namespace?: string; /** Specify the workspaces for this operation */ - workspaces?: string[]; + workspaces?: SavedObject['workspaces'] | null; } /** diff --git a/src/legacy/server/osd_server.d.ts b/src/legacy/server/osd_server.d.ts index 9d94349cf1c0..1f8535b59e87 100644 --- a/src/legacy/server/osd_server.d.ts +++ b/src/legacy/server/osd_server.d.ts @@ -60,14 +60,6 @@ declare module 'hapi' { } } -declare module '@hapi/hapi' { - interface PluginsStates { - workspace?: { - id?: string; - }; - } -} - type OsdMixinFunc = (osdServer: OsdServer, server: Server, config: any) => Promise | void; export interface PluginsSetup { diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 2be4a9d121db..1781b7e32771 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -20,3 +20,5 @@ export enum WorkspacePermissionMode { LibraryRead = 'library_read', LibraryWrite = 'library_write', } + +export const WORKSPACE_ID_CONSUMER_WRAPPER_ID = 'workspace_id_consumer'; diff --git a/src/plugins/workspace/server/plugin.test.ts b/src/plugins/workspace/server/plugin.test.ts index 684f754ce9dd..0ad72b51b6dc 100644 --- a/src/plugins/workspace/server/plugin.test.ts +++ b/src/plugins/workspace/server/plugin.test.ts @@ -3,8 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { coreMock } from '../../../core/server/mocks'; +import { OnPreRoutingHandler } from 'src/core/server'; +import { coreMock, httpServerMock } from '../../../core/server/mocks'; import { WorkspacePlugin } from './plugin'; +import { getWorkspaceState } from '../../../core/server/utils'; describe('Workspace server plugin', () => { it('#setup', async () => { @@ -24,5 +26,66 @@ describe('Workspace server plugin', () => { }, } `); + expect(setupMock.savedObjects.addClientWrapper).toBeCalledTimes(3); + }); + + it('#proxyWorkspaceTrafficToRealHandler', async () => { + const setupMock = coreMock.createSetup(); + const initializerContextConfigMock = coreMock.createPluginInitializerContext({ + enabled: true, + permission: { + enabled: true, + }, + }); + let onPreRoutingFn: OnPreRoutingHandler = () => httpServerMock.createResponseFactory().ok(); + setupMock.http.registerOnPreRouting.mockImplementation((fn) => { + onPreRoutingFn = fn; + return fn; + }); + const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock); + await workspacePlugin.setup(setupMock); + const toolKitMock = httpServerMock.createToolkit(); + + const requestWithWorkspaceInUrl = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/w/foo/app', + }); + onPreRoutingFn(requestWithWorkspaceInUrl, httpServerMock.createResponseFactory(), toolKitMock); + expect(toolKitMock.rewriteUrl).toBeCalledWith('http://localhost/app'); + expect(toolKitMock.next).toBeCalledTimes(0); + expect(getWorkspaceState(requestWithWorkspaceInUrl)).toEqual({ + requestWorkspaceId: 'foo', + }); + + const requestWithoutWorkspaceInUrl = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/app', + }); + onPreRoutingFn( + requestWithoutWorkspaceInUrl, + httpServerMock.createResponseFactory(), + toolKitMock + ); + expect(toolKitMock.next).toBeCalledTimes(1); + }); + + it('#start', async () => { + const setupMock = coreMock.createSetup(); + const startMock = coreMock.createStart(); + const initializerContextConfigMock = coreMock.createPluginInitializerContext({ + enabled: true, + permission: { + enabled: true, + }, + }); + + const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock); + await workspacePlugin.setup(setupMock); + await workspacePlugin.start(startMock); + expect(startMock.savedObjects.createSerializer).toBeCalledTimes(1); + }); + + it('#stop', () => { + const initializerContextConfigMock = coreMock.createPluginInitializerContext(); + const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock); + workspacePlugin.stop(); }); }); diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 8e3da6a8cfe5..bd0b32ce62a0 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -16,17 +16,23 @@ import { import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID, + WORKSPACE_ID_CONSUMER_WRAPPER_ID, } from '../common/constants'; -import { cleanWorkspaceId, getWorkspaceIdFromUrl } from '../../../core/server/utils'; import { IWorkspaceClientImpl, WorkspacePluginSetup, WorkspacePluginStart } from './types'; import { WorkspaceClient } from './workspace_client'; import { registerRoutes } from './routes'; import { WorkspaceSavedObjectsClientWrapper } from './saved_objects'; +import { + cleanWorkspaceId, + getWorkspaceIdFromUrl, + updateWorkspaceState, +} from '../../../core/server/utils'; import { WorkspaceConflictSavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper_for_check_workspace_conflict'; import { SavedObjectsPermissionControl, SavedObjectsPermissionControlContract, } from './permission_control/client'; +import { WorkspaceIdConsumerWrapper } from './saved_objects/workspace_id_consumer_wrapper'; export class WorkspacePlugin implements Plugin { private readonly logger: Logger; @@ -47,6 +53,9 @@ export class WorkspacePlugin implements Plugin = { + type: 'dashboard', + attributes: {}, + references: [], +}; + +interface WorkspaceAttributes { + id: string; + name?: string; +} + +describe('workspace_id_consumer integration test', () => { + let root: ReturnType; + let opensearchServer: osdTestServer.TestOpenSearchUtils; + let createdFooWorkspace: WorkspaceAttributes = { + id: '', + }; + let createdBarWorkspace: WorkspaceAttributes = { + id: '', + }; + beforeAll(async () => { + const { startOpenSearch, startOpenSearchDashboards } = osdTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + osd: { + workspace: { + enabled: true, + }, + savedObjects: { + permission: { + enabled: true, + }, + }, + migrations: { + skip: false, + }, + }, + }, + }); + opensearchServer = await startOpenSearch(); + const startOSDResp = await startOpenSearchDashboards(); + root = startOSDResp.root; + const createWorkspace = (workspaceAttribute: Omit) => + osdTestServer.request.post(root, `/api/workspaces`).send({ + attributes: workspaceAttribute, + }); + + createdFooWorkspace = await createWorkspace({ + name: 'foo', + }).then((resp) => { + return resp.body.result; + }); + createdBarWorkspace = await createWorkspace({ + name: 'bar', + }).then((resp) => resp.body.result); + }, 30000); + afterAll(async () => { + await root.shutdown(); + await opensearchServer.stop(); + }); + + const deleteItem = async (object: Pick) => { + expect( + [200, 404].includes( + (await osdTestServer.request.delete(root, `/api/saved_objects/${object.type}/${object.id}`)) + .statusCode + ) + ).toEqual(true); + }; + + const getItem = async (object: Pick) => { + return await osdTestServer.request + .get(root, `/api/saved_objects/${object.type}/${object.id}`) + .expect(200); + }; + + const clearFooAndBar = async () => { + await deleteItem({ + type: dashboard.type, + id: 'foo', + }); + await deleteItem({ + type: dashboard.type, + id: 'bar', + }); + }; + + describe('saved objects client related CRUD', () => { + it('create', async () => { + const createResult = await osdTestServer.request + .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/${dashboard.type}`) + .send({ + attributes: dashboard.attributes, + }) + .expect(200); + + expect(createResult.body.workspaces).toEqual([createdFooWorkspace.id]); + await deleteItem({ + type: dashboard.type, + id: createResult.body.id, + }); + }); + + it('bulk create', async () => { + await clearFooAndBar(); + const createResultFoo = await osdTestServer.request + .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + expect((createResultFoo.body.saved_objects as any[]).some((item) => item.error)).toEqual( + false + ); + expect( + (createResultFoo.body.saved_objects as any[]).every((item) => + isEqual(item.workspaces, [createdFooWorkspace.id]) + ) + ).toEqual(true); + await Promise.all( + [...createResultFoo.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + }); + + it('checkConflicts when importing ndjson', async () => { + await clearFooAndBar(); + const createResultFoo = await osdTestServer.request + .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + const createResultBar = await osdTestServer.request + .post(root, `/w/${createdBarWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'bar', + }, + ]) + .expect(200); + + const getResultFoo = await getItem({ + type: dashboard.type, + id: 'foo', + }); + const getResultBar = await getItem({ + type: dashboard.type, + id: 'bar', + }); + + /** + * import with workspaces when conflicts + */ + const importWithWorkspacesResult = await osdTestServer.request + .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_import?overwrite=false`) + .attach( + 'file', + Buffer.from( + [JSON.stringify(getResultFoo.body), JSON.stringify(getResultBar.body)].join('\n'), + 'utf-8' + ), + 'tmp.ndjson' + ) + .expect(200); + + expect(importWithWorkspacesResult.body.success).toEqual(false); + expect(importWithWorkspacesResult.body.errors.length).toEqual(1); + expect(importWithWorkspacesResult.body.errors[0].id).toEqual('foo'); + expect(importWithWorkspacesResult.body.errors[0].error.type).toEqual('conflict'); + + await Promise.all( + [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + }); + + it('find by workspaces', async () => { + const createResultFoo = await osdTestServer.request + .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + const createResultBar = await osdTestServer.request + .post(root, `/w/${createdBarWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'bar', + }, + ]) + .expect(200); + + const findResult = await osdTestServer.request + .get(root, `/w/${createdBarWorkspace.id}/api/saved_objects/_find?type=${dashboard.type}`) + .expect(200); + + expect(findResult.body.total).toEqual(1); + expect(findResult.body.saved_objects[0].workspaces).toEqual([createdBarWorkspace.id]); + + await Promise.all( + [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + }); + }); +}); diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts new file mode 100644 index 000000000000..112f31baf562 --- /dev/null +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { updateWorkspaceState } from '../../../../core/server/utils'; +import { SavedObject } from '../../../../core/public'; +import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks'; +import { WorkspaceIdConsumerWrapper } from './workspace_id_consumer_wrapper'; + +describe('WorkspaceIdConsumerWrapper', () => { + const requestHandlerContext = coreMock.createRequestHandlerContext(); + const wrapperInstance = new WorkspaceIdConsumerWrapper(); + const mockedClient = savedObjectsClientMock.create(); + const workspaceEnabledMockRequest = httpServerMock.createOpenSearchDashboardsRequest(); + updateWorkspaceState(workspaceEnabledMockRequest, { + requestWorkspaceId: 'foo', + }); + const wrapperClient = wrapperInstance.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: workspaceEnabledMockRequest, + }); + const getSavedObject = (savedObject: Partial) => { + const payload: SavedObject = { + references: [], + id: '', + type: 'dashboard', + attributes: {}, + ...savedObject, + }; + + return payload; + }; + describe('create', () => { + beforeEach(() => { + mockedClient.create.mockClear(); + }); + it(`Should add workspaces parameters when create`, async () => { + await wrapperClient.create('dashboard', { + name: 'foo', + }); + + expect(mockedClient.create).toBeCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + workspaces: ['foo'], + }) + ); + }); + + it(`Should not use options.workspaces when there is no workspaces inside options`, async () => { + await wrapperClient.create( + 'dashboard', + { + name: 'foo', + }, + { + id: 'dashboard:foo', + overwrite: true, + workspaces: null, + } + ); + + expect(mockedClient.create.mock.calls[0][2]?.hasOwnProperty('workspaces')).toEqual(false); + }); + }); + + describe('bulkCreate', () => { + beforeEach(() => { + mockedClient.bulkCreate.mockClear(); + }); + it(`Should add workspaces parameters when bulk create`, async () => { + await wrapperClient.bulkCreate([ + getSavedObject({ + id: 'foo', + }), + ]); + + expect(mockedClient.bulkCreate).toBeCalledWith( + [{ attributes: {}, id: 'foo', references: [], type: 'dashboard' }], + { + workspaces: ['foo'], + } + ); + }); + }); + + describe('checkConflict', () => { + beforeEach(() => { + mockedClient.checkConflicts.mockClear(); + }); + + it(`Should add workspaces parameters when checkConflict`, async () => { + await wrapperClient.checkConflicts(); + expect(mockedClient.checkConflicts).toBeCalledWith([], { + workspaces: ['foo'], + }); + }); + }); + + describe('find', () => { + beforeEach(() => { + mockedClient.find.mockClear(); + }); + + it(`Should add workspaces parameters when find`, async () => { + await wrapperClient.find({ + type: 'dashboard', + }); + expect(mockedClient.find).toBeCalledWith({ + type: 'dashboard', + workspaces: ['foo'], + }); + }); + }); +}); diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts new file mode 100644 index 000000000000..74e8e99af71e --- /dev/null +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getWorkspaceState } from '../../../../core/server/utils'; +import { + SavedObjectsBaseOptions, + SavedObjectsBulkCreateObject, + SavedObjectsClientWrapperFactory, + SavedObjectsCreateOptions, + SavedObjectsCheckConflictsObject, + OpenSearchDashboardsRequest, + SavedObjectsFindOptions, +} from '../../../../core/server'; + +type WorkspaceOptions = Pick | undefined; + +export class WorkspaceIdConsumerWrapper { + private formatWorkspaceIdParams( + request: OpenSearchDashboardsRequest, + options?: T + ): T { + const { workspaces, ...others } = options || {}; + const workspaceState = getWorkspaceState(request); + const workspaceIdParsedFromRequest = workspaceState?.requestWorkspaceId; + const workspaceIdsInUserOptions = options?.workspaces; + let finalWorkspaces: string[] = []; + if (options?.hasOwnProperty('workspaces')) { + finalWorkspaces = workspaceIdsInUserOptions || []; + } else if (workspaceIdParsedFromRequest) { + finalWorkspaces = [workspaceIdParsedFromRequest]; + } + + return { + ...(others as T), + ...(finalWorkspaces.length ? { workspaces: finalWorkspaces } : {}), + }; + } + public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { + return { + ...wrapperOptions.client, + create: (type: string, attributes: T, options: SavedObjectsCreateOptions = {}) => + wrapperOptions.client.create( + type, + attributes, + this.formatWorkspaceIdParams(wrapperOptions.request, options) + ), + bulkCreate: ( + objects: Array>, + options: SavedObjectsCreateOptions = {} + ) => + wrapperOptions.client.bulkCreate( + objects, + this.formatWorkspaceIdParams(wrapperOptions.request, options) + ), + checkConflicts: ( + objects: SavedObjectsCheckConflictsObject[] = [], + options: SavedObjectsBaseOptions = {} + ) => + wrapperOptions.client.checkConflicts( + objects, + this.formatWorkspaceIdParams(wrapperOptions.request, options) + ), + delete: wrapperOptions.client.delete, + find: (options: SavedObjectsFindOptions) => + wrapperOptions.client.find(this.formatWorkspaceIdParams(wrapperOptions.request, options)), + bulkGet: wrapperOptions.client.bulkGet, + get: wrapperOptions.client.get, + update: wrapperOptions.client.update, + bulkUpdate: wrapperOptions.client.bulkUpdate, + addToNamespaces: wrapperOptions.client.addToNamespaces, + deleteFromNamespaces: wrapperOptions.client.deleteFromNamespaces, + deleteByWorkspace: wrapperOptions.client.deleteByWorkspace, + }; + }; + + constructor() {} +} 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 30c1c91c4223..4d5d03641b5f 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 @@ -459,6 +459,10 @@ export class WorkspaceSavedObjectsClientWrapper { [WorkspacePermissionMode.LibraryRead, WorkspacePermissionMode.LibraryWrite] ), }, + // By declaring workspaces as null, + // workspaces won't be appended automatically into the options. + // or workspaces can not be found because workspace object do not have `workspaces` field. + workspaces: null, }) ).saved_objects.map((item) => item.id); diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index f9da4130921b..79df5f5d4558 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -21,7 +21,10 @@ import { WorkspaceAttributeWithPermission } from '../../../core/types'; import { IWorkspaceClientImpl, WorkspaceFindOptions, IResponse, IRequestDetail } from './types'; import { workspace } from './saved_objects'; import { generateRandomId } from './utils'; -import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; +import { + WORKSPACE_ID_CONSUMER_WRAPPER_ID, + WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, +} from '../common/constants'; const WORKSPACE_ID_SIZE = 6; @@ -48,7 +51,15 @@ export class WorkspaceClient implements IWorkspaceClientImpl { requestDetail: IRequestDetail ): SavedObjectsClientContract | undefined { return this.savedObjects?.getScopedClient(requestDetail.request, { - excludedWrappers: [WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID], + excludedWrappers: [ + WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, + /** + * workspace object does not have workspaces field + * so need to bypass workspace id consumer wrapper + * for any kind of operation to saved objects client. + */ + WORKSPACE_ID_CONSUMER_WRAPPER_ID, + ], includedHiddenTypes: [WORKSPACE_TYPE], }); } @@ -57,6 +68,7 @@ export class WorkspaceClient implements IWorkspaceClientImpl { requestDetail: IRequestDetail ): SavedObjectsClientContract { return this.savedObjects?.getScopedClient(requestDetail.request, { + excludedWrappers: [WORKSPACE_ID_CONSUMER_WRAPPER_ID], includedHiddenTypes: [WORKSPACE_TYPE], }) as SavedObjectsClientContract; }