Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Workspace] Support workspace in saved objects client in server side. #6365

Merged
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
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[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the intent to remove this field?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is already a workspaces in SavedObjectsBaseOptions and we can delete this duplicate one.

/** 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
Loading