mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-04-09 06:23:45 +00:00
Extract WebDAV PUT logic into a dedicated WebdavPutController and add comprehensive tests
- Moved `putDocument` method from `WebdavController` to the new `WebdavPutController`. - Introduced stricter locking and permission checks during PUT operations. - Added new test suite `WebdavPutControllerTest` to cover different scenarios, including lock validation and access control. - Updated existing logic in `WebdavControllerTest` to reflect these changes.
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Dav\Response\DavResponse;
|
||||
use Chill\DocStoreBundle\Dav\Utils\LockTokenParser;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\Lock\LockTokenCheckResultEnum;
|
||||
use Chill\DocStoreBundle\Service\Lock\StoredObjectLockManager;
|
||||
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;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class WebdavPutController
|
||||
{
|
||||
public function __construct(
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private Security $security,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LockTokenParser $lockTokenParser,
|
||||
private StoredObjectLockManager $lockManager,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['PUT'])]
|
||||
public function putDocument(StoredObject $storedObject, Request $request): Response
|
||||
{
|
||||
$user = $this->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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:supportedlock>
|
||||
<d:lockentry>
|
||||
<d:lockscope>
|
||||
<d:exclusive/>
|
||||
</d:lockscope>
|
||||
<d:locktype>
|
||||
<d:write/>
|
||||
</d:locktype>
|
||||
</d:lockentry>
|
||||
</d:supportedlock>
|
||||
<d:resourcetype/>
|
||||
<d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype>
|
||||
</d:prop>
|
||||
@@ -241,6 +247,16 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:supportedlock>
|
||||
<d:lockentry>
|
||||
<d:lockscope>
|
||||
<d:exclusive/>
|
||||
</d:lockscope>
|
||||
<d:locktype>
|
||||
<d:write/>
|
||||
</d:locktype>
|
||||
</d:lockentry>
|
||||
</d:supportedlock>
|
||||
<!-- the date scraped from a webserver is >Sun, 10 Sep 2023 14:10:23 GMT -->
|
||||
<d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified>
|
||||
</d:prop>
|
||||
@@ -267,6 +283,16 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:supportedlock>
|
||||
<d:lockentry>
|
||||
<d:lockscope>
|
||||
<d:exclusive/>
|
||||
</d:lockscope>
|
||||
<d:locktype>
|
||||
<d:write/>
|
||||
</d:locktype>
|
||||
</d:lockentry>
|
||||
</d:supportedlock>
|
||||
<d:resourcetype/>
|
||||
<d:creationdate/>
|
||||
<d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified>
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Controller\WebdavPutController;
|
||||
use Chill\DocStoreBundle\Dav\Utils\LockTokenParser;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\Lock\StoredObjectLockManager;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class WebdavPutControllerTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private function buildDocument(): StoredObject
|
||||
{
|
||||
$object = (new StoredObject())
|
||||
->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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user