From 3a31877a4f4752bde7f68bf1307b37154b2f13c3 Mon Sep 17 00:00:00 2001 From: sjaanus Date: Thu, 15 Aug 2024 11:33:50 +0300 Subject: [PATCH 1/5] feat: support private projects for event search --- .../events/event-search-controller.ts | 16 ++- src/lib/features/events/event-service.test.ts | 111 ++++++++++++++++++ src/lib/features/events/event-service.ts | 47 +++++++- 3 files changed, 167 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 d7979e31a701..5aea1fc83d18 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 000000000000..20238c77d2da --- /dev/null +++ b/src/lib/features/events/event-service.test.ts @@ -0,0 +1,111 @@ +import type { ProjectAccess } from '../private-project/privateProjectStore'; +import { filterPrivateProjectsFromParams } 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 = filterPrivateProjectsFromParams( + 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:project1,IS:project3'; + + const result = filterPrivateProjectsFromParams( + 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:project1,IS:project3'; + + const result = filterPrivateProjectsFromParams( + projectParam, + projectAccess, + ); + + expect(result).toBe('IS:project1'); + }); + + it('should return a single project if only one is allowed', () => { + const projectAccess: ProjectAccess = { + mode: 'limited', + projects: ['project1'], + }; + + const projectParam = 'IS:project1,IS:project2'; + + const result = filterPrivateProjectsFromParams( + projectParam, + projectAccess, + ); + + expect(result).toBe('IS:project1'); + }); + + it('should throw an error if no projects match and not throw an error', () => { + const projectAccess: ProjectAccess = { + mode: 'limited', + projects: ['project1', 'project2'], + }; + + const projectParam = 'IS:project3,IS:project4'; + + expect(() => + filterPrivateProjectsFromParams(projectParam, projectAccess), + ).toThrow('No accessible projects in the search parameters'); + }); + + it('should return undefined if projectParam is undefined and projectAccess mode is all', () => { + const projectAccess: ProjectAccess = { + mode: 'all', + }; + + const projectParam = undefined; + + const result = filterPrivateProjectsFromParams( + 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:project1,IS:project2'; + + const result = filterPrivateProjectsFromParams( + projectParam, + projectAccess, + ); + + expect(result).toBe('IS:project1,IS:project2'); + }); +}); diff --git a/src/lib/features/events/event-service.ts b/src/lib/features/events/event-service.ts index 3c108a94a738..af6a075846c7 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 = filterPrivateProjectsFromParams( + 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 filterPrivateProjectsFromParams = ( + 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; +}; From 2e26e3166538cf96bf09ed7885a15015dc020e73 Mon Sep 17 00:00:00 2001 From: sjaanus Date: Thu, 15 Aug 2024 11:35:56 +0300 Subject: [PATCH 2/5] Fix --- src/lib/features/events/event-service.test.ts | 34 +++++-------------- src/lib/features/events/event-service.ts | 4 +-- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/src/lib/features/events/event-service.test.ts b/src/lib/features/events/event-service.test.ts index 20238c77d2da..3002641c83db 100644 --- a/src/lib/features/events/event-service.test.ts +++ b/src/lib/features/events/event-service.test.ts @@ -1,5 +1,5 @@ import type { ProjectAccess } from '../private-project/privateProjectStore'; -import { filterPrivateProjectsFromParams } from './event-service'; +import { filterAccessibleProjects } from './event-service'; describe('filterPrivateProjectsFromParams', () => { it('should return IS_ANY_OF with allowed projects when projectParam is undefined and mode is limited', () => { @@ -10,10 +10,7 @@ describe('filterPrivateProjectsFromParams', () => { const projectParam = undefined; - const result = filterPrivateProjectsFromParams( - projectParam, - projectAccess, - ); + const result = filterAccessibleProjects(projectParam, projectAccess); expect(result).toBe('IS_ANY_OF:project1,project2'); }); @@ -25,10 +22,7 @@ describe('filterPrivateProjectsFromParams', () => { const projectParam = 'IS:project1,IS:project3'; - const result = filterPrivateProjectsFromParams( - projectParam, - projectAccess, - ); + const result = filterAccessibleProjects(projectParam, projectAccess); expect(result).toBe(projectParam); }); @@ -41,10 +35,7 @@ describe('filterPrivateProjectsFromParams', () => { const projectParam = 'IS:project1,IS:project3'; - const result = filterPrivateProjectsFromParams( - projectParam, - projectAccess, - ); + const result = filterAccessibleProjects(projectParam, projectAccess); expect(result).toBe('IS:project1'); }); @@ -57,10 +48,7 @@ describe('filterPrivateProjectsFromParams', () => { const projectParam = 'IS:project1,IS:project2'; - const result = filterPrivateProjectsFromParams( - projectParam, - projectAccess, - ); + const result = filterAccessibleProjects(projectParam, projectAccess); expect(result).toBe('IS:project1'); }); @@ -74,7 +62,7 @@ describe('filterPrivateProjectsFromParams', () => { const projectParam = 'IS:project3,IS:project4'; expect(() => - filterPrivateProjectsFromParams(projectParam, projectAccess), + filterAccessibleProjects(projectParam, projectAccess), ).toThrow('No accessible projects in the search parameters'); }); @@ -85,10 +73,7 @@ describe('filterPrivateProjectsFromParams', () => { const projectParam = undefined; - const result = filterPrivateProjectsFromParams( - projectParam, - projectAccess, - ); + const result = filterAccessibleProjects(projectParam, projectAccess); expect(result).toBeUndefined(); }); @@ -101,10 +86,7 @@ describe('filterPrivateProjectsFromParams', () => { const projectParam = 'IS:project1,IS:project2'; - const result = filterPrivateProjectsFromParams( - projectParam, - projectAccess, - ); + const result = filterAccessibleProjects(projectParam, projectAccess); expect(result).toBe('IS:project1,IS:project2'); }); diff --git a/src/lib/features/events/event-service.ts b/src/lib/features/events/event-service.ts index af6a075846c7..919870ed35aa 100644 --- a/src/lib/features/events/event-service.ts +++ b/src/lib/features/events/event-service.ts @@ -71,7 +71,7 @@ export default class EventService { const projectAccess = await this.privateProjectChecker.getUserAccessibleProjects(userId); - search.project = filterPrivateProjectsFromParams( + search.project = filterAccessibleProjects( search.project, projectAccess, ); @@ -224,7 +224,7 @@ export default class EventService { } } -export const filterPrivateProjectsFromParams = ( +export const filterAccessibleProjects = ( projectParam: string | undefined, projectAccess: ProjectAccess, ): string | undefined => { From dac9630523b2b5044506785dd76baeda8cefaef7 Mon Sep 17 00:00:00 2001 From: sjaanus Date: Thu, 15 Aug 2024 11:51:13 +0300 Subject: [PATCH 3/5] Fix --- src/lib/features/events/event-service.test.ts | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/lib/features/events/event-service.test.ts b/src/lib/features/events/event-service.test.ts index 3002641c83db..32a50aacf60d 100644 --- a/src/lib/features/events/event-service.test.ts +++ b/src/lib/features/events/event-service.test.ts @@ -20,7 +20,7 @@ describe('filterPrivateProjectsFromParams', () => { mode: 'all', }; - const projectParam = 'IS:project1,IS:project3'; + const projectParam = 'IS_ANY_OF:project1,IS:project3'; const result = filterAccessibleProjects(projectParam, projectAccess); @@ -33,11 +33,11 @@ describe('filterPrivateProjectsFromParams', () => { projects: ['project1', 'project2'], }; - const projectParam = 'IS:project1,IS:project3'; + const projectParam = 'IS_ANY_OF:project1,IS:project3'; const result = filterAccessibleProjects(projectParam, projectAccess); - expect(result).toBe('IS:project1'); + expect(result).toBe('IS_ANY_OF:project1'); }); it('should return a single project if only one is allowed', () => { @@ -46,24 +46,11 @@ describe('filterPrivateProjectsFromParams', () => { projects: ['project1'], }; - const projectParam = 'IS:project1,IS:project2'; + const projectParam = 'IS_ANY_OF:project1,IS:project2'; const result = filterAccessibleProjects(projectParam, projectAccess); - expect(result).toBe('IS:project1'); - }); - - it('should throw an error if no projects match and not throw an error', () => { - const projectAccess: ProjectAccess = { - mode: 'limited', - projects: ['project1', 'project2'], - }; - - const projectParam = 'IS:project3,IS:project4'; - - expect(() => - filterAccessibleProjects(projectParam, projectAccess), - ).toThrow('No accessible projects in the search parameters'); + expect(result).toBe('IS_ANY_OF:project1'); }); it('should return undefined if projectParam is undefined and projectAccess mode is all', () => { @@ -84,10 +71,10 @@ describe('filterPrivateProjectsFromParams', () => { projects: ['project1', 'project2', 'project3'], }; - const projectParam = 'IS:project1,IS:project2'; + const projectParam = 'IS_ANY_OF:project1,IS:project2'; const result = filterAccessibleProjects(projectParam, projectAccess); - expect(result).toBe('IS:project1,IS:project2'); + expect(result).toBe('IS_ANY_OF:project1,IS:project2'); }); }); From a0ebbc2c41f9cf86a167401d796f85ab82744299 Mon Sep 17 00:00:00 2001 From: sjaanus Date: Thu, 15 Aug 2024 11:56:39 +0300 Subject: [PATCH 4/5] Fix --- src/lib/features/events/event-service.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib/features/events/event-service.test.ts b/src/lib/features/events/event-service.test.ts index 32a50aacf60d..51bb755b99cd 100644 --- a/src/lib/features/events/event-service.test.ts +++ b/src/lib/features/events/event-service.test.ts @@ -77,4 +77,17 @@ describe('filterPrivateProjectsFromParams', () => { expect(result).toBe('IS_ANY_OF:project1,IS: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'); + }); }); From 1485e2f5a72815c59f5b12f472de33e60d94316f Mon Sep 17 00:00:00 2001 From: sjaanus Date: Thu, 15 Aug 2024 11:59:04 +0300 Subject: [PATCH 5/5] Fix --- src/lib/features/events/event-service.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/features/events/event-service.test.ts b/src/lib/features/events/event-service.test.ts index 51bb755b99cd..79f93d5109fb 100644 --- a/src/lib/features/events/event-service.test.ts +++ b/src/lib/features/events/event-service.test.ts @@ -20,7 +20,7 @@ describe('filterPrivateProjectsFromParams', () => { mode: 'all', }; - const projectParam = 'IS_ANY_OF:project1,IS:project3'; + const projectParam = 'IS:project3'; const result = filterAccessibleProjects(projectParam, projectAccess); @@ -33,7 +33,7 @@ describe('filterPrivateProjectsFromParams', () => { projects: ['project1', 'project2'], }; - const projectParam = 'IS_ANY_OF:project1,IS:project3'; + const projectParam = 'IS_ANY_OF:project1,project3'; const result = filterAccessibleProjects(projectParam, projectAccess); @@ -46,7 +46,7 @@ describe('filterPrivateProjectsFromParams', () => { projects: ['project1'], }; - const projectParam = 'IS_ANY_OF:project1,IS:project2'; + const projectParam = 'IS_ANY_OF:project1,project2'; const result = filterAccessibleProjects(projectParam, projectAccess); @@ -71,11 +71,11 @@ describe('filterPrivateProjectsFromParams', () => { projects: ['project1', 'project2', 'project3'], }; - const projectParam = 'IS_ANY_OF:project1,IS:project2'; + const projectParam = 'IS_ANY_OF:project1,project2'; const result = filterAccessibleProjects(projectParam, projectAccess); - expect(result).toBe('IS_ANY_OF:project1,IS:project2'); + expect(result).toBe('IS_ANY_OF:project1,project2'); }); it('should throw an error if no projects match', () => {