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

Support workspace in saved objects client in server side. #293

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
66d6096
feat: POC implementation
SuZhou-Joe Mar 13, 2024
d275f61
feat: add some comment
SuZhou-Joe Mar 13, 2024
65f6be3
feat: revert dependency
SuZhou-Joe Mar 13, 2024
9c5d2d9
feat: update comment
SuZhou-Joe Mar 13, 2024
d5638ff
feat: address one TODO
SuZhou-Joe Mar 22, 2024
45589cf
feat: address TODO
SuZhou-Joe Mar 22, 2024
5d990c8
feat: add unit test
SuZhou-Joe Mar 22, 2024
08dc9e2
feat: some special logic on specific operation
SuZhou-Joe Mar 22, 2024
d3cccb8
feat: add integration test
SuZhou-Joe Mar 22, 2024
298c2cb
feat: declare workspaces as empty array for advanced settings
SuZhou-Joe Mar 24, 2024
85c522d
feat: unified workspaces parameters when parsing from router
SuZhou-Joe Mar 25, 2024
cdbe9a8
feat: improve code coverage
SuZhou-Joe Mar 25, 2024
cb7ebd9
feat: declare workspaces as null
SuZhou-Joe Mar 27, 2024
294d2c5
feat: use unified types
SuZhou-Joe Mar 27, 2024
42e0e05
feat: update comment
SuZhou-Joe Mar 28, 2024
1b32316
feat: remove null
SuZhou-Joe Apr 1, 2024
b7a6cb7
feat: address comments
SuZhou-Joe Apr 2, 2024
dd6cebf
feat: use request app to store request workspace id
SuZhou-Joe Apr 3, 2024
7fd5160
feat: use app state to store request workspace id
SuZhou-Joe Apr 3, 2024
a5b8c70
feat: merge workspace-pr-integr
SuZhou-Joe Apr 7, 2024
d5ccfad
feat: merge
SuZhou-Joe Apr 7, 2024
431aa38
Merge branch 'workspace-pr-integr' into feature/current-workspace-in-…
SuZhou-Joe Apr 8, 2024
742ec22
refact: update types declaration
SuZhou-Joe Apr 8, 2024
ab0486e
fix: unit test error
SuZhou-Joe Apr 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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';

Expand All @@ -41,7 +46,7 @@ interface CreateSavedObjectsParams<T> {
overwrite?: boolean;
dataSourceId?: string;
dataSourceTitle?: string;
workspaces?: string[];
workspaces?: SavedObjectsBaseOptions['workspaces'];
}
interface CreateSavedObjectsResult<T> {
createdObjects: Array<CreatedObject<T>>;
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[];
SuZhou-Joe marked this conversation as resolved.
Show resolved Hide resolved
/** 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
8 changes: 0 additions & 8 deletions src/legacy/server/osd_server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> | void;

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

export const WORKSPACE_ID_CONSUMER_WRAPPER_ID = 'workspace_id_consumer';
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();
});
});
17 changes: 16 additions & 1 deletion src/plugins/workspace/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkspacePluginSetup, WorkspacePluginStart> {
private readonly logger: Logger;
Expand All @@ -47,6 +53,9 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
);

if (workspaceId) {
updateWorkspaceState(request, {
wanglam marked this conversation as resolved.
Show resolved Hide resolved
requestWorkspaceId: workspaceId,
});
const requestUrl = new URL(request.url.toString());
requestUrl.pathname = cleanWorkspaceId(requestUrl.pathname);
return toolkit.rewriteUrl(requestUrl.toString());
Expand Down Expand Up @@ -78,6 +87,12 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
this.workspaceConflictControl.wrapperFactory
);

core.savedObjects.addClientWrapper(
-2,
WORKSPACE_ID_CONSUMER_WRAPPER_ID,
new WorkspaceIdConsumerWrapper().wrapperFactory
);

this.logger.info('Workspace permission control enabled:' + isPermissionControlEnabled);
if (isPermissionControlEnabled) {
this.permissionControl = new SavedObjectsPermissionControl(this.logger);
Expand Down
Loading
Loading