From 2d913a20d329595da1b820ba1e590156cd41f805 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 18 Sep 2023 18:25:39 +0800 Subject: [PATCH 1/8] feat: setup reserved workspaces when calling list workspaces API Signed-off-by: SuZhou-Joe --- .../server/permission_control/client.mock.ts | 2 +- .../server/permission_control/client.ts | 31 ++-- src/plugins/workspace/server/plugin.ts | 102 +---------- .../workspace_saved_objects_client_wrapper.ts | 3 +- .../workspace/server/workspace_client.ts | 168 +++++++++++++++++- 5 files changed, 183 insertions(+), 123 deletions(-) diff --git a/src/plugins/workspace/server/permission_control/client.mock.ts b/src/plugins/workspace/server/permission_control/client.mock.ts index 13ce45c7975d..278a2fcbccf9 100644 --- a/src/plugins/workspace/server/permission_control/client.mock.ts +++ b/src/plugins/workspace/server/permission_control/client.mock.ts @@ -9,5 +9,5 @@ export const savedObjectsPermissionControlMock: SavedObjectsPermissionControlCon batchValidate: jest.fn(), getPrincipalsOfObjects: jest.fn(), getPermittedWorkspaceIds: jest.fn(), - getPrincipalsFromRequest: jest.fn(), + setup: jest.fn(), }; diff --git a/src/plugins/workspace/server/permission_control/client.ts b/src/plugins/workspace/server/permission_control/client.ts index 2d8bf80491c3..5610512f60d9 100644 --- a/src/plugins/workspace/server/permission_control/client.ts +++ b/src/plugins/workspace/server/permission_control/client.ts @@ -29,19 +29,7 @@ export interface AuthInfo { } export class SavedObjectsPermissionControl { - private readonly logger: Logger; - private _getScopedClient?: SavedObjectsServiceStart['getScopedClient']; - private getScopedClient(request: OpenSearchDashboardsRequest) { - return this._getScopedClient?.(request, { - excludedWrappers: [WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID], - }); - } - - constructor(logger: Logger) { - this.logger = logger; - } - - public getPrincipalsFromRequest(request: OpenSearchDashboardsRequest): Principals { + static getPrincipalsFromRequest(request: OpenSearchDashboardsRequest): Principals { const rawRequest = ensureRawRequest(request); const authInfo = rawRequest?.auth?.credentials?.authInfo as AuthInfo | null; const payload: Principals = {}; @@ -68,6 +56,19 @@ export class SavedObjectsPermissionControl { } return payload; } + + private readonly logger: Logger; + private _getScopedClient?: SavedObjectsServiceStart['getScopedClient']; + private getScopedClient(request: OpenSearchDashboardsRequest) { + return this._getScopedClient?.(request, { + excludedWrappers: [WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID], + }); + } + + constructor(logger: Logger) { + this.logger = logger; + } + private async bulkGetSavedObjects( request: OpenSearchDashboardsRequest, savedObjects: SavedObjectsBulkGetObject[] @@ -114,7 +115,7 @@ export class SavedObjectsPermissionControl { }; } - const principals = this.getPrincipalsFromRequest(request); + const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest(request); let savedObjectsBasicInfo: any[] = []; const hasAllPermission = savedObjectsGet.every((item) => { // for object that doesn't contain ACL like config, return true @@ -168,7 +169,7 @@ export class SavedObjectsPermissionControl { request: OpenSearchDashboardsRequest, permissionModes: SavedObjectsPermissionModes ) { - const principals = this.getPrincipalsFromRequest(request); + const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest(request); const savedObjectClient = this.getScopedClient?.(request); try { const result = await savedObjectClient?.find({ diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 1e30eca32fdd..d926727fd569 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -2,35 +2,19 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { i18n } from '@osd/i18n'; -import { Observable } from 'rxjs'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger, - ISavedObjectsRepository, - WORKSPACE_TYPE, - ACL, - PUBLIC_WORKSPACE_ID, - MANAGEMENT_WORKSPACE_ID, - Permissions, - WorkspacePermissionMode, SavedObjectsClient, - WorkspaceAttribute, - DEFAULT_APP_CATEGORIES, } from '../../../core/server'; import { IWorkspaceDBImpl } from './types'; import { WorkspaceClientWithSavedObject } from './workspace_client'; import { WorkspaceSavedObjectsClientWrapper } from './saved_objects'; import { registerRoutes } from './routes'; -import { - WORKSPACE_OVERVIEW_APP_ID, - WORKSPACE_UPDATE_APP_ID, - WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, -} from '../common/constants'; -import { ConfigSchema } from '../config'; +import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; import { SavedObjectsPermissionControl, SavedObjectsPermissionControlContract, @@ -40,7 +24,6 @@ import { registerPermissionCheckRoutes } from './permission_control/routes'; export class WorkspacePlugin implements Plugin<{}, {}> { private readonly logger: Logger; private client?: IWorkspaceDBImpl; - private config$: Observable; private permissionControl?: SavedObjectsPermissionControlContract; private proxyWorkspaceTrafficToRealHandler(setupDeps: CoreSetup) { @@ -62,13 +45,12 @@ export class WorkspacePlugin implements Plugin<{}, {}> { constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('plugins', 'workspace'); - this.config$ = initializerContext.config.create(); } public async setup(core: CoreSetup) { this.logger.debug('Setting up Workspaces service'); - this.client = new WorkspaceClientWithSavedObject(core); + this.client = new WorkspaceClientWithSavedObject(core, this.logger); await this.client.setup(core); this.permissionControl = new SavedObjectsPermissionControl(this.logger); @@ -79,10 +61,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> { }); const workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper( - this.permissionControl, - { - config$: this.config$, - } + this.permissionControl ); core.savedObjects.addClientWrapper( @@ -108,85 +87,10 @@ export class WorkspacePlugin implements Plugin<{}, {}> { }; } - private async checkAndCreateWorkspace( - internalRepository: ISavedObjectsRepository, - workspaceId: string, - workspaceAttribute: Omit, - permissions?: Permissions - ) { - /** - * Internal repository is attached to global tenant. - */ - try { - await internalRepository.get(WORKSPACE_TYPE, workspaceId); - } catch (error) { - this.logger.debug(error?.toString() || ''); - this.logger.info(`Workspace ${workspaceId} is not found, create it by using internal user`); - try { - const createResult = await internalRepository.create(WORKSPACE_TYPE, workspaceAttribute, { - id: workspaceId, - permissions, - }); - if (createResult.id) { - this.logger.info(`Created workspace ${createResult.id} in global tenant.`); - } - } catch (e) { - this.logger.error(`Create ${workspaceId} workspace error: ${e?.toString() || ''}`); - } - } - } - - private async setupWorkspaces(startDeps: CoreStart) { - const internalRepository = startDeps.savedObjects.createInternalRepository(); - const publicWorkspaceACL = new ACL().addPermission( - [WorkspacePermissionMode.LibraryRead, WorkspacePermissionMode.LibraryWrite], - { - users: ['*'], - } - ); - const managementWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.LibraryRead], { - users: ['*'], - }); - const DSM_APP_ID = 'dataSources'; - const DEV_TOOLS_APP_ID = 'dev_tools'; - - await Promise.all([ - this.checkAndCreateWorkspace( - internalRepository, - PUBLIC_WORKSPACE_ID, - { - name: i18n.translate('workspaces.public.workspace.default.name', { - defaultMessage: 'public', - }), - features: ['*', `!@${DEFAULT_APP_CATEGORIES.management.id}`], - }, - publicWorkspaceACL.getPermissions() - ), - this.checkAndCreateWorkspace( - internalRepository, - MANAGEMENT_WORKSPACE_ID, - { - name: i18n.translate('workspaces.management.workspace.default.name', { - defaultMessage: 'Management', - }), - features: [ - `@${DEFAULT_APP_CATEGORIES.management.id}`, - WORKSPACE_OVERVIEW_APP_ID, - WORKSPACE_UPDATE_APP_ID, - DSM_APP_ID, - DEV_TOOLS_APP_ID, - ], - }, - managementWorkspaceACL.getPermissions() - ), - ]); - } - public start(core: CoreStart) { this.logger.debug('Starting SavedObjects service'); this.permissionControl?.setup(core.savedObjects.getScopedClient); this.client?.setSavedObjects(core.savedObjects); - this.setupWorkspaces(core); return { client: this.client as IWorkspaceDBImpl, diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 31ef3789bb1f..60a818cde0b8 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -30,6 +30,7 @@ import { } from '../../../../core/server'; import { SavedObjectsPermissionControlContract } from '../permission_control/client'; import { WorkspaceFindOptions } from '../types'; +import { SavedObjectsPermissionControl, SavedObjectsPermissionControlContract } from '../permission_control/client'; // Can't throw unauthorized for now, the page will be refreshed if unauthorized const generateWorkspacePermissionError = () => { @@ -355,7 +356,7 @@ export class WorkspaceSavedObjectsClientWrapper { const findWithWorkspacePermissionControl = async ( options: SavedObjectsFindOptions & Pick ) => { - const principals = this.permissionControl.getPrincipalsFromRequest(wrapperOptions.request); + const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest(wrapperOptions.request); if (!options.ACLSearchParams) { options.ACLSearchParams = {}; } diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index eda57b8d4bf1..ec56b87f682b 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -9,8 +9,18 @@ import type { CoreSetup, WorkspaceAttribute, SavedObjectsServiceStart, + Logger, + Permissions, + OpenSearchDashboardsRequest, +} from '../../../core/server'; +import { + ACL, + DEFAULT_APP_CATEGORIES, + MANAGEMENT_WORKSPACE_ID, + PUBLIC_WORKSPACE_ID, + WORKSPACE_TYPE, + WorkspacePermissionMode, } from '../../../core/server'; -import { WORKSPACE_TYPE } from '../../../core/server'; import { IWorkspaceDBImpl, WorkspaceFindOptions, @@ -20,7 +30,12 @@ import { } from './types'; import { workspace } from './saved_objects'; import { generateRandomId } from './utils'; -import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; +import { + WORKSPACE_OVERVIEW_APP_ID, + WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, + WORKSPACE_UPDATE_APP_ID, +} from '../common/constants'; +import { SavedObjectsPermissionControl } from './permission_control/client'; const WORKSPACE_ID_SIZE = 6; @@ -30,11 +45,13 @@ const DUPLICATE_WORKSPACE_NAME_ERROR = i18n.translate('workspace.duplicate.name. export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { private setupDep: CoreSetup; + private logger: Logger; private savedObjects?: SavedObjectsServiceStart; - setSavedObjects(savedObjects: SavedObjectsServiceStart) { - this.savedObjects = savedObjects; + constructor(core: CoreSetup, logger: Logger) { + this.setupDep = core; + this.logger = logger; } private getScopedClientWithoutPermission( @@ -45,9 +62,6 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { }); } - constructor(core: CoreSetup) { - this.setupDep = core; - } private getSavedObjectClientsFromRequestDetail( requestDetail: IRequestDetail ): SavedObjectsClientContract { @@ -65,6 +79,97 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { private formatError(error: Error | any): string { return error.message || error.error || 'Error'; } + private async checkAndCreateWorkspace( + savedObjectClient: SavedObjectsClientContract | undefined, + workspaceId: string, + workspaceAttribute: Omit, + permissions?: Permissions + ) { + try { + await savedObjectClient?.get(WORKSPACE_TYPE, workspaceId); + } catch (error) { + this.logger.debug(error?.toString() || ''); + this.logger.info(`Workspace ${workspaceId} is not found, create it by using internal user`); + try { + const createResult = await savedObjectClient?.create(WORKSPACE_TYPE, workspaceAttribute, { + id: workspaceId, + permissions, + }); + if (createResult?.id) { + this.logger.info(`Created workspace ${createResult.id}.`); + } + } catch (e) { + this.logger.error(`Create ${workspaceId} workspace error: ${e?.toString() || ''}`); + } + } + } + private async setupPublicWorkspace(savedObjectClient?: SavedObjectsClientContract) { + const publicWorkspaceACL = new ACL().addPermission( + [WorkspacePermissionMode.Management], + { + users: ['*'], + } + ); + return this.checkAndCreateWorkspace( + savedObjectClient, + PUBLIC_WORKSPACE_ID, + { + name: i18n.translate('workspaces.public.workspace.default.name', { + defaultMessage: 'public', + }), + features: ['*', `!@${DEFAULT_APP_CATEGORIES.management.id}`], + reserved: true, + }, + publicWorkspaceACL.getPermissions() + ); + } + private async setupManagementWorkspace(savedObjectClient?: SavedObjectsClientContract) { + const managementWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.Management], { + users: ['*'], + }); + const DSM_APP_ID = 'dataSources'; + const DEV_TOOLS_APP_ID = 'dev_tools'; + + return this.checkAndCreateWorkspace( + savedObjectClient, + MANAGEMENT_WORKSPACE_ID, + { + name: i18n.translate('workspaces.management.workspace.default.name', { + defaultMessage: 'Management', + }), + features: [ + `@${DEFAULT_APP_CATEGORIES.management.id}`, + WORKSPACE_OVERVIEW_APP_ID, + WORKSPACE_UPDATE_APP_ID, + DSM_APP_ID, + DEV_TOOLS_APP_ID, + ], + reserved: true, + }, + managementWorkspaceACL.getPermissions() + ); + } + private async setupPersonalWorkspace( + request: OpenSearchDashboardsRequest, + savedObjectClient?: SavedObjectsClientContract + ) { + const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest(request); + const personalWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.Management], { + users: principals.users, + }); + return this.checkAndCreateWorkspace( + savedObjectClient, + MANAGEMENT_WORKSPACE_ID, + { + name: i18n.translate('workspaces.personal.workspace.default.name', { + defaultMessage: 'Personal workspace', + }), + features: ['*', `!@${DEFAULT_APP_CATEGORIES.management.id}`], + reserved: true, + }, + personalWorkspaceACL.getPermissions() + ); + } public async setup(core: CoreSetup): Promise> { this.setupDep.savedObjects.registerType(workspace); return { @@ -125,6 +230,52 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { type: WORKSPACE_TYPE, } ); + const scopedClientWithoutPermissionCheck = this.getScopeClientWithoutPermisson(requestDetail); + const tasks: Promise[] = []; + + /** + * Setup public workspace if public workspace can not be found + */ + const hasPublicWorkspace = savedObjects.find((item) => item.id === PUBLIC_WORKSPACE_ID); + + if (!hasPublicWorkspace) { + tasks.push(this.setupPublicWorkspace(scopedClientWithoutPermissionCheck)); + } + + /** + * Setup management workspace if management workspace can not be found + */ + const hasManagementWorkspace = savedObjects.find( + (item) => item.id === MANAGEMENT_WORKSPACE_ID + ); + if (!hasManagementWorkspace) { + tasks.push(this.setupManagementWorkspace(scopedClientWithoutPermissionCheck)); + } + + /** + * Setup personal workspace + */ + const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest( + requestDetail.request + ); + /** + * Only when authentication is enabled will personal workspace be created + */ + if (principals.users) { + const hasPersonalWorkspace = savedObjects.find((item) => + principals.users?.includes(item.id) + ); + if (!hasPersonalWorkspace) { + tasks.push( + this.setupPersonalWorkspace(requestDetail.request, scopedClientWithoutPermissionCheck) + ); + } + } + try { + await Promise.all(tasks); + } catch (e) { + this.logger.error(`Some error happened when initializing reserved workspace: ${e}`); + } return { success: true, result: { @@ -228,6 +379,9 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { }; } } + public setSavedObjects(savedObjects: SavedObjectsServiceStart) { + this.savedObjects = savedObjects; + } public async destroy(): Promise> { return { success: true, From 8f99b8c926e7d62aa8781d690e3109f1e862a5f7 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 19 Sep 2023 07:43:28 +0800 Subject: [PATCH 2/8] feat: fix test flow Signed-off-by: SuZhou-Joe --- .../saved_objects/workspace_saved_objects_client_wrapper.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 60a818cde0b8..4cc78857125b 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -28,9 +28,8 @@ import { SavedObjectsDeleteByWorkspaceOptions, SavedObjectsErrorHelpers, } from '../../../../core/server'; -import { SavedObjectsPermissionControlContract } from '../permission_control/client'; -import { WorkspaceFindOptions } from '../types'; import { SavedObjectsPermissionControl, SavedObjectsPermissionControlContract } from '../permission_control/client'; +import { WorkspaceFindOptions } from '../types'; // Can't throw unauthorized for now, the page will be refreshed if unauthorized const generateWorkspacePermissionError = () => { From eed6e6deb663e65743c6010cf383f7f2e4785f5b Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 19 Sep 2023 08:55:06 +0800 Subject: [PATCH 3/8] feat: fix lint Signed-off-by: SuZhou-Joe --- .../workspace_saved_objects_client_wrapper.ts | 9 +++++++-- src/plugins/workspace/server/workspace_client.ts | 11 ++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 4cc78857125b..6384566b9e20 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -28,7 +28,10 @@ import { SavedObjectsDeleteByWorkspaceOptions, SavedObjectsErrorHelpers, } from '../../../../core/server'; -import { SavedObjectsPermissionControl, SavedObjectsPermissionControlContract } from '../permission_control/client'; +import { + SavedObjectsPermissionControl, + SavedObjectsPermissionControlContract, +} from '../permission_control/client'; import { WorkspaceFindOptions } from '../types'; // Can't throw unauthorized for now, the page will be refreshed if unauthorized @@ -355,7 +358,9 @@ export class WorkspaceSavedObjectsClientWrapper { const findWithWorkspacePermissionControl = async ( options: SavedObjectsFindOptions & Pick ) => { - const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest(wrapperOptions.request); + const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest( + wrapperOptions.request + ); if (!options.ACLSearchParams) { options.ACLSearchParams = {}; } diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index ec56b87f682b..a4cf298e737d 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -104,12 +104,9 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { } } private async setupPublicWorkspace(savedObjectClient?: SavedObjectsClientContract) { - const publicWorkspaceACL = new ACL().addPermission( - [WorkspacePermissionMode.Management], - { - users: ['*'], - } - ); + const publicWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.Management], { + users: ['*'], + }); return this.checkAndCreateWorkspace( savedObjectClient, PUBLIC_WORKSPACE_ID, @@ -231,7 +228,7 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { } ); const scopedClientWithoutPermissionCheck = this.getScopeClientWithoutPermisson(requestDetail); - const tasks: Promise[] = []; + const tasks: Array> = []; /** * Setup public workspace if public workspace can not be found From feefd271eaba5140c0d78ef2cd728c3f701624d3 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 19 Sep 2023 12:35:09 +0800 Subject: [PATCH 4/8] feat: fix typos Signed-off-by: SuZhou-Joe --- src/plugins/workspace/server/workspace_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index a4cf298e737d..92737a8106ce 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -227,7 +227,7 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { type: WORKSPACE_TYPE, } ); - const scopedClientWithoutPermissionCheck = this.getScopeClientWithoutPermisson(requestDetail); + const scopedClientWithoutPermissionCheck = this.getScopedClientWithoutPermission(requestDetail); const tasks: Array> = []; /** From 1f85495d3fb01943529acd8ce372476fbac65236 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 19 Sep 2023 13:36:38 +0800 Subject: [PATCH 5/8] fix: setup personal workspace using correct id Signed-off-by: SuZhou-Joe --- src/core/server/index.ts | 1 + src/core/utils/constants.ts | 2 + src/core/utils/index.ts | 1 + .../workspace/server/workspace_client.ts | 38 ++++++++++++------- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/core/server/index.ts b/src/core/server/index.ts index a103a90bf16c..42725b1c90d4 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -362,6 +362,7 @@ export { PUBLIC_WORKSPACE_ID, MANAGEMENT_WORKSPACE_ID, WORKSPACE_TYPE, + PERSONAL_WORKSPACE_ID_PREFIX, } from '../utils'; export { diff --git a/src/core/utils/constants.ts b/src/core/utils/constants.ts index 21cbaa4e5a2e..718996a0718f 100644 --- a/src/core/utils/constants.ts +++ b/src/core/utils/constants.ts @@ -18,3 +18,5 @@ export enum WorkspacePermissionMode { export const PUBLIC_WORKSPACE_ID = 'public'; export const MANAGEMENT_WORKSPACE_ID = 'management'; + +export const PERSONAL_WORKSPACE_ID_PREFIX = 'personal'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 060624fd91de..1b7d29406443 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -43,4 +43,5 @@ export { PUBLIC_WORKSPACE_ID, MANAGEMENT_WORKSPACE_ID, WORKSPACE_TYPE, + PERSONAL_WORKSPACE_ID_PREFIX, } from './constants'; diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index 92737a8106ce..0c40ccc876a5 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -12,6 +12,7 @@ import type { Logger, Permissions, OpenSearchDashboardsRequest, + SavedObjectsFindResult, } from '../../../core/server'; import { ACL, @@ -20,6 +21,7 @@ import { PUBLIC_WORKSPACE_ID, WORKSPACE_TYPE, WorkspacePermissionMode, + PERSONAL_WORKSPACE_ID_PREFIX, } from '../../../core/server'; import { IWorkspaceDBImpl, @@ -156,7 +158,7 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { }); return this.checkAndCreateWorkspace( savedObjectClient, - MANAGEMENT_WORKSPACE_ID, + `${PERSONAL_WORKSPACE_ID_PREFIX}-${principals.users?.[0] || ''}`, { name: i18n.translate('workspaces.personal.workspace.default.name', { defaultMessage: 'Personal workspace', @@ -218,16 +220,17 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { options: WorkspaceFindOptions ): ReturnType { try { - const { - saved_objects: savedObjects, - ...others - } = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find( - { - ...options, - type: WORKSPACE_TYPE, - } + let savedObjects: Array> = []; + const { saved_objects, ...others } = await this.getSavedObjectClientsFromRequestDetail( + requestDetail + ).find({ + ...options, + type: WORKSPACE_TYPE, + }); + savedObjects = saved_objects; + const scopedClientWithoutPermissionCheck = this.getScopedClientWithoutPermission( + requestDetail ); - const scopedClientWithoutPermissionCheck = this.getScopedClientWithoutPermission(requestDetail); const tasks: Array> = []; /** @@ -258,9 +261,9 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { /** * Only when authentication is enabled will personal workspace be created */ - if (principals.users) { - const hasPersonalWorkspace = savedObjects.find((item) => - principals.users?.includes(item.id) + if (principals.users && principals.users?.[0]) { + const hasPersonalWorkspace = savedObjects.find( + (item) => `${PERSONAL_WORKSPACE_ID_PREFIX}-${principals.users?.[0] || ''}` === item.id ); if (!hasPersonalWorkspace) { tasks.push( @@ -270,6 +273,15 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { } try { await Promise.all(tasks); + if (tasks.length) { + const retryFindResp = await this.getSavedObjectClientsFromRequestDetail( + requestDetail + ).find({ + ...options, + type: WORKSPACE_TYPE, + }); + savedObjects = retryFindResp.saved_objects; + } } catch (e) { this.logger.error(`Some error happened when initializing reserved workspace: ${e}`); } From 5cbd86306d7885711774d6e36be4f43946d4aa72 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 19 Sep 2023 13:48:24 +0800 Subject: [PATCH 6/8] refractor: move function getPrincipalsFromRequest to a util function Signed-off-by: SuZhou-Joe --- .../server/permission_control/client.ts | 42 ++----------------- .../workspace_saved_objects_client_wrapper.ts | 10 ++--- src/plugins/workspace/server/types.ts | 5 +++ src/plugins/workspace/server/utils.ts | 35 ++++++++++++++++ .../workspace/server/workspace_client.ts | 21 ++++------ 5 files changed, 56 insertions(+), 57 deletions(-) diff --git a/src/plugins/workspace/server/permission_control/client.ts b/src/plugins/workspace/server/permission_control/client.ts index 5610512f60d9..3740518995d8 100644 --- a/src/plugins/workspace/server/permission_control/client.ts +++ b/src/plugins/workspace/server/permission_control/client.ts @@ -3,18 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ import { i18n } from '@osd/i18n'; -import { ensureRawRequest, OpenSearchDashboardsRequest } from '../../../../core/server'; +import { OpenSearchDashboardsRequest } from '../../../../core/server'; import { ACL, - Principals, TransformedPermission, - PrincipalType, SavedObjectsBulkGetObject, SavedObjectsServiceStart, Logger, WORKSPACE_TYPE, } from '../../../../core/server'; import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../../common/constants'; +import { getPrincipalsFromRequest } from '../utils'; export type SavedObjectsPermissionControlContract = Pick< SavedObjectsPermissionControl, @@ -23,40 +22,7 @@ export type SavedObjectsPermissionControlContract = Pick< export type SavedObjectsPermissionModes = string[]; -export interface AuthInfo { - backend_roles?: string[]; - user_name?: string; -} - export class SavedObjectsPermissionControl { - static getPrincipalsFromRequest(request: OpenSearchDashboardsRequest): Principals { - const rawRequest = ensureRawRequest(request); - const authInfo = rawRequest?.auth?.credentials?.authInfo as AuthInfo | null; - const payload: Principals = {}; - if (!authInfo) { - /** - * Login user have access to all the workspaces when no authentication is presented. - * The logic will be used when users create workspaces with authentication enabled but turn off authentication for any reason. - */ - return payload; - } - if (!authInfo?.backend_roles?.length && !authInfo.user_name) { - /** - * It means OSD can not recognize who the user is even if authentication is enabled, - * use a fake user that won't be granted permission explicitly. - */ - payload[PrincipalType.Users] = [`_user_fake_${Date.now()}_`]; - return payload; - } - if (authInfo?.backend_roles) { - payload[PrincipalType.Groups] = authInfo.backend_roles; - } - if (authInfo?.user_name) { - payload[PrincipalType.Users] = [authInfo.user_name]; - } - return payload; - } - private readonly logger: Logger; private _getScopedClient?: SavedObjectsServiceStart['getScopedClient']; private getScopedClient(request: OpenSearchDashboardsRequest) { @@ -115,7 +81,7 @@ export class SavedObjectsPermissionControl { }; } - const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest(request); + const principals = getPrincipalsFromRequest(request); let savedObjectsBasicInfo: any[] = []; const hasAllPermission = savedObjectsGet.every((item) => { // for object that doesn't contain ACL like config, return true @@ -169,7 +135,7 @@ export class SavedObjectsPermissionControl { request: OpenSearchDashboardsRequest, permissionModes: SavedObjectsPermissionModes ) { - const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest(request); + const principals = getPrincipalsFromRequest(request); const savedObjectClient = this.getScopedClient?.(request); try { const result = await savedObjectClient?.find({ diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 6384566b9e20..df978636195f 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -28,11 +28,9 @@ import { SavedObjectsDeleteByWorkspaceOptions, SavedObjectsErrorHelpers, } from '../../../../core/server'; -import { - SavedObjectsPermissionControl, - SavedObjectsPermissionControlContract, -} from '../permission_control/client'; +import { SavedObjectsPermissionControlContract } from '../permission_control/client'; import { WorkspaceFindOptions } from '../types'; +import { getPrincipalsFromRequest } from '../utils'; // Can't throw unauthorized for now, the page will be refreshed if unauthorized const generateWorkspacePermissionError = () => { @@ -358,9 +356,7 @@ export class WorkspaceSavedObjectsClientWrapper { const findWithWorkspacePermissionControl = async ( options: SavedObjectsFindOptions & Pick ) => { - const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest( - wrapperOptions.request - ); + const principals = getPrincipalsFromRequest(wrapperOptions.request); if (!options.ACLSearchParams) { options.ACLSearchParams = {}; } diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts index 243035e8223c..5762c7d08ca5 100644 --- a/src/plugins/workspace/server/types.ts +++ b/src/plugins/workspace/server/types.ts @@ -81,3 +81,8 @@ export type WorkspaceRoutePermissionItem = { | WorkspacePermissionMode.Management >; } & ({ type: 'user'; userId: string } | { type: 'group'; group: string }); + +export interface AuthInfo { + backend_roles?: string[]; + user_name?: string; +} diff --git a/src/plugins/workspace/server/utils.ts b/src/plugins/workspace/server/utils.ts index 89bfabd52657..d2ff66e8b486 100644 --- a/src/plugins/workspace/server/utils.ts +++ b/src/plugins/workspace/server/utils.ts @@ -4,6 +4,13 @@ */ import crypto from 'crypto'; +import { + ensureRawRequest, + OpenSearchDashboardsRequest, + Principals, + PrincipalType, +} from '../../../core/server'; +import { AuthInfo } from './types'; /** * Generate URL friendly random ID @@ -11,3 +18,31 @@ import crypto from 'crypto'; export const generateRandomId = (size: number) => { return crypto.randomBytes(size).toString('base64url').slice(0, size); }; + +export const getPrincipalsFromRequest = (request: OpenSearchDashboardsRequest): Principals => { + const rawRequest = ensureRawRequest(request); + const authInfo = rawRequest?.auth?.credentials?.authInfo as AuthInfo | null; + const payload: Principals = {}; + if (!authInfo) { + /** + * Login user have access to all the workspaces when no authentication is presented. + * The logic will be used when users create workspaces with authentication enabled but turn off authentication for any reason. + */ + return payload; + } + if (!authInfo?.backend_roles?.length && !authInfo.user_name) { + /** + * It means OSD can not recognize who the user is even if authentication is enabled, + * use a fake user that won't be granted permission explicitly. + */ + payload[PrincipalType.Users] = [`_user_fake_${Date.now()}_`]; + return payload; + } + if (authInfo?.backend_roles) { + payload[PrincipalType.Groups] = authInfo.backend_roles; + } + if (authInfo?.user_name) { + payload[PrincipalType.Users] = [authInfo.user_name]; + } + return payload; +}; diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index 0c40ccc876a5..61899c76c409 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { i18n } from '@osd/i18n'; +import { omit } from 'lodash'; import type { SavedObject, SavedObjectsClientContract, @@ -12,7 +13,6 @@ import type { Logger, Permissions, OpenSearchDashboardsRequest, - SavedObjectsFindResult, } from '../../../core/server'; import { ACL, @@ -31,13 +31,12 @@ import { WorkspaceAttributeWithPermission, } from './types'; import { workspace } from './saved_objects'; -import { generateRandomId } from './utils'; +import { generateRandomId, getPrincipalsFromRequest } from './utils'; import { WORKSPACE_OVERVIEW_APP_ID, WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, WORKSPACE_UPDATE_APP_ID, } from '../common/constants'; -import { SavedObjectsPermissionControl } from './permission_control/client'; const WORKSPACE_ID_SIZE = 6; @@ -152,7 +151,7 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { request: OpenSearchDashboardsRequest, savedObjectClient?: SavedObjectsClientContract ) { - const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest(request); + const principals = getPrincipalsFromRequest(request); const personalWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.Management], { users: principals.users, }); @@ -220,14 +219,14 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { options: WorkspaceFindOptions ): ReturnType { try { - let savedObjects: Array> = []; - const { saved_objects, ...others } = await this.getSavedObjectClientsFromRequestDetail( - requestDetail - ).find({ + const resultResp = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find< + WorkspaceAttribute + >({ ...options, type: WORKSPACE_TYPE, }); - savedObjects = saved_objects; + const others = omit(resultResp, 'saved_objects'); + let savedObjects = resultResp.saved_objects; const scopedClientWithoutPermissionCheck = this.getScopedClientWithoutPermission( requestDetail ); @@ -255,9 +254,7 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { /** * Setup personal workspace */ - const principals = SavedObjectsPermissionControl.getPrincipalsFromRequest( - requestDetail.request - ); + const principals = getPrincipalsFromRequest(requestDetail.request); /** * Only when authentication is enabled will personal workspace be created */ From ec7c21dacb54a98ac7cf89303d66ad0b40bca65d Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 19 Sep 2023 13:53:55 +0800 Subject: [PATCH 7/8] feat: change code according to comment Signed-off-by: SuZhou-Joe --- src/plugins/workspace/server/workspace_client.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index 61899c76c409..790a08c2c0e3 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -113,7 +113,7 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { PUBLIC_WORKSPACE_ID, { name: i18n.translate('workspaces.public.workspace.default.name', { - defaultMessage: 'public', + defaultMessage: 'Global workspace', }), features: ['*', `!@${DEFAULT_APP_CATEGORIES.management.id}`], reserved: true, @@ -235,7 +235,7 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { /** * Setup public workspace if public workspace can not be found */ - const hasPublicWorkspace = savedObjects.find((item) => item.id === PUBLIC_WORKSPACE_ID); + const hasPublicWorkspace = savedObjects.some((item) => item.id === PUBLIC_WORKSPACE_ID); if (!hasPublicWorkspace) { tasks.push(this.setupPublicWorkspace(scopedClientWithoutPermissionCheck)); @@ -244,7 +244,7 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { /** * Setup management workspace if management workspace can not be found */ - const hasManagementWorkspace = savedObjects.find( + const hasManagementWorkspace = savedObjects.some( (item) => item.id === MANAGEMENT_WORKSPACE_ID ); if (!hasManagementWorkspace) { @@ -256,7 +256,8 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { */ const principals = getPrincipalsFromRequest(requestDetail.request); /** - * Only when authentication is enabled will personal workspace be created + * Only when authentication is enabled will personal workspace be created. + * and the personal workspace id will be like "personal-{userId}" */ if (principals.users && principals.users?.[0]) { const hasPersonalWorkspace = savedObjects.find( From c19d904d58bff23b05b90e12c270fd3c2d83f0dc Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 19 Sep 2023 17:41:00 +0800 Subject: [PATCH 8/8] feat: modify reserved workspace permission Signed-off-by: SuZhou-Joe --- .../workspace/server/workspace_client.ts | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index 790a08c2c0e3..56a8fd2d0d1e 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -105,9 +105,12 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { } } private async setupPublicWorkspace(savedObjectClient?: SavedObjectsClientContract) { - const publicWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.Management], { - users: ['*'], - }); + const publicWorkspaceACL = new ACL().addPermission( + [WorkspacePermissionMode.LibraryWrite, WorkspacePermissionMode.Write], + { + users: ['*'], + } + ); return this.checkAndCreateWorkspace( savedObjectClient, PUBLIC_WORKSPACE_ID, @@ -122,9 +125,12 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { ); } private async setupManagementWorkspace(savedObjectClient?: SavedObjectsClientContract) { - const managementWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.Management], { - users: ['*'], - }); + const managementWorkspaceACL = new ACL().addPermission( + [WorkspacePermissionMode.LibraryWrite, WorkspacePermissionMode.Write], + { + users: ['*'], + } + ); const DSM_APP_ID = 'dataSources'; const DEV_TOOLS_APP_ID = 'dev_tools'; @@ -152,9 +158,12 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { savedObjectClient?: SavedObjectsClientContract ) { const principals = getPrincipalsFromRequest(request); - const personalWorkspaceACL = new ACL().addPermission([WorkspacePermissionMode.Management], { - users: principals.users, - }); + const personalWorkspaceACL = new ACL().addPermission( + [WorkspacePermissionMode.LibraryWrite, WorkspacePermissionMode.Write], + { + users: principals.users, + } + ); return this.checkAndCreateWorkspace( savedObjectClient, `${PERSONAL_WORKSPACE_ID_PREFIX}-${principals.users?.[0] || ''}`,