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 c9a951b7580a..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?.setSavedObjectes(core.savedObjects); - this.setupWorkspaces(core); + this.client?.setSavedObjects(core.savedObjects); 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 eb255eb72771..7ac9ec0f22cb 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 @@ -23,13 +23,13 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkUpdateResponse, SavedObjectsBulkUpdateOptions, - SavedObjectsPermissionControlContract, WORKSPACE_TYPE, WorkspacePermissionMode, SavedObjectsDeleteByWorkspaceOptions, SavedObjectsErrorHelpers, } from '../../../../core/server'; 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 = () => { @@ -345,7 +345,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/types.ts b/src/plugins/workspace/server/types.ts index d8eef9c13389..ac86b555c91c 100644 --- a/src/plugins/workspace/server/types.ts +++ b/src/plugins/workspace/server/types.ts @@ -36,7 +36,7 @@ export interface IRequestDetail { export interface IWorkspaceDBImpl { setup(dep: CoreSetup): Promise>; - setSavedObjectes(savedObjectes: SavedObjectsServiceStart): void; + setSavedObjects(savedObjectes: SavedObjectsServiceStart): void; create( requestDetail: IRequestDetail, payload: Omit diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index 6a1b36e11c38..b9300e30b9db 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; - setSavedObjectes(savedObjects: SavedObjectsServiceStart) { - this.savedObjects = savedObjects; + constructor(core: CoreSetup, logger: Logger) { + this.setupDep = core; + this.logger = logger; } private getScopeClientWithoutPermisson( @@ -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 { @@ -123,6 +228,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: { @@ -226,6 +377,9 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { }; } } + public setSavedObjects(savedObjects: SavedObjectsServiceStart) { + this.savedObjects = savedObjects; + } public async destroy(): Promise> { return { success: true,