fix(gateway): enforce task and mission ownership
This commit is contained in:
@@ -86,4 +86,52 @@ describe('Resource ownership checks', () => {
|
||||
ForbiddenException,
|
||||
);
|
||||
});
|
||||
|
||||
it('forbids creating a task with an unowned project', async () => {
|
||||
const brain = createBrain();
|
||||
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||
const controller = new TasksController(brain as never);
|
||||
|
||||
await expect(
|
||||
controller.create(
|
||||
{
|
||||
title: 'Task',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
{ id: 'user-1' },
|
||||
),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('forbids listing tasks for an unowned project', async () => {
|
||||
const brain = createBrain();
|
||||
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||
const controller = new TasksController(brain as never);
|
||||
|
||||
await expect(
|
||||
controller.list({ id: 'user-1' }, 'project-1', undefined, undefined),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('lists only tasks for the current user owned projects when no filter is provided', async () => {
|
||||
const brain = createBrain();
|
||||
brain.projects.findAll.mockResolvedValue([
|
||||
{ id: 'project-1', ownerId: 'user-1' },
|
||||
{ id: 'project-2', ownerId: 'user-2' },
|
||||
]);
|
||||
brain.missions.findAll.mockResolvedValue([{ id: 'mission-1', projectId: 'project-1' }]);
|
||||
brain.tasks.findAll.mockResolvedValue([
|
||||
{ id: 'task-1', projectId: 'project-1' },
|
||||
{ id: 'task-2', missionId: 'mission-1' },
|
||||
{ id: 'task-3', projectId: 'project-2' },
|
||||
]);
|
||||
const controller = new TasksController(brain as never);
|
||||
|
||||
await expect(
|
||||
controller.list({ id: 'user-1' }, undefined, undefined, undefined),
|
||||
).resolves.toEqual([
|
||||
{ id: 'task-1', projectId: 'project-1' },
|
||||
{ id: 'task-2', missionId: 'mission-1' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,7 +36,10 @@ export class MissionsController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() dto: CreateMissionDto) {
|
||||
async create(@Body() dto: CreateMissionDto, @CurrentUser() user: { id: string }) {
|
||||
if (dto.projectId) {
|
||||
await this.getOwnedProject(dto.projectId, user.id, 'Mission');
|
||||
}
|
||||
return this.brain.missions.create({
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
|
||||
@@ -28,17 +28,48 @@ export class TasksController {
|
||||
|
||||
@Get()
|
||||
async list(
|
||||
@CurrentUser() user: { id: string },
|
||||
@Query('projectId') projectId?: string,
|
||||
@Query('missionId') missionId?: string,
|
||||
@Query('status') status?: string,
|
||||
) {
|
||||
if (projectId) return this.brain.tasks.findByProject(projectId);
|
||||
if (missionId) return this.brain.tasks.findByMission(missionId);
|
||||
if (status)
|
||||
return this.brain.tasks.findByStatus(
|
||||
status as Parameters<typeof this.brain.tasks.findByStatus>[0],
|
||||
);
|
||||
return this.brain.tasks.findAll();
|
||||
if (projectId) {
|
||||
await this.getOwnedProject(projectId, user.id, 'Task');
|
||||
return this.brain.tasks.findByProject(projectId);
|
||||
}
|
||||
if (missionId) {
|
||||
await this.getOwnedMission(missionId, user.id, 'Task');
|
||||
return this.brain.tasks.findByMission(missionId);
|
||||
}
|
||||
|
||||
const [projects, missions, tasks] = await Promise.all([
|
||||
this.brain.projects.findAll(),
|
||||
this.brain.missions.findAll(),
|
||||
status
|
||||
? this.brain.tasks.findByStatus(
|
||||
status as Parameters<typeof this.brain.tasks.findByStatus>[0],
|
||||
)
|
||||
: this.brain.tasks.findAll(),
|
||||
]);
|
||||
|
||||
const ownedProjectIds = new Set(
|
||||
projects.filter((project) => project.ownerId === user.id).map((project) => project.id),
|
||||
);
|
||||
const ownedMissionIds = new Set(
|
||||
missions
|
||||
.filter(
|
||||
(ownedMission) =>
|
||||
typeof ownedMission.projectId === 'string' &&
|
||||
ownedProjectIds.has(ownedMission.projectId),
|
||||
)
|
||||
.map((ownedMission) => ownedMission.id),
|
||||
);
|
||||
|
||||
return tasks.filter(
|
||||
(task) =>
|
||||
(task.projectId ? ownedProjectIds.has(task.projectId) : false) ||
|
||||
(task.missionId ? ownedMissionIds.has(task.missionId) : false),
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -47,7 +78,13 @@ export class TasksController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() dto: CreateTaskDto) {
|
||||
async create(@Body() dto: CreateTaskDto, @CurrentUser() user: { id: string }) {
|
||||
if (dto.projectId) {
|
||||
await this.getOwnedProject(dto.projectId, user.id, 'Task');
|
||||
}
|
||||
if (dto.missionId) {
|
||||
await this.getOwnedMission(dto.missionId, user.id, 'Task');
|
||||
}
|
||||
return this.brain.tasks.create({
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
|
||||
Reference in New Issue
Block a user