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/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..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,11 +22,6 @@ export type SavedObjectsPermissionControlContract = Pick< export type SavedObjectsPermissionModes = string[]; -export interface AuthInfo { - backend_roles?: string[]; - user_name?: string; -} - export class SavedObjectsPermissionControl { private readonly logger: Logger; private _getScopedClient?: SavedObjectsServiceStart['getScopedClient']; @@ -41,33 +35,6 @@ export class SavedObjectsPermissionControl { this.logger = logger; } - public 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 async bulkGetSavedObjects( request: OpenSearchDashboardsRequest, savedObjects: SavedObjectsBulkGetObject[] @@ -114,7 +81,7 @@ export class SavedObjectsPermissionControl { }; } - const principals = this.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 @@ -168,7 +135,7 @@ export class SavedObjectsPermissionControl { request: OpenSearchDashboardsRequest, permissionModes: SavedObjectsPermissionModes ) { - const principals = this.getPrincipalsFromRequest(request); + const principals = 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..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 @@ -30,6 +30,7 @@ import { } from '../../../../core/server'; 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 = () => { @@ -355,7 +356,7 @@ export class WorkspaceSavedObjectsClientWrapper { const findWithWorkspacePermissionControl = async ( options: SavedObjectsFindOptions & Pick ) => { - const principals = this.permissionControl.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 eda57b8d4bf1..56a8fd2d0d1e 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -3,14 +3,26 @@ * SPDX-License-Identifier: Apache-2.0 */ import { i18n } from '@osd/i18n'; +import { omit } from 'lodash'; import type { SavedObject, SavedObjectsClientContract, CoreSetup, WorkspaceAttribute, SavedObjectsServiceStart, + Logger, + Permissions, + OpenSearchDashboardsRequest, +} from '../../../core/server'; +import { + ACL, + DEFAULT_APP_CATEGORIES, + MANAGEMENT_WORKSPACE_ID, + PUBLIC_WORKSPACE_ID, + WORKSPACE_TYPE, + WorkspacePermissionMode, + PERSONAL_WORKSPACE_ID_PREFIX, } from '../../../core/server'; -import { WORKSPACE_TYPE } from '../../../core/server'; import { IWorkspaceDBImpl, WorkspaceFindOptions, @@ -19,8 +31,12 @@ import { WorkspaceAttributeWithPermission, } from './types'; import { workspace } from './saved_objects'; -import { generateRandomId } from './utils'; -import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; +import { generateRandomId, getPrincipalsFromRequest } from './utils'; +import { + WORKSPACE_OVERVIEW_APP_ID, + WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, + WORKSPACE_UPDATE_APP_ID, +} from '../common/constants'; const WORKSPACE_ID_SIZE = 6; @@ -30,11 +46,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 +63,6 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { }); } - constructor(core: CoreSetup) { - this.setupDep = core; - } private getSavedObjectClientsFromRequestDetail( requestDetail: IRequestDetail ): SavedObjectsClientContract { @@ -65,6 +80,103 @@ 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.LibraryWrite, WorkspacePermissionMode.Write], + { + users: ['*'], + } + ); + return this.checkAndCreateWorkspace( + savedObjectClient, + PUBLIC_WORKSPACE_ID, + { + name: i18n.translate('workspaces.public.workspace.default.name', { + defaultMessage: 'Global workspace', + }), + features: ['*', `!@${DEFAULT_APP_CATEGORIES.management.id}`], + reserved: true, + }, + publicWorkspaceACL.getPermissions() + ); + } + private async setupManagementWorkspace(savedObjectClient?: SavedObjectsClientContract) { + const managementWorkspaceACL = new ACL().addPermission( + [WorkspacePermissionMode.LibraryWrite, WorkspacePermissionMode.Write], + { + 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 = getPrincipalsFromRequest(request); + const personalWorkspaceACL = new ACL().addPermission( + [WorkspacePermissionMode.LibraryWrite, WorkspacePermissionMode.Write], + { + users: principals.users, + } + ); + return this.checkAndCreateWorkspace( + savedObjectClient, + `${PERSONAL_WORKSPACE_ID_PREFIX}-${principals.users?.[0] || ''}`, + { + 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 { @@ -116,15 +228,70 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { options: WorkspaceFindOptions ): ReturnType { try { - const { - saved_objects: savedObjects, - ...others - } = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find( - { - ...options, - type: WORKSPACE_TYPE, - } + const resultResp = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find< + WorkspaceAttribute + >({ + ...options, + type: WORKSPACE_TYPE, + }); + const others = omit(resultResp, 'saved_objects'); + let savedObjects = resultResp.saved_objects; + const scopedClientWithoutPermissionCheck = this.getScopedClientWithoutPermission( + requestDetail ); + const tasks: Array> = []; + + /** + * Setup public workspace if public workspace can not be found + */ + const hasPublicWorkspace = savedObjects.some((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.some( + (item) => item.id === MANAGEMENT_WORKSPACE_ID + ); + if (!hasManagementWorkspace) { + tasks.push(this.setupManagementWorkspace(scopedClientWithoutPermissionCheck)); + } + + /** + * Setup personal workspace + */ + const principals = getPrincipalsFromRequest(requestDetail.request); + /** + * 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( + (item) => `${PERSONAL_WORKSPACE_ID_PREFIX}-${principals.users?.[0] || ''}` === item.id + ); + if (!hasPersonalWorkspace) { + tasks.push( + this.setupPersonalWorkspace(requestDetail.request, scopedClientWithoutPermissionCheck) + ); + } + } + 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}`); + } return { success: true, result: { @@ -228,6 +395,9 @@ export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { }; } } + public setSavedObjects(savedObjects: SavedObjectsServiceStart) { + this.savedObjects = savedObjects; + } public async destroy(): Promise> { return { success: true,