From d27bcc4258bb2646e0b4726ec7ab755de9b03ec3 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 31 Jul 2023 17:59:27 +0800 Subject: [PATCH] Permission control service for saved objects (#63) * feat: move permission control to saved objects directory Signed-off-by: SuZhou-Joe * feat: use bulkGetObjects and fix unit test Signed-off-by: SuZhou-Joe * feat: add http routes for validate & list Signed-off-by: SuZhou-Joe * feat: move permissionModes to common place Signed-off-by: SuZhou-Joe * feat: rename routes Signed-off-by: SuZhou-Joe * feat: some side effects Signed-off-by: SuZhou-Joe * feat: some side effects Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- src/core/server/legacy/legacy_service.ts | 1 + src/core/server/plugins/plugin_context.ts | 1 + .../permission_control/client.mock.ts | 15 ++++ .../permission_control/client.ts | 83 +++++++++++++++++++ .../permission_control/routes/index.ts | 20 +++++ .../permission_control/routes/principles.ts | 33 ++++++++ .../permission_control/routes/validate.ts | 40 +++++++++ .../saved_objects_service.mock.ts | 2 + .../saved_objects/saved_objects_service.ts | 21 ++++- src/core/server/workspaces/index.ts | 1 - .../workspace_saved_objects_client_wrapper.ts | 68 ++++++++++----- .../workspace_permission_control.ts | 30 ------- .../server/workspaces/workspaces_service.ts | 10 +-- src/core/utils/constants.ts | 8 ++ src/core/utils/index.ts | 2 +- 15 files changed, 273 insertions(+), 62 deletions(-) create mode 100644 src/core/server/saved_objects/permission_control/client.mock.ts create mode 100644 src/core/server/saved_objects/permission_control/client.ts create mode 100644 src/core/server/saved_objects/permission_control/routes/index.ts create mode 100644 src/core/server/saved_objects/permission_control/routes/principles.ts create mode 100644 src/core/server/saved_objects/permission_control/routes/validate.ts delete mode 100644 src/core/server/workspaces/workspace_permission_control.ts diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index a48ff12e859a..1f6bbc3a3b49 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -276,6 +276,7 @@ export class LegacyService implements CoreService { registerType: setupDeps.core.savedObjects.registerType, getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, setRepositoryFactoryProvider: setupDeps.core.savedObjects.setRepositoryFactoryProvider, + permissionControl: setupDeps.core.savedObjects.permissionControl, }, status: { isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 7782fd93041e..bcea61fdd797 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -205,6 +205,7 @@ export function createPluginSetupContext( registerType: deps.savedObjects.registerType, getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit, setRepositoryFactoryProvider: deps.savedObjects.setRepositoryFactoryProvider, + permissionControl: deps.savedObjects.permissionControl, }, status: { core$: deps.status.core$, diff --git a/src/core/server/saved_objects/permission_control/client.mock.ts b/src/core/server/saved_objects/permission_control/client.mock.ts new file mode 100644 index 000000000000..eeafc83995b6 --- /dev/null +++ b/src/core/server/saved_objects/permission_control/client.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsPermissionControlContract } from './client'; + +export const savedObjectsPermissionControlMock: SavedObjectsPermissionControlContract = { + setup: jest.fn(), + validate: jest.fn(), + addPrinciplesToObjects: jest.fn(), + removePrinciplesFromObjects: jest.fn(), + getPrinciplesOfObjects: jest.fn(), + getPermittedWorkspaceIds: jest.fn(), +}; diff --git a/src/core/server/saved_objects/permission_control/client.ts b/src/core/server/saved_objects/permission_control/client.ts new file mode 100644 index 000000000000..9bd41a8c7a30 --- /dev/null +++ b/src/core/server/saved_objects/permission_control/client.ts @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpenSearchDashboardsRequest } from '../../http'; +import { SavedObjectsServiceStart } from '../saved_objects_service'; +import { SavedObjectsBulkGetObject } from '../service'; + +export type SavedObjectsPermissionControlContract = Pick< + SavedObjectsPermissionControl, + keyof SavedObjectsPermissionControl +>; + +export type SavedObjectsPermissionModes = string[]; + +export class SavedObjectsPermissionControl { + private getScopedClient?: SavedObjectsServiceStart['getScopedClient']; + private getScopedSavedObjectsClient(request: OpenSearchDashboardsRequest) { + return this.getScopedClient?.(request); + } + private async bulkGetSavedObjects( + request: OpenSearchDashboardsRequest, + savedObjects: SavedObjectsBulkGetObject[] + ) { + return ( + (await this.getScopedSavedObjectsClient(request)?.bulkGet(savedObjects))?.saved_objects || [] + ); + } + public async setup(getScopedClient: SavedObjectsServiceStart['getScopedClient']) { + this.getScopedClient = getScopedClient; + } + public async validate( + request: OpenSearchDashboardsRequest, + savedObject: SavedObjectsBulkGetObject, + permissionModeOrModes: SavedObjectsPermissionModes + ) { + const savedObjectsGet = await this.bulkGetSavedObjects(request, [savedObject]); + if (savedObjectsGet) { + return { + success: true, + result: true, + }; + } + + return { + success: true, + result: false, + }; + } + + public async addPrinciplesToObjects( + request: OpenSearchDashboardsRequest, + savedObjects: SavedObjectsBulkGetObject[], + personas: string[], + permissionModeOrModes: SavedObjectsPermissionModes + ): Promise { + return true; + } + + public async removePrinciplesFromObjects( + request: OpenSearchDashboardsRequest, + savedObjects: SavedObjectsBulkGetObject[], + personas: string[], + permissionModeOrModes: SavedObjectsPermissionModes + ): Promise { + return true; + } + + public async getPrinciplesOfObjects( + request: OpenSearchDashboardsRequest, + savedObjects: SavedObjectsBulkGetObject[] + ): Promise> { + return {}; + } + + public async getPermittedWorkspaceIds( + request: OpenSearchDashboardsRequest, + permissionModeOrModes: SavedObjectsPermissionModes + ) { + return []; + } +} diff --git a/src/core/server/saved_objects/permission_control/routes/index.ts b/src/core/server/saved_objects/permission_control/routes/index.ts new file mode 100644 index 000000000000..edd694b0ada0 --- /dev/null +++ b/src/core/server/saved_objects/permission_control/routes/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InternalHttpServiceSetup } from '../../../http'; +import { SavedObjectsPermissionControlContract } from '../client'; +import { registerValidateRoute } from './validate'; + +export function registerPermissionCheckRoutes({ + http, + permissionControl, +}: { + http: InternalHttpServiceSetup; + permissionControl: SavedObjectsPermissionControlContract; +}) { + const router = http.createRouter('/api/saved_objects_permission_control/'); + + registerValidateRoute(router, permissionControl); +} diff --git a/src/core/server/saved_objects/permission_control/routes/principles.ts b/src/core/server/saved_objects/permission_control/routes/principles.ts new file mode 100644 index 000000000000..986bf46ed967 --- /dev/null +++ b/src/core/server/saved_objects/permission_control/routes/principles.ts @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { IRouter } from '../../../http'; +import { SavedObjectsPermissionControlContract } from '../client'; + +export const registerListRoute = ( + router: IRouter, + permissionControl: SavedObjectsPermissionControlContract +) => { + router.post( + { + path: '/principles', + validate: { + body: schema.object({ + objects: schema.arrayOf( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const result = await permissionControl.getPrinciplesOfObjects(req, req.body.objects); + return res.ok({ body: result }); + }) + ); +}; diff --git a/src/core/server/saved_objects/permission_control/routes/validate.ts b/src/core/server/saved_objects/permission_control/routes/validate.ts new file mode 100644 index 000000000000..746608d2a74c --- /dev/null +++ b/src/core/server/saved_objects/permission_control/routes/validate.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { IRouter } from '../../../http'; +import { SavedObjectsPermissionControlContract } from '../client'; + +export const registerValidateRoute = ( + router: IRouter, + permissionControl: SavedObjectsPermissionControlContract +) => { + router.post( + { + path: '/validate/{type}/{id}', + validate: { + params: schema.object({ + type: schema.string(), + id: schema.string(), + }), + body: schema.object({ + permissionModes: schema.arrayOf(schema.string()), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { type, id } = req.params; + const result = await permissionControl.validate( + req, + { + type, + id, + }, + req.body.permissionModes + ); + return res.ok({ body: result }); + }) + ); +}; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 74168c436c3d..bf4509adad39 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -45,6 +45,7 @@ import { typeRegistryMock } from './saved_objects_type_registry.mock'; import { migrationMocks } from './migrations/mocks'; import { ServiceStatusLevels } from '../status'; import { ISavedObjectTypeRegistry } from './saved_objects_type_registry'; +import { savedObjectsPermissionControlMock } from './permission_control/client.mock'; type SavedObjectsServiceContract = PublicMethodsOf; @@ -80,6 +81,7 @@ const createSetupContractMock = () => { registerType: jest.fn(), getImportExportObjectLimit: jest.fn(), setRepositoryFactoryProvider: jest.fn(), + permissionControl: savedObjectsPermissionControlMock, }; setupContract.getImportExportObjectLimit.mockReturnValue(100); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b6fc21617bcc..1000948a3ee0 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -65,6 +65,11 @@ import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; import { createMigrationOpenSearchClient } from './migrations/core/'; +import { + SavedObjectsPermissionControl, + SavedObjectsPermissionControlContract, +} from './permission_control/client'; +import { registerPermissionCheckRoutes } from './permission_control/routes'; /** * Saved Objects is OpenSearchDashboards's data persistence mechanism allowing plugins to * use OpenSearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods @@ -175,6 +180,8 @@ export interface SavedObjectsServiceSetup { setRepositoryFactoryProvider: ( respositoryFactoryProvider: SavedObjectRepositoryFactoryProvider ) => void; + + permissionControl: SavedObjectsPermissionControlContract; } /** @@ -301,6 +308,7 @@ export class SavedObjectsService private started = false; private respositoryFactoryProvider?: SavedObjectRepositoryFactoryProvider; + private permissionControl?: SavedObjectsPermissionControlContract; constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('savedobjects-service'); @@ -328,6 +336,13 @@ export class SavedObjectsService migratorPromise: this.migrator$.pipe(first()).toPromise(), }); + this.permissionControl = new SavedObjectsPermissionControl(); + + registerPermissionCheckRoutes({ + http: setupDeps.http, + permissionControl: this.permissionControl, + }); + return { status$: calculateStatus$( this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())), @@ -368,6 +383,7 @@ export class SavedObjectsService } this.respositoryFactoryProvider = repositoryProvider; }, + permissionControl: this.permissionControl, }; } @@ -483,8 +499,11 @@ export class SavedObjectsService this.started = true; + const getScopedClient = clientProvider.getClient.bind(clientProvider); + this.permissionControl?.setup(getScopedClient); + return { - getScopedClient: clientProvider.getClient.bind(clientProvider), + getScopedClient, createScopedRepository: repositoryFactory.createScopedRepository, createInternalRepository: repositoryFactory.createInternalRepository, createSerializer: () => new SavedObjectsSerializer(this.typeRegistry), diff --git a/src/core/server/workspaces/index.ts b/src/core/server/workspaces/index.ts index c1f88784aa0e..6a312f00484d 100644 --- a/src/core/server/workspaces/index.ts +++ b/src/core/server/workspaces/index.ts @@ -12,6 +12,5 @@ export { export { WorkspaceAttribute, WorkspaceFindOptions } from './types'; -export { WorkspacePermissionControl } from './workspace_permission_control'; export { workspacesValidator, formatWorkspaces } from './utils'; export { WORKSPACE_TYPE } from './constants'; diff --git a/src/core/server/workspaces/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/core/server/workspaces/saved_objects/workspace_saved_objects_client_wrapper.ts index 25c3aa157d9f..0e4452dc23e3 100644 --- a/src/core/server/workspaces/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/core/server/workspaces/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -18,10 +18,9 @@ import { SavedObjectsDeleteOptions, SavedObjectsFindOptions, } from 'opensearch-dashboards/server'; -import { - WorkspacePermissionControl, - WorkspacePermissionMode, -} from '../workspace_permission_control'; +import { SavedObjectsPermissionControlContract } from '../../saved_objects/permission_control/client'; +import { WORKSPACE_TYPE } from '../constants'; +import { PermissionMode } from '../../../utils'; // Can't throw unauthorized for now, the page will be refreshed if unauthorized const generateWorkspacePermissionError = () => @@ -42,16 +41,34 @@ const isWorkspacesLikeAttributes = (attributes: unknown): attributes is Attribut Array.isArray((attributes as { workspaces: unknown }).workspaces); export class WorkspaceSavedObjectsClientWrapper { + private formatPermissionModeToStringArray( + permission: PermissionMode | PermissionMode[] + ): string[] { + if (Array.isArray(permission)) { + return permission; + } + + return [permission]; + } private async validateMultiWorkspacesPermissions( workspaces: string[] | undefined, request: OpenSearchDashboardsRequest, - permissionMode: WorkspacePermissionMode | WorkspacePermissionMode[] + permissionMode: PermissionMode | PermissionMode[] ) { if (!workspaces) { return; } for (const workspaceId of workspaces) { - if (!(await this.permissionControl.validate(workspaceId, permissionMode, request))) { + if ( + !(await this.permissionControl.validate( + request, + { + type: WORKSPACE_TYPE, + id: workspaceId, + }, + this.formatPermissionModeToStringArray(permissionMode) + )) + ) { throw generateWorkspacePermissionError(); } } @@ -60,14 +77,23 @@ export class WorkspaceSavedObjectsClientWrapper { private async validateAtLeastOnePermittedWorkspaces( workspaces: string[] | undefined, request: OpenSearchDashboardsRequest, - permissionMode: WorkspacePermissionMode | WorkspacePermissionMode[] + permissionMode: PermissionMode | PermissionMode[] ) { if (!workspaces) { return; } let permitted = false; for (const workspaceId of workspaces) { - if (await this.permissionControl.validate(workspaceId, permissionMode, request)) { + if ( + await this.permissionControl.validate( + request, + { + type: WORKSPACE_TYPE, + id: workspaceId, + }, + this.formatPermissionModeToStringArray(permissionMode) + ) + ) { permitted = true; break; } @@ -87,7 +113,7 @@ export class WorkspaceSavedObjectsClientWrapper { await this.validateMultiWorkspacesPermissions( objectToDeleted.workspaces, wrapperOptions.request, - WorkspacePermissionMode.Admin + PermissionMode.Management ); return await wrapperOptions.client.delete(type, id, options); }; @@ -108,7 +134,7 @@ export class WorkspaceSavedObjectsClientWrapper { await this.validateMultiWorkspacesPermissions( attributes.workspaces, wrapperOptions.request, - WorkspacePermissionMode.Admin + PermissionMode.Management ); } return await wrapperOptions.client.create(type, attributes, options); @@ -123,7 +149,7 @@ export class WorkspaceSavedObjectsClientWrapper { await this.validateAtLeastOnePermittedWorkspaces( objectToGet.workspaces, wrapperOptions.request, - WorkspacePermissionMode.Read + PermissionMode.Read ); return objectToGet; }; @@ -137,7 +163,7 @@ export class WorkspaceSavedObjectsClientWrapper { await this.validateAtLeastOnePermittedWorkspaces( object.workspaces, wrapperOptions.request, - WorkspacePermissionMode.Read + PermissionMode.Read ); } return objectToBulkGet; @@ -150,18 +176,20 @@ export class WorkspaceSavedObjectsClientWrapper { options.workspaces = options.workspaces.filter( async (workspaceId) => await this.permissionControl.validate( - workspaceId, - WorkspacePermissionMode.Read, - wrapperOptions.request + wrapperOptions.request, + { + type: WORKSPACE_TYPE, + id: workspaceId, + }, + [PermissionMode.Read] ) ); } else { options.workspaces = [ 'public', - ...(await this.permissionControl.getPermittedWorkspaceIds( - WorkspacePermissionMode.Read, - wrapperOptions.request - )), + ...(await this.permissionControl.getPermittedWorkspaceIds(wrapperOptions.request, [ + PermissionMode.Read, + ])), ]; } return await wrapperOptions.client.find(options); @@ -184,5 +212,5 @@ export class WorkspaceSavedObjectsClientWrapper { }; }; - constructor(private readonly permissionControl: WorkspacePermissionControl) {} + constructor(private readonly permissionControl: SavedObjectsPermissionControlContract) {} } diff --git a/src/core/server/workspaces/workspace_permission_control.ts b/src/core/server/workspaces/workspace_permission_control.ts deleted file mode 100644 index 203ce354561e..000000000000 --- a/src/core/server/workspaces/workspace_permission_control.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { OpenSearchDashboardsRequest } from '../http'; - -export enum WorkspacePermissionMode { - Read, - Admin, -} - -export class WorkspacePermissionControl { - public async validate( - workspaceId: string, - permissionModeOrModes: WorkspacePermissionMode | WorkspacePermissionMode[], - request: OpenSearchDashboardsRequest - ) { - return true; - } - - public async getPermittedWorkspaceIds( - permissionModeOrModes: WorkspacePermissionMode | WorkspacePermissionMode[], - request: OpenSearchDashboardsRequest - ) { - return []; - } - - public async setup() {} -} diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index 30be3b9d7526..cac4b551650b 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -14,18 +14,15 @@ import { } from '../saved_objects'; 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; - permissionControl: WorkspacePermissionControl; } export interface WorkspacesServiceStart { client: IWorkspaceDBImpl; - permissionControl: WorkspacePermissionControl; } export interface WorkspacesSetupDeps { @@ -46,7 +43,6 @@ export class WorkspacesService implements CoreService { private logger: Logger; private client?: IWorkspaceDBImpl; - private permissionControl?: WorkspacePermissionControl; private startDeps?: WorkspacesStartDeps; constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('workspaces-service'); @@ -87,12 +83,10 @@ export class WorkspacesService this.logger.debug('Setting up Workspaces service'); this.client = new WorkspacesClientWithSavedObject(setupDeps); - this.permissionControl = new WorkspacePermissionControl(); await this.client.setup(setupDeps); - await this.permissionControl.setup(); const workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper( - this.permissionControl + setupDeps.savedObject.permissionControl ); setupDeps.savedObject.addClientWrapper( @@ -111,7 +105,6 @@ export class WorkspacesService return { client: this.client, - permissionControl: this.permissionControl, }; } @@ -121,7 +114,6 @@ export class WorkspacesService return { client: this.client as IWorkspaceDBImpl, - permissionControl: this.permissionControl as WorkspacePermissionControl, }; } diff --git a/src/core/utils/constants.ts b/src/core/utils/constants.ts index e35ff014b387..5bd25db2c848 100644 --- a/src/core/utils/constants.ts +++ b/src/core/utils/constants.ts @@ -4,3 +4,11 @@ */ export const WORKSPACE_PATH_PREFIX = '/w'; + +export enum PermissionMode { + Read = 'read', + Write = 'write', + Management = 'management', + LibraryRead = 'library_read', + LibraryWrite = 'library_write', +} diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 5c3130c500de..174152ffd750 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -38,4 +38,4 @@ export { } from './context'; export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; export { DEFAULT_WORKSPACE_TEMPLATES } from './default_workspace_templates'; -export { WORKSPACE_PATH_PREFIX } from './constants'; +export { WORKSPACE_PATH_PREFIX, PermissionMode } from './constants';