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

add workspace saved objects client wrapper #51

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually saved_objects directory is used to store the saved object type registration file according to OSD convention, I'd recommend placing this file to other directory.

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
Loading