1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: support private projects for event search (#7884)

Adds private projects support for event search. Unit test should cover
the usecases.
This commit is contained in:
Jaanus Sellin 2024-08-15 12:51:04 +03:00 committed by GitHub
parent a89f05181d
commit 183a9fc737
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 149 additions and 7 deletions

View File

@ -19,7 +19,7 @@ import { normalizeQueryParams } from '../../features/feature-search/search-utils
import Controller from '../../routes/controller'; import Controller from '../../routes/controller';
import type { IAuthRequest } from '../../server-impl'; import type { IAuthRequest } from '../../server-impl';
import type { IEvent } from '../../types'; import type { IEvent } from '../../types';
import { anonymiseKeys } from '../../util'; import { anonymiseKeys, extractUserIdFromUser } from '../../util';
const ANON_KEYS = ['email', 'username', 'createdBy']; const ANON_KEYS = ['email', 'username', 'createdBy'];
const version = 1 as const; const version = 1 as const;
@ -67,6 +67,7 @@ export default class EventSearchController extends Controller {
req: IAuthRequest<any, any, any, EventSearchQueryParameters>, req: IAuthRequest<any, any, any, EventSearchQueryParameters>,
res: Response<EventSearchResponseSchema>, res: Response<EventSearchResponseSchema>,
): Promise<void> { ): Promise<void> {
const { user } = req;
const { normalizedLimit, normalizedOffset } = normalizeQueryParams( const { normalizedLimit, normalizedOffset } = normalizeQueryParams(
req.query, req.query,
{ {
@ -75,11 +76,14 @@ export default class EventSearchController extends Controller {
}, },
); );
const { events, totalEvents } = await this.eventService.searchEvents({ const { events, totalEvents } = await this.eventService.searchEvents(
...req.query, {
offset: normalizedOffset, ...req.query,
limit: normalizedLimit, offset: normalizedOffset,
}); limit: normalizedLimit,
},
extractUserIdFromUser(user),
);
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,

View File

@ -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');
});
});

View File

@ -15,6 +15,7 @@ import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strateg
import { parseSearchOperatorValue } from '../feature-search/search-utils'; import { parseSearchOperatorValue } from '../feature-search/search-utils';
import { endOfDay, formatISO } from 'date-fns'; import { endOfDay, formatISO } from 'date-fns';
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
import type { ProjectAccess } from '../private-project/privateProjectStore';
export default class EventService { export default class EventService {
private logger: Logger; private logger: Logger;
@ -63,8 +64,20 @@ export default class EventService {
}; };
} }
async searchEvents(search: IEventSearchParams): Promise<IEventList> { async searchEvents(
search: IEventSearchParams,
userId: number,
): Promise<IEventList> {
const projectAccess =
await this.privateProjectChecker.getUserAccessibleProjects(userId);
search.project = filterAccessibleProjects(
search.project,
projectAccess,
);
const queryParams = this.convertToDbParams(search); const queryParams = this.convertToDbParams(search);
const totalEvents = await this.eventStore.searchEventsCount( const totalEvents = await this.eventStore.searchEventsCount(
{ {
limit: search.limit, limit: search.limit,
@ -210,3 +223,35 @@ export default class EventService {
return this.eventStore.getEventCreators(); 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;
};