From 183a9fc73719cbd9435af4e31e4005421b62ca39 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Thu, 15 Aug 2024 12:51:04 +0300 Subject: [PATCH] feat: support private projects for event search (#7884) Adds private projects support for event search. Unit test should cover the usecases. --- .../events/event-search-controller.ts | 16 ++-- src/lib/features/events/event-service.test.ts | 93 +++++++++++++++++++ src/lib/features/events/event-service.ts | 47 +++++++++- 3 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 src/lib/features/events/event-service.test.ts diff --git a/src/lib/features/events/event-search-controller.ts b/src/lib/features/events/event-search-controller.ts index d7979e31a7..5aea1fc83d 100644 --- a/src/lib/features/events/event-search-controller.ts +++ b/src/lib/features/events/event-search-controller.ts @@ -19,7 +19,7 @@ import { normalizeQueryParams } from '../../features/feature-search/search-utils import Controller from '../../routes/controller'; import type { IAuthRequest } from '../../server-impl'; import type { IEvent } from '../../types'; -import { anonymiseKeys } from '../../util'; +import { anonymiseKeys, extractUserIdFromUser } from '../../util'; const ANON_KEYS = ['email', 'username', 'createdBy']; const version = 1 as const; @@ -67,6 +67,7 @@ export default class EventSearchController extends Controller { req: IAuthRequest, res: Response, ): Promise { + const { user } = req; const { normalizedLimit, normalizedOffset } = normalizeQueryParams( req.query, { @@ -75,11 +76,14 @@ export default class EventSearchController extends Controller { }, ); - const { events, totalEvents } = await this.eventService.searchEvents({ - ...req.query, - offset: normalizedOffset, - limit: normalizedLimit, - }); + const { events, totalEvents } = await this.eventService.searchEvents( + { + ...req.query, + offset: normalizedOffset, + limit: normalizedLimit, + }, + extractUserIdFromUser(user), + ); this.openApiService.respondWithValidation( 200, diff --git a/src/lib/features/events/event-service.test.ts b/src/lib/features/events/event-service.test.ts new file mode 100644 index 0000000000..79f93d5109 --- /dev/null +++ b/src/lib/features/events/event-service.test.ts @@ -0,0 +1,93 @@ +import type { ProjectAccess } from '../private-project/privateProjectStore'; +import { filterAccessibleProjects } from './event-service'; + +describe('filterPrivateProjectsFromParams', () => { + it('should return IS_ANY_OF with allowed projects when projectParam is undefined and mode is limited', () => { + const projectAccess: ProjectAccess = { + mode: 'limited', + projects: ['project1', 'project2'], + }; + + const projectParam = undefined; + + const result = filterAccessibleProjects(projectParam, projectAccess); + + expect(result).toBe('IS_ANY_OF:project1,project2'); + }); + + it('should return the original projectParam when mode is all', () => { + const projectAccess: ProjectAccess = { + mode: 'all', + }; + + const projectParam = 'IS:project3'; + + const result = filterAccessibleProjects(projectParam, projectAccess); + + expect(result).toBe(projectParam); + }); + + it('should filter out projects not in allowedProjects when mode is limited', () => { + const projectAccess: ProjectAccess = { + mode: 'limited', + projects: ['project1', 'project2'], + }; + + const projectParam = 'IS_ANY_OF:project1,project3'; + + const result = filterAccessibleProjects(projectParam, projectAccess); + + expect(result).toBe('IS_ANY_OF:project1'); + }); + + it('should return a single project if only one is allowed', () => { + const projectAccess: ProjectAccess = { + mode: 'limited', + projects: ['project1'], + }; + + const projectParam = 'IS_ANY_OF:project1,project2'; + + const result = filterAccessibleProjects(projectParam, projectAccess); + + expect(result).toBe('IS_ANY_OF:project1'); + }); + + it('should return undefined if projectParam is undefined and projectAccess mode is all', () => { + const projectAccess: ProjectAccess = { + mode: 'all', + }; + + const projectParam = undefined; + + const result = filterAccessibleProjects(projectParam, projectAccess); + + expect(result).toBeUndefined(); + }); + + it('should return the original projectParam if all projects are allowed when mode is limited', () => { + const projectAccess: ProjectAccess = { + mode: 'limited', + projects: ['project1', 'project2', 'project3'], + }; + + const projectParam = 'IS_ANY_OF:project1,project2'; + + const result = filterAccessibleProjects(projectParam, projectAccess); + + expect(result).toBe('IS_ANY_OF:project1,project2'); + }); + + it('should throw an error if no projects match', () => { + const projectAccess: ProjectAccess = { + mode: 'limited', + projects: ['project1', 'project2'], + }; + + const projectParam = 'IS_ANY_OF:project3,project4'; + + expect(() => + filterAccessibleProjects(projectParam, projectAccess), + ).toThrow('No accessible projects in the search parameters'); + }); +}); diff --git a/src/lib/features/events/event-service.ts b/src/lib/features/events/event-service.ts index 3c108a94a7..919870ed35 100644 --- a/src/lib/features/events/event-service.ts +++ b/src/lib/features/events/event-service.ts @@ -15,6 +15,7 @@ import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strateg import { parseSearchOperatorValue } from '../feature-search/search-utils'; import { endOfDay, formatISO } from 'date-fns'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType'; +import type { ProjectAccess } from '../private-project/privateProjectStore'; export default class EventService { private logger: Logger; @@ -63,8 +64,20 @@ export default class EventService { }; } - async searchEvents(search: IEventSearchParams): Promise { + async searchEvents( + search: IEventSearchParams, + userId: number, + ): Promise { + const projectAccess = + await this.privateProjectChecker.getUserAccessibleProjects(userId); + + search.project = filterAccessibleProjects( + search.project, + projectAccess, + ); + const queryParams = this.convertToDbParams(search); + const totalEvents = await this.eventStore.searchEventsCount( { limit: search.limit, @@ -210,3 +223,35 @@ export default class EventService { return this.eventStore.getEventCreators(); } } + +export const filterAccessibleProjects = ( + projectParam: string | undefined, + projectAccess: ProjectAccess, +): string | undefined => { + if (projectAccess.mode !== 'all') { + const allowedProjects = projectAccess.projects; + + if (!projectParam) { + return `IS_ANY_OF:${allowedProjects.join(',')}`; + } else { + const searchProjectList = projectParam.split(','); + const filteredProjects = searchProjectList + .filter((proj) => + allowedProjects.includes( + proj.replace(/^(IS|IS_ANY_OF):/, ''), + ), + ) + .join(','); + + if (!filteredProjects) { + throw new Error( + 'No accessible projects in the search parameters', + ); + } + + return filteredProjects; + } + } + + return projectParam; +};