From 64f6fc530615ff82a45d166d0816f8dbc36f0105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 2 Mar 2026 17:31:19 +0100 Subject: [PATCH] Integrate audit functionality across multiple controllers and services for WebDAV, WOPI, and Async Upload - Added `TriggerAuditInterface` to `WebdavController`, `ConvertController`, and `AsyncUploadController`, with audit triggers for create, update, view, and convert actions using translatable messages. - Enhanced `ChillWopi` service with audit triggers for file operations (view, update) and integrated French translations for audit labels. - Updated `messages.fr.yml` to include new translatable keys for audit actions in both WOPI and WebDAV contexts. - Registered `TriggerAuditInterface` as a dependency in modified classes to enable audit tracking functionality. --- .../Controller/AsyncUploadController.php | 18 +++++++++ .../Controller/WebdavController.php | 18 +++++++++ .../translations/messages.fr.yml | 5 +++ .../src/Controller/ConvertController.php | 10 +++++ .../src/Service/Wopi/ChillWopi.php | 39 +++++++++++++++++-- .../src/translations/messages.fr.yml | 6 +++ 6 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php b/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php index cdbb94d29..22cc03c9c 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php @@ -15,6 +15,8 @@ use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObjectVersion; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; +use Chill\MainBundle\Audit\TriggerAuditInterface; +use Chill\MainBundle\Entity\AuditTrail; use Chill\MainBundle\Entity\User; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -25,6 +27,7 @@ use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\Translation\TranslatableMessage; final readonly class AsyncUploadController { @@ -33,6 +36,7 @@ final readonly class AsyncUploadController private SerializerInterface $serializer, private Security $security, private LoggerInterface $chillLogger, + private TriggerAuditInterface $triggerAudit, ) {} #[Route(path: '/api/1.0/doc-store/async-upload/temp_url/{uuid}/generate/post', name: 'chill_docstore_asyncupload_getsignedurlpost')] @@ -56,6 +60,13 @@ final readonly class AsyncUploadController 'doc_uuid' => $storedObject->getUuid(), ]); + $this->triggerAudit->triggerAudit( + AuditTrail::AUDIT_VIEW, + $storedObject, + description: new TranslatableMessage('audit.stored_object.write_by_ui'), + metadata: ['so_write' => 'signed_url'], + ); + return new JsonResponse( $this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]), Response::HTTP_OK, @@ -111,6 +122,13 @@ final readonly class AsyncUploadController 'user_id' => $userId, ]); + $this->triggerAudit->triggerAudit( + AuditTrail::AUDIT_VIEW, + $storedObject, + description: new TranslatableMessage('audit.stored_object.read_by_ui'), + metadata: ['so_read' => 'signed_url'], + ); + return new JsonResponse( $this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]), Response::HTTP_OK, diff --git a/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php index 022d544cb..9c6f21a0e 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php @@ -16,6 +16,8 @@ use Chill\DocStoreBundle\Dav\Response\DavResponse; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; +use Chill\MainBundle\Audit\TriggerAuditInterface; +use Chill\MainBundle\Entity\AuditTrail; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -23,6 +25,7 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Translation\TranslatableMessage; /** * Provide endpoint for editing a document on the desktop using dav. @@ -44,6 +47,7 @@ final readonly class WebdavController private StoredObjectManagerInterface $storedObjectManager, private Security $security, private EntityManagerInterface $entityManager, + private TriggerAuditInterface $triggerAudit, ) { $this->requestAnalyzer = new PropfindRequestAnalyzer(); } @@ -122,6 +126,13 @@ final readonly class WebdavController throw new AccessDeniedHttpException(); } + $this->triggerAudit->triggerAudit( + AuditTrail::AUDIT_VIEW, + $storedObject, + description: new TranslatableMessage('audit.stored_object_webdav.read_by_webdav'), + metadata: ['so_read' => 'webdav'], + ); + return (new DavResponse($this->storedObjectManager->read($storedObject))) ->setEtag($this->storedObjectManager->etag($storedObject)); } @@ -205,6 +216,13 @@ final readonly class WebdavController $this->entityManager->flush(); + $this->triggerAudit->triggerAudit( + AuditTrail::AUDIT_UPDATE, + $storedObject, + description: new TranslatableMessage('audit.stored_object_webdav.update_by_webdav'), + metadata: ['so_update' => 'webdav'], + ); + return new DavResponse('', Response::HTTP_NO_CONTENT); } diff --git a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml index ad3b2906d..fe6a3555f 100644 --- a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml @@ -133,8 +133,13 @@ audit: stored_object: display: Document n°{id} display_with_title: Document n°{id} - {title} + read_by_ui: Téléchargement par l'interface web + write_by_ui: Écriture par l'interface web generic-doc: list_for_accompanying_period: Liste des documents d'un parcours d'accompagnement list_for_person: Liste des documents de l'usager accompanying_course_document: display: Document d'un parcours d'accompagnement n°{id} + stored_object_webdav: + read_by_webdav: Lecture par WebDAV + update_by_webdav: Mise à jour par WebDAV diff --git a/src/Bundle/ChillWopiBundle/src/Controller/ConvertController.php b/src/Bundle/ChillWopiBundle/src/Controller/ConvertController.php index 247abfa13..eb799d56c 100644 --- a/src/Bundle/ChillWopiBundle/src/Controller/ConvertController.php +++ b/src/Bundle/ChillWopiBundle/src/Controller/ConvertController.php @@ -14,12 +14,15 @@ namespace Chill\WopiBundle\Controller; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; +use Chill\MainBundle\Audit\TriggerAuditInterface; +use Chill\MainBundle\Entity\AuditTrail; use Chill\WopiBundle\Service\WopiConverter; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Translation\TranslatableMessage; class ConvertController { @@ -30,6 +33,7 @@ class ConvertController private readonly StoredObjectManagerInterface $storedObjectManager, private readonly WopiConverter $wopiConverter, private readonly LoggerInterface $logger, + private readonly TriggerAuditInterface $triggerAudit, ) {} public function __invoke(StoredObject $storedObject, Request $request): Response @@ -51,6 +55,12 @@ class ConvertController $lang = $request->getLocale(); try { + $this->triggerAudit->triggerAudit( + AuditTrail::AUDIT_VIEW, + $storedObject, + description: new TranslatableMessage('audit.stored_object_wopi.convert_to_pdf') + ); + return new Response($this->wopiConverter->convert($lang, $content, $storedObject->getType()), Response::HTTP_OK, [ 'Content-Type' => 'application/pdf', ]); diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php index 7f68ac0ea..3f9cd14df 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php @@ -12,21 +12,41 @@ declare(strict_types=1); namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Service\WopiInterface; +use Chill\MainBundle\Audit\TriggerAuditInterface; +use Chill\MainBundle\Entity\AuditTrail; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\Translation\TranslatableMessage; final readonly class ChillWopi implements WopiInterface { - public function __construct(private WopiInterface $wopi) {} + public function __construct( + private WopiInterface $wopi, + private TriggerAuditInterface $triggerAudit, + private ChillDocumentManager $documentManager, + ) {} public function checkFileInfo( string $fileId, ?string $accessToken, RequestInterface $request, ): ResponseInterface { - return $this->wopi->checkFileInfo($fileId, $accessToken, $request, [ + $response = $this->wopi->checkFileInfo($fileId, $accessToken, $request, [ 'SupportsRename' => false, ]); + + $document = $this->documentManager->findByDocumentId($fileId); + + $this->triggerAudit->triggerAudit( + AuditTrail::AUDIT_VIEW, + $document, + description: new TranslatableMessage( + 'audit.stored_object_wopi.view_through_wopi' + ), + metadata: ['so_read' => 'wopi'], + ); + + return $response; } public function deleteFile(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface @@ -76,7 +96,20 @@ final readonly class ChillWopi implements WopiInterface string $xWopiEditors, RequestInterface $request, ): ResponseInterface { - return $this->wopi->putFile($fileId, $accessToken, $xWopiLock, $xWopiEditors, $request); + $response = $this->wopi->putFile($fileId, $accessToken, $xWopiLock, $xWopiEditors, $request); + + $document = $this->documentManager->findByDocumentId($fileId); + + $this->triggerAudit->triggerAudit( + AuditTrail::AUDIT_UPDATE, + $document, + description: new TranslatableMessage( + 'audit.stored_object_wopi.update_through_wopi' + ), + metadata: ['so_update' => 'wopi'] + ); + + return $response; } public function putRelativeFile(string $fileId, string $accessToken, ?string $suggestedTarget, ?string $relativeTarget, bool $overwriteRelativeTarget, int $size, RequestInterface $request): ResponseInterface diff --git a/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml b/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml index 2875550a1..78c7b2012 100644 --- a/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml +++ b/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml @@ -1,2 +1,8 @@ wopi_editor: document unsupported for edition: Ce format de document n'est pas éditable + +audit: + stored_object_wopi: + convert_to_pdf: 'Conversion du document en PDF' + update_through_wopi: 'Mise à jour du document par WOPI' + view_through_wopi: 'Visualisation du document par WOPI'