Skip to content

Commit

Permalink
[Workspace] Support workspace in saved objects client in server side. (
Browse files Browse the repository at this point in the history
…opensearch-project#6365)

* Support workspace in saved objects client in server side. (opensearch-project#293)

* feat: POC implementation

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: add some comment

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: revert dependency

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: update comment

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: address one TODO

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: address TODO

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: add unit test

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: some special logic on specific operation

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: add integration test

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: declare workspaces as empty array for advanced settings

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: unified workspaces parameters when parsing from router

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: improve code coverage

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: declare workspaces as null

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: use unified types

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: update comment

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: remove null

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: address comments

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: use request app to store request workspace id

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: use app state to store request workspace id

Signed-off-by: SuZhou-Joe <[email protected]>

* refact: update types declaration

Signed-off-by: SuZhou-Joe <[email protected]>

* fix: unit test error

Signed-off-by: SuZhou-Joe <[email protected]>

---------

Signed-off-by: SuZhou-Joe <[email protected]>

* fix: import error

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: add integration test

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: add unit test

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: update CHANGELOG

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: use consts and add comment

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: change the priority value

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: update

Signed-off-by: SuZhou-Joe <[email protected]>

---------

Signed-off-by: SuZhou-Joe <[email protected]>
  • Loading branch information
SuZhou-Joe authored Apr 12, 2024
1 parent 7eda01a commit 3b03fa9
Show file tree
Hide file tree
Showing 25 changed files with 628 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Multiple Datasource] Refactor data source selector component to include placeholder and add tests ([#6372](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6372))
- [Dynamic Configurations] Improve dynamic configurations by adding cache and simplifying client fetch ([#6364](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6364))
- [MD] Add OpenSearch cluster group label to top of single selectable dropdown ([#6400](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6400))
- [Workspace] Support workspace in saved objects client in server side. ([#6365](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6365))

### 🐛 Bug Fixes

Expand Down
5 changes: 3 additions & 2 deletions src/core/public/saved_objects/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
SavedObjectsClientContract as SavedObjectsApi,
SavedObjectsFindOptions as SavedObjectFindOptionsServer,
SavedObjectsMigrationVersion,
SavedObjectsBaseOptions,
} from '../../server';

import { SimpleSavedObject } from './simple_saved_object';
Expand Down Expand Up @@ -65,7 +66,7 @@ export interface SavedObjectsCreateOptions {
/** {@inheritDoc SavedObjectsMigrationVersion} */
migrationVersion?: SavedObjectsMigrationVersion;
references?: SavedObjectReference[];
workspaces?: string[];
workspaces?: SavedObjectsBaseOptions['workspaces'];
}

/**
Expand All @@ -83,7 +84,7 @@ export interface SavedObjectsBulkCreateObject<T = unknown> 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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

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

/**
Expand Down Expand Up @@ -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`);
Expand Down
5 changes: 3 additions & 2 deletions src/core/server/saved_objects/import/check_conflicts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
SavedObjectsImportError,
SavedObjectError,
SavedObjectsImportRetry,
SavedObjectsBaseOptions,
} from '../types';

interface CheckConflictsParams {
Expand All @@ -44,7 +45,7 @@ interface CheckConflictsParams {
ignoreRegularConflicts?: boolean;
retries?: SavedObjectsImportRetry[];
createNewCopies?: boolean;
workspaces?: string[];
workspaces?: SavedObjectsBaseOptions['workspaces'];
}

const isUnresolvableConflict = (error: SavedObjectError) =>
Expand Down Expand Up @@ -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),
Expand Down
10 changes: 10 additions & 0 deletions src/core/server/saved_objects/import/create_saved_objects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,4 +660,14 @@ describe('#createSavedObjects', () => {
});
});
});

describe('with a undefined workspaces', () => {
test('calls bulkCreate once with input objects', async () => {
const options = setupParams({ objects: objs });
setupMockResults(options);

await createSavedObjects(options);
expect(bulkCreate.mock.calls[0][1]?.hasOwnProperty('workspaces')).toEqual(false);
});
});
});
11 changes: 8 additions & 3 deletions src/core/server/saved_objects/import/create_saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
import { extractVegaSpecFromSavedObject, updateDataSourceNameInVegaSpec } from './utils';
Expand All @@ -42,7 +47,7 @@ interface CreateSavedObjectsParams<T> {
overwrite?: boolean;
dataSourceId?: string;
dataSourceTitle?: string;
workspaces?: string[];
workspaces?: SavedObjectsBaseOptions['workspaces'];
}
interface CreateSavedObjectsResult<T> {
createdObjects: Array<CreatedObject<T>>;
Expand Down Expand Up @@ -199,7 +204,7 @@ export const createSavedObjects = async <T>({
const bulkCreateResponse = await savedObjectsClient.bulkCreate(objectsToCreate, {
namespace,
overwrite,
workspaces,
...(workspaces ? { workspaces } : {}),
});
expectedResults = bulkCreateResponse.saved_objects;
}
Expand Down
6 changes: 3 additions & 3 deletions src/core/server/saved_objects/import/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/

import { Readable } from 'stream';
import { SavedObjectsClientContract, SavedObject } from '../types';
import { SavedObjectsClientContract, SavedObject, SavedObjectsBaseOptions } from '../types';
import { ISavedObjectTypeRegistry } from '..';

/**
Expand Down Expand Up @@ -190,7 +190,7 @@ export interface SavedObjectsImportOptions {
dataSourceId?: string;
dataSourceTitle?: string;
/** if specified, will import in given workspaces */
workspaces?: string[];
workspaces?: SavedObjectsBaseOptions['workspaces'];
}

/**
Expand All @@ -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<T> = SavedObject<T> & { destinationId?: string };
2 changes: 1 addition & 1 deletion src/core/server/saved_objects/routes/bulk_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
})
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/saved_objects/routes/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/saved_objects/routes/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig)
exportSizeLimit: maxImportExportSize,
includeReferencesDeep,
excludeExportDetails,
workspaces,
...(workspaces ? { workspaces } : {}),
});

const docsToExport: string[] = await createPromiseFromStreams([
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/saved_objects/routes/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
6 changes: 3 additions & 3 deletions src/core/server/saved_objects/serialization/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -53,7 +53,7 @@ export interface SavedObjectsRawDocSource {
updated_at?: string;
references?: SavedObjectReference[];
originId?: string;
workspaces?: string[];
workspaces?: SavedObject['workspaces'];

[typeMapping: string]: any;
}
Expand All @@ -71,7 +71,7 @@ interface SavedObjectDoc<T = unknown> {
version?: string;
updated_at?: string;
originId?: string;
workspaces?: string[];
workspaces?: SavedObject['workspaces'];
permissions?: Permissions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ interface QueryParams {
defaultSearchOperator?: string;
hasReference?: HasReferenceQueryParams;
kueryNode?: KueryNode;
workspaces?: string[];
workspaces?: SavedObjectsFindOptions['workspaces'];
workspacesSearchOperator?: 'AND' | 'OR';
ACLSearchParams?: SavedObjectsFindOptions['ACLSearchParams'];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ interface GetSearchDslOptions {
id: string;
};
kueryNode?: KueryNode;
workspaces?: string[];
workspaces?: SavedObjectsFindOptions['workspaces'];
workspacesSearchOperator?: 'AND' | 'OR';
ACLSearchParams?: SavedObjectsFindOptions['ACLSearchParams'];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -101,7 +97,7 @@ export interface SavedObjectsBulkCreateObject<T = unknown> {
/**
* workspaces the objects belong to, will only be used when overwrite is enabled.
*/
workspaces?: string[];
workspaces?: SavedObject['workspaces'];
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/core/server/saved_objects/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
/**
Expand All @@ -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;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions src/plugins/workspace/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ export enum WorkspacePermissionMode {
LibraryRead = 'library_read',
LibraryWrite = 'library_write',
}

export const WORKSPACE_ID_CONSUMER_WRAPPER_ID = 'workspace_id_consumer';

/**
* The priority for these wrappers matters:
* 1. WORKSPACE_ID_CONSUMER should be placed before the other two wrappers(smaller than the other two wrappers) as it cost little
* and will append the essential workspaces field into the options, which will be honored by permission control wrapper and conflict wrapper.
* 2. The order of permission wrapper and conflict wrapper does not matter as no dependency between these two wrappers.
*/
export const PRIORITY_FOR_WORKSPACE_ID_CONSUMER_WRAPPER = -2;
export const PRIORITY_FOR_PERMISSION_CONTROL_WRAPPER = 0;
export const PRIORITY_FOR_WORKSPACE_CONFLICT_CONTROL_WRAPPER = -1;
65 changes: 64 additions & 1 deletion src/plugins/workspace/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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();
});
});
Loading

0 comments on commit 3b03fa9

Please sign in to comment.