fix(security) : harden ROLE_CLIENT isolation + tighten cross-module contracts

Findings from the post-migration code review. The arrival of ROLE_CLIENT
exposed internal endpoints still guarded only by IS_AUTHENTICATED_FULLY (or no
security), reachable by a client. Verified by re-running a multi-role smoke
test (client -> 403, internal roles -> 200).

Security (closed real client-isolation holes):
- TaskDocumentDownloadController: add ownership check (admin all / client only
  own clientTicket docs / user only task-linked docs) — the custom download
  bypassed the cloistered provider.
- Share browse/download/search/status controllers: IS_AUTHENTICATED_FULLY ->
  ROLE_USER (SMB share is internal).
- User Get/GetCollection: add security ROLE_USER (was exposing the internal
  directory to clients).
- BookStackLink GetCollection/Post/Delete: IS_AUTHENTICATED_FULLY -> ROLE_USER.

Contracts / robustness:
- TaskInterface gains getProject(): ?ProjectInterface; TimeTracking export
  controller/service drop concrete cross-module entities for repo interfaces.
- Shared MCP Serializer signatures widened to the contracts (user/projectRef/
  taskRef/tags/users); project()/userFull()/etc. kept concrete (use getters
  outside the contracts).
- RecurrenceHandler: null-guard before findMaxNumberByProjectForUpdate().

180 tests green, cs-fixer clean, routes unchanged.
This commit is contained in:
Matthieu
2026-06-21 19:31:09 +02:00
parent da3d190216
commit 96ef1bf436
14 changed files with 84 additions and 27 deletions
@@ -81,8 +81,12 @@ final readonly class RecurrenceHandler
$newTask->setCalendarEventUid($savedEventUid);
// Generate task number in transaction
$this->entityManager->wrapInTransaction(function () use ($newTask): void {
$maxNumber = $this->taskRepository->findMaxNumberByProjectForUpdate($newTask->getProject());
$project = $newTask->getProject();
if (null === $project) {
return;
}
$this->entityManager->wrapInTransaction(function () use ($newTask, $project): void {
$maxNumber = $this->taskRepository->findMaxNumberByProjectForUpdate($project);
$newTask->setNumber($maxNumber + 1);
$this->entityManager->persist($newTask);
$this->entityManager->flush();