diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 64f74255e2ad..356e555cfc53 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -363,6 +363,7 @@ export class SavedObjectsClient { namespaces: 'namespaces', preference: 'preference', workspaces: 'workspaces', + queryDSL: 'queryDSL', }; const workspaces = [ diff --git a/src/core/server/saved_objects/permission_control/acl.test.ts b/src/core/server/saved_objects/permission_control/acl.test.ts index b292fb747b3c..95da5ca4d33b 100644 --- a/src/core/server/saved_objects/permission_control/acl.test.ts +++ b/src/core/server/saved_objects/permission_control/acl.test.ts @@ -111,13 +111,13 @@ describe('SavedObjectTypeRegistry', () => { expect(result?.length).toEqual(3); }); - it('test genereate query DSL', () => { + it('test generate query DSL', () => { const principals = { users: ['user1'], groups: ['group1'], }; const result = ACL.genereateGetPermittedSavedObjectsQueryDSL( - PermissionMode.Read, + [PermissionMode.Read], principals, 'workspace' ); diff --git a/src/core/server/saved_objects/permission_control/acl.ts b/src/core/server/saved_objects/permission_control/acl.ts index 4b7d506e11ba..2bac6823634a 100644 --- a/src/core/server/saved_objects/permission_control/acl.ts +++ b/src/core/server/saved_objects/permission_control/acl.ts @@ -10,7 +10,7 @@ export interface Principals { groups?: string[]; } -export type Permissions = Partial>; +export type Permissions = Record; export interface TransformedPermission { type: string; @@ -121,11 +121,14 @@ export class ACL { } for (const permissionType of permissionTypes) { - this.permissions[permissionType] = deleteFromPrincipals( + const result = deleteFromPrincipals( this.permissions![permissionType], principals.users, principals.groups ); + if (result) { + this.permissions[permissionType] = result; + } } return this; @@ -204,11 +207,11 @@ export class ACL { generate query DSL by the specific conditions, used for fetching saved objects from the saved objects index */ public static genereateGetPermittedSavedObjectsQueryDSL( - permissionType: string, + permissionTypes: string[], principals: Principals, savedObjectType?: string | string[] ) { - if (!principals || !permissionType) { + if (!principals || !permissionTypes) { return { query: { match_none: {}, @@ -222,30 +225,21 @@ export class ACL { const subBool: any = { should: [], }; - if (!!principals.users) { - subBool.should.push({ - terms: { - ['permissions.' + permissionType + '.users']: principals.users, - }, - }); - subBool.should.push({ - term: { - ['permissions.' + permissionType + '.users']: '*', - }, - }); - } - if (!!principals.groups) { - subBool.should.push({ - terms: { - ['permissions.' + permissionType + '.groups']: principals.groups, - }, - }); - subBool.should.push({ - term: { - ['permissions.' + permissionType + '.groups']: '*', - }, + + permissionTypes.forEach((permissionType) => { + Object.entries(principals).forEach(([principalType, principalsInCurrentType]) => { + subBool.should.push({ + terms: { + ['permissions.' + permissionType + `.${principalType}`]: principalsInCurrentType, + }, + }); + subBool.should.push({ + term: { + ['permissions.' + permissionType + `.${principalType}`]: '*', + }, + }); }); - } + }); bool.filter.push({ bool: subBool, diff --git a/src/core/server/saved_objects/permission_control/client.mock.ts b/src/core/server/saved_objects/permission_control/client.mock.ts index 218e62dcd4d4..4cae55f62890 100644 --- a/src/core/server/saved_objects/permission_control/client.mock.ts +++ b/src/core/server/saved_objects/permission_control/client.mock.ts @@ -2,15 +2,13 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - import { SavedObjectsPermissionControlContract } from './client'; export const savedObjectsPermissionControlMock: SavedObjectsPermissionControlContract = { setup: jest.fn(), validate: jest.fn(), batchValidate: jest.fn(), - addPrinciplesToObjects: jest.fn(), - removePrinciplesFromObjects: jest.fn(), - getPrinciplesOfObjects: jest.fn(), + getPrincipalsOfObjects: jest.fn(), getPermittedWorkspaceIds: jest.fn(), + getPrincipalsFromRequest: jest.fn(), }; diff --git a/src/core/server/saved_objects/permission_control/client.ts b/src/core/server/saved_objects/permission_control/client.ts index ee2fad51c651..0e34a4bc8cb6 100644 --- a/src/core/server/saved_objects/permission_control/client.ts +++ b/src/core/server/saved_objects/permission_control/client.ts @@ -2,10 +2,14 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - +import { i18n } from '@osd/i18n'; import { OpenSearchDashboardsRequest } from '../../http'; +import { ensureRawRequest } from '../../http/router'; import { SavedObjectsServiceStart } from '../saved_objects_service'; import { SavedObjectsBulkGetObject } from '../service'; +import { ACL, Principals, TransformedPermission } from './acl'; +import { PrincipalType } from '../../../utils/constants'; +import { WORKSPACE_TYPE } from '../../workspaces'; export type SavedObjectsPermissionControlContract = Pick< SavedObjectsPermissionControl, @@ -14,78 +18,127 @@ export type SavedObjectsPermissionControlContract = Pick< export type SavedObjectsPermissionModes = string[]; +export interface AuthInfo { + backend_roles?: string[]; + user_name?: string; +} + export class SavedObjectsPermissionControl { - private getScopedClient?: SavedObjectsServiceStart['getScopedClient']; - private getScopedSavedObjectsClient(request: OpenSearchDashboardsRequest) { - return this.getScopedClient?.(request); + private createInternalRepository?: SavedObjectsServiceStart['createInternalRepository']; + private getInternalRepository() { + return this.createInternalRepository?.(); + } + 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[] ) { - return ( - (await this.getScopedSavedObjectsClient(request)?.bulkGet(savedObjects))?.saved_objects || [] - ); + return (await this.getInternalRepository()?.bulkGet(savedObjects))?.saved_objects || []; } - public async setup(getScopedClient: SavedObjectsServiceStart['getScopedClient']) { - this.getScopedClient = getScopedClient; + public async setup( + createInternalRepository: SavedObjectsServiceStart['createInternalRepository'] + ) { + this.createInternalRepository = createInternalRepository; } public async validate( request: OpenSearchDashboardsRequest, savedObject: SavedObjectsBulkGetObject, - permissionModeOrModes: SavedObjectsPermissionModes + permissionModes: SavedObjectsPermissionModes ) { - return await this.batchValidate(request, [savedObject], permissionModeOrModes); + return await this.batchValidate(request, [savedObject], permissionModes); } + /** + * In batch validate case, the logic is a.withPermission && b.withPermission + * @param request + * @param savedObjects + * @param permissionModes + * @returns + */ public async batchValidate( request: OpenSearchDashboardsRequest, savedObjects: SavedObjectsBulkGetObject[], - permissionModeOrModes: SavedObjectsPermissionModes + permissionModes: SavedObjectsPermissionModes ) { const savedObjectsGet = await this.bulkGetSavedObjects(request, savedObjects); if (savedObjectsGet) { + const principals = this.getPrincipalsFromRequest(request); + const hasAllPermission = savedObjectsGet.every((item) => { + // item.permissions + const aclInstance = new ACL(item.permissions); + return aclInstance.hasPermission(permissionModes, principals); + }); return { success: true, - result: true, + result: hasAllPermission, }; } return { - success: true, - result: false, + success: false, + error: i18n.translate('savedObjects.permission.notFound', { + defaultMessage: 'Can not find target saved objects.', + }), }; } - 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( + public async getPrincipalsOfObjects( request: OpenSearchDashboardsRequest, savedObjects: SavedObjectsBulkGetObject[] - ): Promise> { - return {}; + ): Promise> { + const detailedSavedObjects = await this.bulkGetSavedObjects(request, savedObjects); + return detailedSavedObjects.reduce((total, current) => { + return { + ...total, + [current.id]: new ACL(current.permissions).transformPermissions(), + }; + }, {}); } public async getPermittedWorkspaceIds( request: OpenSearchDashboardsRequest, - permissionModeOrModes: SavedObjectsPermissionModes + permissionModes: SavedObjectsPermissionModes ) { - return []; + const principals = this.getPrincipalsFromRequest(request); + const queryDSL = ACL.genereateGetPermittedSavedObjectsQueryDSL(permissionModes, principals, [ + WORKSPACE_TYPE, + ]); + const repository = this.getInternalRepository(); + try { + const result = await repository?.find({ + type: [WORKSPACE_TYPE], + queryDSL, + perPage: 999, + }); + return result?.saved_objects.map((item) => item.id); + } catch (e) { + return []; + } } } diff --git a/src/core/server/saved_objects/permission_control/routes/principles.ts b/src/core/server/saved_objects/permission_control/routes/principles.ts index 986bf46ed967..e21e4151146b 100644 --- a/src/core/server/saved_objects/permission_control/routes/principles.ts +++ b/src/core/server/saved_objects/permission_control/routes/principles.ts @@ -13,7 +13,7 @@ export const registerListRoute = ( ) => { router.post( { - path: '/principles', + path: '/principals', validate: { body: schema.object({ objects: schema.arrayOf( @@ -26,7 +26,7 @@ export const registerListRoute = ( }, }, router.handleLegacyErrors(async (context, req, res) => { - const result = await permissionControl.getPrinciplesOfObjects(req, req.body.objects); + const result = await permissionControl.getPrincipalsOfObjects(req, req.body.objects); return res.ok({ body: result }); }) ); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 1000948a3ee0..413490b8d221 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -500,7 +500,7 @@ export class SavedObjectsService this.started = true; const getScopedClient = clientProvider.getClient.bind(clientProvider); - this.permissionControl?.setup(getScopedClient); + this.permissionControl?.setup(repositoryFactory.createInternalRepository); return { getScopedClient, diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index 473a63cf65f4..360cdc6b3a62 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -28,6 +28,7 @@ * under the License. */ +import { Permissions } from '../permission_control/acl'; import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types'; /** @@ -53,6 +54,7 @@ export interface SavedObjectsRawDocSource { references?: SavedObjectReference[]; originId?: string; workspaces?: string[]; + permissions?: Permissions; [typeMapping: string]: any; } diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 9545eec608b1..7b033b8e0211 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -769,6 +769,7 @@ export class SavedObjectsRepository { filter, preference, workspaces, + queryDSL, } = options; if (!type && !typeToNamespacesMap) { @@ -843,6 +844,7 @@ export class SavedObjectsRepository { hasReference, kueryNode, workspaces, + queryDSL, }), }, }; @@ -1897,7 +1899,7 @@ function getSavedObjectFromSource( id: string, doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } ): SavedObject { - const { originId, updated_at: updatedAt, workspaces } = doc._source; + const { originId, updated_at: updatedAt, workspaces, permissions } = doc._source; let namespaces: string[] = []; if (!registry.isNamespaceAgnostic(type)) { @@ -1917,6 +1919,7 @@ function getSavedObjectFromSource( attributes: doc._source[type], references: doc._source.references || [], migrationVersion: doc._source.migrationVersion, + permissions, }; } diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 5a2aae5943a6..4ddbe992bcfc 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -27,7 +27,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { mergeWith, isArray } from 'lodash'; // @ts-expect-error no ts import { opensearchKuery } from '../../../opensearch_query'; type KueryNode = any; @@ -174,6 +174,7 @@ interface QueryParams { hasReference?: HasReferenceQueryParams; kueryNode?: KueryNode; workspaces?: string[]; + queryDSL?: Record; } export function getClauseForReference(reference: HasReferenceQueryParams) { @@ -231,6 +232,7 @@ export function getQueryParams({ hasReference, kueryNode, workspaces, + queryDSL, }: QueryParams) { const types = getTypes( registry, @@ -287,7 +289,16 @@ export function getQueryParams({ } } - return { query: { bool } }; + const result = { query: { bool } }; + + if (queryDSL) { + return mergeWith({}, result, queryDSL, (objValue, srcValue) => { + if (isArray(objValue)) { + return objValue.concat(srcValue); + } + }); + } + return result; } // we only want to add match_phrase_prefix clauses diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index df6109eb9d0a..d6b8b83ac87e 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -53,6 +53,7 @@ interface GetSearchDslOptions { }; kueryNode?: KueryNode; workspaces?: string[]; + queryDSL?: Record; } export function getSearchDsl( @@ -73,6 +74,7 @@ export function getSearchDsl( hasReference, kueryNode, workspaces, + queryDSL, } = options; if (!type) { @@ -96,6 +98,7 @@ export function getSearchDsl( hasReference, kueryNode, workspaces, + queryDSL, }), ...getSortingParams(mappings, type, sortField, sortOrder), }; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 33862cb149fb..25ccbff66dd7 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -111,6 +111,7 @@ export interface SavedObjectsFindOptions { /** An optional OpenSearch preference value to be used for the query **/ preference?: string; workspaces?: string[]; + queryDSL?: Record; } /** 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 b812568d1be4..0a028b7e356e 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 @@ -23,6 +23,7 @@ import { import { SavedObjectsPermissionControlContract } from '../../saved_objects/permission_control/client'; import { WORKSPACE_TYPE } from '../constants'; import { PermissionMode } from '../../../utils'; +import { ACL } from '../../saved_objects/permission_control/acl'; // Can't throw unauthorized for now, the page will be refreshed if unauthorized const generateWorkspacePermissionError = () => @@ -112,6 +113,16 @@ export class WorkspaceSavedObjectsClientWrapper { } } + /** + * check if the type include workspace + * Workspace permission check is totally different from object permission check. + * @param type + * @returns + */ + private isRelatedToWorkspace(type: string | string[]): boolean { + return type === WORKSPACE_TYPE || (Array.isArray(type) && type.includes(WORKSPACE_TYPE)); + } + public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { const deleteWithWorkspacePermissionControl = async ( type: string, @@ -187,26 +198,45 @@ export class WorkspaceSavedObjectsClientWrapper { const findWithWorkspacePermissionControl = async ( options: SavedObjectsFindOptions ) => { - if (options.workspaces) { - options.workspaces = options.workspaces.filter( - async (workspaceId) => - await this.permissionControl.validate( - wrapperOptions.request, - { - type: WORKSPACE_TYPE, - id: workspaceId, - }, - [PermissionMode.Read] - ) + const principals = this.permissionControl.getPrincipalsFromRequest(wrapperOptions.request); + + if (this.isRelatedToWorkspace(options.type)) { + const queryDSLForQueryingWorkspaces = ACL.genereateGetPermittedSavedObjectsQueryDSL( + [PermissionMode.LibraryRead, PermissionMode.LibraryWrite, PermissionMode.Management], + principals, + WORKSPACE_TYPE ); + options.queryDSL = queryDSLForQueryingWorkspaces; } else { - options.workspaces = [ - 'public', - ...(await this.permissionControl.getPermittedWorkspaceIds(wrapperOptions.request, [ - PermissionMode.Read, - ])), - ]; + const permittedWorkspaceIds = await this.permissionControl.getPermittedWorkspaceIds( + wrapperOptions.request, + [PermissionMode.LibraryRead, PermissionMode.LibraryWrite, PermissionMode.Management] + ); + if (options.workspaces) { + const isEveryWorkspaceIsPermitted = options.workspaces.every((item) => + // TODO modify this line to use permittedWorkspaceIds if public workspace is also a workspace + ['public', ...(permittedWorkspaceIds || [])]?.includes(item) + ); + if (!isEveryWorkspaceIsPermitted) { + throw generateWorkspacePermissionError(); + } + } else { + const queryDSL = ACL.genereateGetPermittedSavedObjectsQueryDSL( + [ + PermissionMode.LibraryRead, + PermissionMode.LibraryWrite, + PermissionMode.Management, + PermissionMode.Read, + PermissionMode.Write, + ], + principals, + options.type + ); + options.workspaces = permittedWorkspaceIds; + options.queryDSL = queryDSL; + } } + return await wrapperOptions.client.find(options); }; diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index 47faffb0b922..fccce5f72947 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -9,6 +9,7 @@ * GitHub history for details. */ +import { Permissions } from '../server/saved_objects/permission_control/acl'; /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -114,6 +115,7 @@ export interface SavedObject { */ originId?: string; workspaces?: string[]; + permissions?: Permissions; } export interface SavedObjectError {