From b6edbb3eedbb369cb6332ff9384077a81a3fdd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 28 Aug 2024 23:19:24 +0200 Subject: [PATCH] Refactor StoredObject normalization handling Deprecate and remove specific context constants from StoredObjectNormalizer. Update object properties for better clarity and add permissions handling. Introduce related tests and adjust other files relying on the old context constants. --- .../Controller/AsyncUploadController.php | 2 +- .../StoredObjectDataTransformer.php | 7 +- .../Resources/public/types.ts | 40 +++++--- .../Normalizer/StoredObjectNormalizer.php | 29 ++++-- .../Normalizer/StoredObjectNormalizerTest.php | 98 +++++++++++++++++++ .../StoredObjectVersionNormalizerTest.php | 61 ++++++++++++ .../AccompanyingCourseWorkController.php | 3 +- 7 files changed, 211 insertions(+), 29 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectNormalizerTest.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectVersionNormalizerTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php b/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php index 879230cd9..52e0882a1 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php @@ -78,7 +78,7 @@ final readonly class AsyncUploadController if ($request->query->has('version')) { $filename = $request->query->get('version'); - $storedObjectVersion = $storedObject->getVersions()->findFirst(fn(int $index, StoredObjectVersion $version): bool => $version->getFilename() === $filename); + $storedObjectVersion = $storedObject->getVersions()->findFirst(fn (int $index, StoredObjectVersion $version): bool => $version->getFilename() === $filename); if (null === $storedObjectVersion) { // we are here in the case where the version is not stored into the database diff --git a/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php b/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php index e06d8d7cc..b5c7c3930 100644 --- a/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php +++ b/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Form\DataTransformer; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Serializer\SerializerInterface; @@ -30,11 +29,7 @@ class StoredObjectDataTransformer implements DataTransformerInterface } if ($value instanceof StoredObject) { - return $this->serializer->serialize($value, 'json', [ - 'groups' => [ - StoredObjectNormalizer::ADD_DAV_EDIT_LINK_CONTEXT, - ], - ]); + return $this->serializer->serialize($value, 'json'); } throw new UnexpectedTypeException($value, StoredObject::class); diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts index 1d21feacd..235b375ce 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -1,28 +1,44 @@ -import {DateTime} from "../../../ChillMainBundle/Resources/public/types"; +import {DateTime, User} from "../../../ChillMainBundle/Resources/public/types"; export type StoredObjectStatus = "empty"|"ready"|"failure"|"pending"; export interface StoredObject { id: number, - - /** - * filename of the object in the object storage - */ - filename: string, - creationDate: DateTime, - datas: object, - iv: number[], - keyInfos: object, title: string, - type: string, uuid: string, + prefix: string, status: StoredObjectStatus, + currentVersion: null|StoredObjectVersion, + totalVersions: number, + datas: object, + /** @deprecated */ + creationDate: DateTime, + createdAt: DateTime|null, + createdBy: User|null, + _permissions: { + canEdit: boolean, + canSee: boolean, + }, _links?: { dav_link?: { href: string expiration: number }, - } + }, +} + +export interface StoredObjectVersion { + /** + * filename of the object in the object storage + */ + filename: string, + version: number, + id: number, + iv: number[], + keyInfos: object, + type: string, + createdAt: DateTime|null, + createdBy: User|null, } export interface StoredObjectCreated { diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php index ebd7d3564..dbd9d72b8 100644 --- a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php @@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -28,8 +29,16 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface; final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; - public const ADD_DAV_SEE_LINK_CONTEXT = 'dav-see-link-context'; - public const ADD_DAV_EDIT_LINK_CONTEXT = 'dav-edit-link-context'; + + /** + * @deprecated + */ + public const string ADD_DAV_SEE_LINK_CONTEXT = 'dav-see-link-context'; + + /** + * @deprecated + */ + public const string ADD_DAV_EDIT_LINK_CONTEXT = 'dav-edit-link-context'; public function __construct( private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider, @@ -41,17 +50,16 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa { /** @var StoredObject $object */ $datas = [ - 'datas' => $object->getDatas(), - 'filename' => $object->getFilename(), 'id' => $object->getId(), - 'iv' => $object->getIv(), - 'keyInfos' => $object->getKeyInfos(), + 'datas' => $object->getDatas(), + 'prefix' => $object->getPrefix(), 'title' => $object->getTitle(), - 'type' => $object->getType(), - 'uuid' => $object->getUuid(), + 'uuid' => $object->getUuid()->toString(), 'status' => $object->getStatus(), 'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context), 'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context), + 'currentVersion' => $this->normalizer->normalize($object->getCurrentVersion(), $format, [...$context, [AbstractNormalizer::GROUPS => 'read']]), + 'totalVersions' => $object->getVersions()->count(), ]; // deprecated property @@ -60,6 +68,11 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa $canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object); $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object); + $datas['_permissions'] = [ + 'canEdit' => $canEdit, + 'canSee' => $canSee, + ]; + if ($canSee || $canEdit) { $accessToken = $this->JWTDavTokenProvider->createToken( $object, diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectNormalizerTest.php new file mode 100644 index 000000000..a2cc75541 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectNormalizerTest.php @@ -0,0 +1,98 @@ +setTitle('test'); + $reflection = new \ReflectionClass(StoredObject::class); + $idProperty = $reflection->getProperty('id'); + $idProperty->setValue($storedObject, 1); + + $jwtProvider = $this->createMock(JWTDavTokenProviderInterface::class); + $jwtProvider->expects($this->once())->method('createToken')->withAnyParameters()->willReturn('token'); + $jwtProvider->expects($this->once())->method('getTokenExpiration')->with('token')->willReturn($d = new \DateTimeImmutable()); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator->expects($this->once())->method('generate') + ->with( + 'chill_docstore_dav_document_get', + [ + 'uuid' => $storedObject->getUuid(), + 'access_token' => 'token', + ], + UrlGeneratorInterface::ABSOLUTE_URL, + ) + ->willReturn($davLink = 'http://localhost/dav/token'); + + $security = $this->createMock(Security::class); + $security->expects($this->exactly(2))->method('isGranted') + ->with( + $this->logicalOr(StoredObjectRoleEnum::EDIT->value, StoredObjectRoleEnum::SEE->value), + $storedObject + ) + ->willReturn(true); + + $globalNormalizer = $this->createMock(NormalizerInterface::class); + $globalNormalizer->expects($this->exactly(3))->method('normalize') + ->withAnyParameters() + ->willReturnCallback(function (?object $object, string $format, array $context) { + if (null === $object) { + return null; + } + + return ['sub' => 'sub']; + }); + + $normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security); + $normalizer->setNormalizer($globalNormalizer); + + $actual = $normalizer->normalize($storedObject, 'json'); + + self::assertArrayHasKey('id', $actual); + self::assertEquals(1, $actual['id']); + self::assertArrayHasKey('title', $actual); + self::assertEquals('test', $actual['title']); + self::assertArrayHasKey('uuid', $actual); + self::assertArrayHasKey('prefix', $actual); + self::assertArrayHaskey('status', $actual); + self::assertArrayHasKey('currentVersion', $actual); + self::assertEquals(null, $actual['currentVersion']); + self::assertArrayHasKey('totalVersions', $actual); + self::assertEquals(0, $actual['totalVersions']); + self::assertArrayHasKey('datas', $actual); + self::assertArrayHasKey('createdAt', $actual); + self::assertArrayHasKey('createdBy', $actual); + self::assertArrayHasKey('_permissions', $actual); + self::assertEqualsCanonicalizing(['canEdit' => true, 'canSee' => true], $actual['_permissions']); + self::assertArrayHaskey('_links', $actual); + self::assertArrayHasKey('dav_link', $actual['_links']); + self::assertEqualsCanonicalizing(['href' => $davLink, 'expiration' => $d->getTimestamp()], $actual['_links']['dav_link']); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectVersionNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectVersionNormalizerTest.php new file mode 100644 index 000000000..c6bd87686 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectVersionNormalizerTest.php @@ -0,0 +1,61 @@ +normalizer = self::getContainer()->get(NormalizerInterface::class); + } + + public function testNormalize(): void + { + $storedObject = new StoredObject(); + $version = $storedObject->registerVersion( + iv: [1, 2, 3, 4], + keyInfos: ['someKey' => 'someKey'], + type: 'text/text', + ); + $reflection = new \ReflectionClass($version); + $idProperty = $reflection->getProperty('id'); + $idProperty->setValue($version, 1); + + $actual = $this->normalizer->normalize($version, 'json', ['groups' => ['read']]); + + self::assertEqualsCanonicalizing( + [ + 'id' => 1, + 'version' => 0, + 'filename' => $version->getFilename(), + 'iv' => [1, 2, 3, 4], + 'keyInfos' => ['someKey' => 'someKey'], + 'type' => 'text/text', + 'createdAt' => null, + 'createdBy' => null, + ], + $actual + ); + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php index 0243d55b5..c43dbd128 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; -use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Templating\Listing\FilterOrderHelper; use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; @@ -116,7 +115,7 @@ final class AccompanyingCourseWorkController extends AbstractController { $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::UPDATE, $work); - $json = $this->serializer->normalize($work, 'json', ['groups' => ['read', StoredObjectNormalizer::ADD_DAV_EDIT_LINK_CONTEXT]]); + $json = $this->serializer->normalize($work, 'json', ['groups' => ['read']]); return $this->render('@ChillPerson/AccompanyingCourseWork/edit.html.twig', [ 'accompanyingCourse' => $work->getAccompanyingPeriod(),