diff --git a/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php index d289a0131..ef9fd35aa 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php @@ -16,7 +16,6 @@ use Chill\DocStoreBundle\Dav\Response\DavResponse; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -43,7 +42,6 @@ final readonly class WebdavController private \Twig\Environment $engine, private StoredObjectManagerInterface $storedObjectManager, private Security $security, - private EntityManagerInterface $entityManager, ) { $this->requestAnalyzer = new PropfindRequestAnalyzer(); } @@ -194,20 +192,6 @@ final readonly class WebdavController return $response; } - #[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['PUT'])] - public function putDocument(StoredObject $storedObject, Request $request): Response - { - if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) { - throw new AccessDeniedHttpException(); - } - - $this->storedObjectManager->write($storedObject, $request->getContent()); - - $this->entityManager->flush(); - - return new DavResponse('', Response::HTTP_NO_CONTENT); - } - /** * @return array{0: array, 1: \DateTimeInterface, 2: string, 3: int} properties, lastModified, etag, length */ diff --git a/src/Bundle/ChillDocStoreBundle/Controller/WebdavPutController.php b/src/Bundle/ChillDocStoreBundle/Controller/WebdavPutController.php new file mode 100644 index 000000000..8333a0f85 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/WebdavPutController.php @@ -0,0 +1,71 @@ +security->getUser(); + + if (null !== $token = $this->lockTokenParser->parseIfCondition($request)) { + // this is a renew of a token. We first perform checks + if (true !== $error = $this->lockManager->checkLock($storedObject, $token, $user)) { + $e = match ($error) { + LockTokenCheckResultEnum::NO_LOCK_FOUND, LockTokenCheckResultEnum::LOCK_TOKEN_DO_NOT_MATCH => new PreconditionFailedHttpException(), + LockTokenCheckResultEnum::LOCK_TOKEN_DO_NOT_BELONG_TO_USER => new ConflictHttpException(), + }; + + throw $e; + } + } else { + if ($this->lockManager->hasLock($storedObject)) { + throw new ConflictHttpException(); + } + } + + if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + $this->storedObjectManager->write($storedObject, $request->getContent()); + + $this->entityManager->flush(); + + return new DavResponse('', Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavControllerTest.php index ec25ff5d6..c0a79f6a2 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavControllerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavControllerTest.php @@ -47,15 +47,11 @@ class WebdavControllerTest extends KernelTestCase $storedObjectManager = new MockedStoredObjectManager(); } - if (null === $entityManager) { - $entityManager = $this->createMock(EntityManagerInterface::class); - } - $security = $this->prophesize(Security::class); $security->isGranted(Argument::in(['SEE_AND_EDIT', 'SEE']), Argument::type(StoredObject::class)) ->willReturn(true); - return new WebdavController($this->engine, $storedObjectManager, $security->reveal(), $entityManager); + return new WebdavController($this->engine, $storedObjectManager, $security->reveal()); } private function buildDocument(): StoredObject @@ -152,6 +148,16 @@ class WebdavControllerTest extends KernelTestCase /dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d + + + + + + + + + + application/vnd.oasis.opendocument.text @@ -241,6 +247,16 @@ class WebdavControllerTest extends KernelTestCase /dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d + + + + + + + + + + Wed, 13 Sep 2023 14:15:00 +0200 @@ -267,6 +283,16 @@ class WebdavControllerTest extends KernelTestCase /dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d + + + + + + + + + + Wed, 13 Sep 2023 14:15:00 +0200 @@ -390,30 +416,6 @@ class WebdavControllerTest extends KernelTestCase self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type')); self::assertEquals(5, $response->headers->get('content-length')); } - - public function testPutDocument(): void - { - $document = $this->buildDocument(); - $entityManager = $this->createMock(EntityManagerInterface::class); - $storedObjectManager = $this->createMock(StoredObjectManagerInterface::class); - - // entity manager must be flushed - $entityManager->expects($this->once()) - ->method('flush'); - - // object must be written by StoredObjectManager - $storedObjectManager->expects($this->once()) - ->method('write') - ->with($this->identicalTo($document), $this->identicalTo('1234')); - - $controller = $this->buildController($entityManager, $storedObjectManager); - - $request = new Request(content: '1234'); - $response = $controller->putDocument($document, $request); - - self::assertEquals(204, $response->getStatusCode()); - self::assertEquals('', $response->getContent()); - } } class MockedStoredObjectManager implements StoredObjectManagerInterface diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavPutControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavPutControllerTest.php new file mode 100644 index 000000000..a58b7cabb --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavPutControllerTest.php @@ -0,0 +1,236 @@ +registerVersion(type: 'application/vnd.oasis.opendocument.text') + ->getStoredObject(); + + $reflectionObject = new \ReflectionClass($object); + $reflectionObjectUuid = $reflectionObject->getProperty('uuid'); + + $reflectionObjectUuid->setValue($object, Uuid::fromString('716e6688-4579-4938-acf3-c4ab5856803b')); + + return $object; + } + + private function buildController(EntityManagerInterface $entityManager, StoredObjectManagerInterface $storedObjectManager, LockTokenParser $lockTokenParser, StoredObjectLockManager $storedObjectLockManager, Security $security): WebdavPutController + { + return new WebdavPutController($storedObjectManager, $security, $entityManager, $lockTokenParser, $storedObjectLockManager); + } + + public function testPutDocumentHappyScenario(): void + { + $user = new User(); + $document = $this->buildDocument(); + $entityManager = $this->prophesize(EntityManagerInterface::class); + + // entity manager must be flushed + $entityManager->flush()->shouldBeCalled(); + + // object must be written by StoredObjectManager + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $storedObjectManager->write($document, '1234')->shouldBeCalled(); + + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + $security->isGranted(StoredObjectRoleEnum::EDIT->value, $document) + ->willReturn(true)->shouldBeCalled(); + + $storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class); + $storedObjectLockManager->checkLock($document, $token = 'opaquelocktoken:88f391da-2f72-11f1-bf99-67e47d3105b8', $user) + ->willReturn(true)->shouldBeCalled(); + $storedObjectLockManager->hasLock($document)->willReturn(true); + + + $controller = $this->buildController( + $entityManager->reveal(), + $storedObjectManager->reveal(), + new LockTokenParser(), + $storedObjectLockManager->reveal(), + $security->reveal() + ); + + $request = new Request( + content: '1234', + ); + $request->headers->set('If', '"(<'.$token.'>)"'); + $response = $controller->putDocument($document, $request); + + self::assertEquals(204, $response->getStatusCode()); + self::assertEquals('', $response->getContent()); + } + + public function testPutDocumentNoLockFound(): void + { + $user = new User(); + $document = $this->buildDocument(); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class); + $storedObjectLockManager->checkLock($document, $token = 'token', $user) + ->willReturn(\Chill\DocStoreBundle\Service\Lock\LockTokenCheckResultEnum::NO_LOCK_FOUND); + + $controller = $this->buildController( + $entityManager->reveal(), + $storedObjectManager->reveal(), + new LockTokenParser(), + $storedObjectLockManager->reveal(), + $security->reveal() + ); + + $request = new Request(); + $request->headers->set('If', '"(<'.$token.'>)"'); + + $this->expectException(\Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException::class); + $controller->putDocument($document, $request); + } + + public function testPutDocumentLockTokenDoNotMatch(): void + { + $user = new User(); + $document = $this->buildDocument(); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class); + $storedObjectLockManager->checkLock($document, $token = 'token', $user) + ->willReturn(\Chill\DocStoreBundle\Service\Lock\LockTokenCheckResultEnum::LOCK_TOKEN_DO_NOT_MATCH); + + $controller = $this->buildController( + $entityManager->reveal(), + $storedObjectManager->reveal(), + new LockTokenParser(), + $storedObjectLockManager->reveal(), + $security->reveal() + ); + + $request = new Request(); + $request->headers->set('If', '"(<'.$token.'>)"'); + + $this->expectException(\Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException::class); + $controller->putDocument($document, $request); + } + + public function testPutDocumentLockTokenDoNotBelongToUser(): void + { + $user = new User(); + $document = $this->buildDocument(); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class); + $storedObjectLockManager->checkLock($document, $token = 'token', $user) + ->willReturn(\Chill\DocStoreBundle\Service\Lock\LockTokenCheckResultEnum::LOCK_TOKEN_DO_NOT_BELONG_TO_USER); + + $controller = $this->buildController( + $entityManager->reveal(), + $storedObjectManager->reveal(), + new LockTokenParser(), + $storedObjectLockManager->reveal(), + $security->reveal() + ); + + $request = new Request(); + $request->headers->set('If', '"(<'.$token.'>)"'); + + $this->expectException(\Symfony\Component\HttpKernel\Exception\ConflictHttpException::class); + $controller->putDocument($document, $request); + } + + public function testPutDocumentNoLockGivenButTokenRequired(): void + { + $user = new User(); + $document = $this->buildDocument(); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class); + $storedObjectLockManager->hasLock($document)->willReturn(true); + + $controller = $this->buildController( + $entityManager->reveal(), + $storedObjectManager->reveal(), + new LockTokenParser(), + $storedObjectLockManager->reveal(), + $security->reveal() + ); + + $request = new Request(); + + $this->expectException(\Symfony\Component\HttpKernel\Exception\ConflictHttpException::class); + $controller->putDocument($document, $request); + } + + public function testPutDocumentNotGranted(): void + { + $user = new User(); + $document = $this->buildDocument(); + $entityManager = $this->prophesize(EntityManagerInterface::class); + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + $security->isGranted(StoredObjectRoleEnum::EDIT->value, $document) + ->willReturn(false); + + $storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class); + $storedObjectLockManager->hasLock($document)->willReturn(false); + + $controller = $this->buildController( + $entityManager->reveal(), + $storedObjectManager->reveal(), + new LockTokenParser(), + $storedObjectLockManager->reveal(), + $security->reveal() + ); + + $request = new Request(); + + $this->expectException(\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException::class); + $controller->putDocument($document, $request); + } +}