Skip to content

Commit

Permalink
add workspace saved objects client wrapper (opensearch-project#51)
Browse files Browse the repository at this point in the history
* add workspace savedd objects client wrapper

Signed-off-by: Lin Wang <wonglam@amazon.com>

* feat: add more methods to saved objects client wrapper

Signed-off-by: Lin Wang <wonglam@amazon.com>

* feat: add findWithWorkspacePermissionControl in workspace saved objects client wrapper

Signed-off-by: Lin Wang <wonglam@amazon.com>

* feat: throw 451 instead of interval error

Signed-off-by: Lin Wang <wonglam@amazon.com>

* chore: fix workspace client init method type error

Signed-off-by: Lin Wang <wonglam@amazon.com>

* feat: fix workspaces attribute type error in client wrapper

Signed-off-by: Lin Wang <wonglam@amazon.com>

---------

Signed-off-by: Lin Wang <wonglam@amazon.com>
  • Loading branch information
wanglam authored and ruanyl committed Sep 15, 2023
1 parent 34b0bc5 commit 6f5c215
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/core/public/workspace/workspaces_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class WorkspacesClient {
/**
* Initialize workspace list
*/
init() {
public init() {
this.updateWorkspaceListAndNotify();
}

Expand Down
7 changes: 4 additions & 3 deletions src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt
*
* @public
*/
export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions {
export interface SavedObjectsDeleteByNamespaceOptions
extends Omit<SavedObjectsBaseOptions, 'workspaces'> {
/** The OpenSearch supports only boolean flag for this operation */
refresh?: boolean;
}
Expand Down Expand Up @@ -891,7 +892,7 @@ export class SavedObjectsRepository {
*/
async bulkGet<T = unknown>(
objects: SavedObjectsBulkGetObject[] = [],
options: SavedObjectsBaseOptions = {}
options: Omit<SavedObjectsBaseOptions, 'workspaces'> = {}
): Promise<SavedObjectsBulkResponse<T>> {
const namespace = normalizeNamespace(options.namespace);

Expand Down Expand Up @@ -979,7 +980,7 @@ export class SavedObjectsRepository {
async get<T = unknown>(
type: string,
id: string,
options: SavedObjectsBaseOptions = {}
options: Omit<SavedObjectsBaseOptions, 'workspaces'> = {}
): Promise<SavedObject<T>> {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export interface SavedObjectsBulkUpdateOptions extends SavedObjectsBaseOptions {
*
* @public
*/
export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions {
export interface SavedObjectsDeleteOptions extends Omit<SavedObjectsBaseOptions, 'workspaces'> {
/** The OpenSearch Refresh setting for this operation */
refresh?: MutatingOperationRefreshSetting;
/** Force deletion of an object that exists in multiple namespaces */
Expand Down
1 change: 1 addition & 0 deletions src/core/server/workspaces/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
*/

export { workspace } from './workspace';
export { WorkspaceSavedObjectsClientWrapper } from './workspace_saved_objects_client_wrapper';
Original file line number Diff line number Diff line change
@@ -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 <T = unknown>(
objects: Array<SavedObjectsBulkCreateObject<T>>,
options: SavedObjectsCreateOptions = {}
): Promise<SavedObjectsBulkResponse<T>> => {
return await wrapperOptions.client.bulkCreate(objects, options);
};

const createWithWorkspacePermissionControl = async <T = unknown>(
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 <T = unknown>(
type: string,
id: string,
options: SavedObjectsBaseOptions = {}
): Promise<SavedObject<T>> => {
const objectToGet = await wrapperOptions.client.get<T>(type, id, options);
await this.validateAtLeastOnePermittedWorkspaces(
objectToGet.workspaces,
wrapperOptions.request,
WorkspacePermissionMode.Read
);
return objectToGet;
};

const bulkGetWithWorkspacePermissionControl = async <T = unknown>(
objects: SavedObjectsBulkGetObject[] = [],
options: SavedObjectsBaseOptions = {}
): Promise<SavedObjectsBulkResponse<T>> => {
const objectToBulkGet = await wrapperOptions.client.bulkGet<T>(objects, options);
for (const object of objectToBulkGet.saved_objects) {
await this.validateAtLeastOnePermittedWorkspaces(
object.workspaces,
wrapperOptions.request,
WorkspacePermissionMode.Read
);
}
return objectToBulkGet;
};

const findWithWorkspacePermissionControl = async <T = unknown>(
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<T>(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) {}
}
7 changes: 7 additions & 0 deletions src/core/server/workspaces/workspace_permission_control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,12 @@ export class WorkspacePermissionControl {
return true;
}

public async getPermittedWorkspaceIds(
permissionModeOrModes: WorkspacePermissionMode | WorkspacePermissionMode[],
request: OpenSearchDashboardsRequest
) {
return [];
}

public async setup() {}
}
10 changes: 10 additions & 0 deletions src/core/server/workspaces/workspaces_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 6f5c215

Please sign in to comment.