mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Dav: implements JWT extraction from the URL, and add the access_token in dav urls
This commit is contained in:
parent
146e0090fb
commit
6f6683f549
@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer;
|
||||
use Chill\DocStoreBundle\Dav\Response\DavResponse;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use DateTimeInterface;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@ -30,41 +31,44 @@ final readonly class WebdavController
|
||||
public function __construct(
|
||||
private \Twig\Environment $engine,
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private Security $security,
|
||||
private ?JWTTokenManagerInterface $JWTTokenManager = null,
|
||||
) {
|
||||
$this->requestAnalyzer = new PropfindRequestAnalyzer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/open/{uuid}")
|
||||
* @Route("/chdoc/open/{uuid}")
|
||||
*/
|
||||
public function open(StoredObject $storedObject): Response
|
||||
{
|
||||
/*$accessToken = $this->JWTTokenManager->createFromPayload($this->security->getUser(), [
|
||||
$accessToken = $this->JWTTokenManager?->createFromPayload($this->security->getUser(), [
|
||||
'UserCanWrite' => true,
|
||||
'UserCanAttend' => true,
|
||||
'UserCanPresent' => true,
|
||||
'fileId' => $storedObject->getUuid(),
|
||||
]);*/
|
||||
]);
|
||||
|
||||
return new DavResponse($this->engine->render('@ChillDocStore/Webdav/open_in_browser.html.twig', [
|
||||
'stored_object' => $storedObject, 'access_token' => '',
|
||||
'stored_object' => $storedObject, 'access_token' => $accessToken,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get")
|
||||
* @Route("/dav/{access_token}/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get")
|
||||
*/
|
||||
public function getDirectory(StoredObject $storedObject): Response
|
||||
public function getDirectory(StoredObject $storedObject, string $access_token): Response
|
||||
{
|
||||
return new DavResponse(
|
||||
$this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [
|
||||
'stored_object' => $storedObject
|
||||
'stored_object' => $storedObject,
|
||||
'access_token' => $access_token,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/get/{uuid}/", methods={"OPTIONS"})
|
||||
* @Route("/dav/{access_token}/get/{uuid}/", methods={"OPTIONS"})
|
||||
*/
|
||||
public function optionsDirectory(StoredObject $storedObject): Response
|
||||
{
|
||||
@ -78,9 +82,9 @@ final readonly class WebdavController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/get/{uuid}/", methods={"PROPFIND"})
|
||||
* @Route("/dav/{access_token}/get/{uuid}/", methods={"PROPFIND"})
|
||||
*/
|
||||
public function propfindDirectory(StoredObject $storedObject, Request $request): Response
|
||||
public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response
|
||||
{
|
||||
$depth = $request->headers->get('depth');
|
||||
|
||||
@ -111,10 +115,11 @@ final readonly class WebdavController
|
||||
$this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [
|
||||
'stored_object' => $storedObject,
|
||||
'properties' => $properties,
|
||||
'last_modified' => $lastModified ?? null,
|
||||
'etag' => $etag ?? null,
|
||||
'content_length' => $length ?? null,
|
||||
'depth' => (int) $depth
|
||||
'last_modified' => $lastModified ,
|
||||
'etag' => $etag,
|
||||
'content_length' => $length,
|
||||
'depth' => (int) $depth,
|
||||
'access_token' => $access_token,
|
||||
]),
|
||||
207
|
||||
);
|
||||
@ -127,7 +132,7 @@ final readonly class WebdavController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"})
|
||||
* @Route("/dav/{access_token}/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"})
|
||||
*/
|
||||
public function getDocument(StoredObject $storedObject): Response
|
||||
{
|
||||
@ -136,7 +141,7 @@ final readonly class WebdavController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/get/{uuid}/d", methods={"HEAD"})
|
||||
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"HEAD"})
|
||||
*/
|
||||
public function headDocument(StoredObject $storedObject): Response
|
||||
{
|
||||
@ -154,7 +159,7 @@ final readonly class WebdavController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/get/{uuid}/d", methods={"OPTIONS"})
|
||||
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"OPTIONS"})
|
||||
*/
|
||||
public function optionsDocument(StoredObject $storedObject): Response
|
||||
{
|
||||
@ -176,9 +181,9 @@ final readonly class WebdavController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/get/{uuid}/d", methods={"PROPFIND"})
|
||||
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"PROPFIND"})
|
||||
*/
|
||||
public function propfindDocument(StoredObject $storedObject, Request $request): Response
|
||||
public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response
|
||||
{
|
||||
$content = $request->getContent();
|
||||
$xml = new \DOMDocument();
|
||||
@ -204,9 +209,10 @@ final readonly class WebdavController
|
||||
[
|
||||
'stored_object' => $storedObject,
|
||||
'properties' => $properties,
|
||||
'etag' => $etag ?? null,
|
||||
'last_modified' => $lastModified ?? null,
|
||||
'content_length' => $length ?? null,
|
||||
'etag' => $etag,
|
||||
'last_modified' => $lastModified,
|
||||
'content_length' => $length,
|
||||
'access_token' => $access_token,
|
||||
]
|
||||
),
|
||||
207
|
||||
@ -221,7 +227,7 @@ final readonly class WebdavController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dav/get/{uuid}/d", methods={"PUT"})
|
||||
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"PUT"})
|
||||
*/
|
||||
public function putDocument(StoredObject $storedObject, Request $request): Response
|
||||
{
|
||||
|
@ -6,7 +6,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li><a href="{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}">d</a></li>
|
||||
<li><a href="{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}">d</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>{{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid } ) }}</d:href>
|
||||
<d:href>{{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid, 'access_token': access_token } ) }}</d:href>
|
||||
{% if properties.resourceType or properties.contentType %}
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
@ -28,7 +28,7 @@
|
||||
</d:response>
|
||||
{% if depth == 1 %}
|
||||
<d:response>
|
||||
<d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }}</d:href>
|
||||
<d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token':access_token}) }}</d:href>
|
||||
{% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %}
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }}</d:href>
|
||||
<d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token}) }}</d:href>
|
||||
{% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %}
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
|
@ -2,6 +2,6 @@
|
||||
|
||||
{% block content %}
|
||||
<p>document uuid: {{ stored_object.uuid }}</p>
|
||||
<p>{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}</p>
|
||||
<a href="vnd.libreoffice.command:ofe|u|{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}">Open document</a>
|
||||
<p>{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}</p>
|
||||
<a href="vnd.libreoffice.command:ofe|u|{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}">Open document</a>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,49 @@
|
||||
<?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\Security\Guard;
|
||||
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final readonly class DavOnUrlTokenExtractor implements TokenExtractorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function extract(Request $request): string|false
|
||||
{
|
||||
$uri = $request->getRequestUri();
|
||||
|
||||
$segments = array_values(
|
||||
array_filter(
|
||||
explode('/', $uri),
|
||||
fn ($item) => '' !== trim($item)
|
||||
)
|
||||
);
|
||||
|
||||
if (2 > count($segments)) {
|
||||
$this->logger->info("not enough segment for parsing URL");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('dav' !== $segments[0]) {
|
||||
$this->logger->info("the first segment of the url must be DAV");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $segments[1];
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?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\Security\Guard;
|
||||
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class JWTOnDavUrlAuthenticator extends JWTTokenAuthenticator
|
||||
{
|
||||
public function __construct(
|
||||
JWTTokenManagerInterface $jwtManager,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
TokenExtractorInterface $tokenExtractor,
|
||||
TokenStorageInterface $preAuthenticationTokenStorage,
|
||||
TranslatorInterface $translator = null,
|
||||
private readonly DavOnUrlTokenExtractor $davOnUrlTokenExtractor,
|
||||
) {
|
||||
parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $preAuthenticationTokenStorage, $translator);
|
||||
}
|
||||
|
||||
protected function getTokenExtractor()
|
||||
{
|
||||
return $this->davOnUrlTokenExtractor;
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ 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;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
/**
|
||||
@ -43,8 +44,9 @@ class WebdavControllerTest extends KernelTestCase
|
||||
private function buildController(): WebdavController
|
||||
{
|
||||
$storedObjectManager = new MockedStoredObjectManager();
|
||||
$security = $this->prophesize(Security::class);
|
||||
|
||||
return new WebdavController($this->engine, $storedObjectManager);
|
||||
return new WebdavController($this->engine, $storedObjectManager, $security->reveal());
|
||||
}
|
||||
|
||||
private function buildDocument(): StoredObject
|
||||
@ -115,7 +117,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
|
||||
$request = new Request([], [], [], [], [], [], $requestContent);
|
||||
$request->setMethod('PROPFIND');
|
||||
$response = $controller->propfindDocument($this->buildDocument(), $request);
|
||||
$response = $controller->propfindDocument($this->buildDocument(), '1234', $request);
|
||||
|
||||
self::assertEquals($expectedStatusCode, $response->getStatusCode());
|
||||
self::assertContains('content-type', $response->headers->keys());
|
||||
@ -134,7 +136,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
$request = new Request([], [], [], [], [], [], $requestContent);
|
||||
$request->setMethod('PROPFIND');
|
||||
$request->headers->add(["Depth" => "0"]);
|
||||
$response = $controller->propfindDirectory($this->buildDocument(), $request);
|
||||
$response = $controller->propfindDirectory($this->buildDocument(), '1234', $request);
|
||||
|
||||
self::assertEquals($expectedStatusCode, $response->getStatusCode());
|
||||
self::assertContains('content-type', $response->headers->keys());
|
||||
@ -170,7 +172,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:multistatus xmlns:d="DAV:" >
|
||||
<d:response>
|
||||
<d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:resourcetype/>
|
||||
@ -205,7 +207,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:propstat>
|
||||
<d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
|
||||
<ns0:IsReadOnly/>
|
||||
@ -232,7 +234,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:propstat>
|
||||
<d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
|
||||
<ns0:BaseURI/>
|
||||
@ -259,7 +261,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<!-- the date scraped from a webserver is >Sun, 10 Sep 2023 14:10:23 GMT -->
|
||||
@ -285,7 +287,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:resourcetype/>
|
||||
@ -323,7 +325,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:resourcetype><d:collection/></d:resourcetype>
|
||||
@ -365,7 +367,7 @@ class WebdavControllerTest extends KernelTestCase
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
|
||||
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
|
||||
<d:propstat>
|
||||
<d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/" >
|
||||
<ns0:CreatableContentsInfo/>
|
||||
|
@ -0,0 +1,54 @@
|
||||
<?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\Security\Guard;
|
||||
|
||||
use Chill\DocStoreBundle\Security\Guard\DavOnUrlTokenExtractor;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class DavOnUrlTokenExtractorTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataUri
|
||||
*/
|
||||
public function testExtract(string $uri, string|false $expected): void
|
||||
{
|
||||
$request = $this->prophesize(Request::class);
|
||||
$request->getRequestUri()->willReturn($uri);
|
||||
|
||||
$extractor = new DavOnUrlTokenExtractor(new NullLogger());
|
||||
|
||||
$actual = $extractor->extract($request->reveal());
|
||||
|
||||
self::assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-pure
|
||||
*/
|
||||
public static function provideDataUri(): iterable
|
||||
{
|
||||
yield ['/dav/123456789/get/d07d2230-5326-11ee-8fd4-93696acf5ea1/d', '123456789'];
|
||||
yield ['/dav/123456789', '123456789'];
|
||||
yield ['/not-dav/123456978', false];
|
||||
yield ['/dav', false];
|
||||
yield ['/', false];
|
||||
}
|
||||
}
|
@ -34,6 +34,11 @@ services:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
Chill\DocStoreBundle\Security\:
|
||||
resource: './../Security'
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
Chill\DocStoreBundle\Serializer\Normalizer\:
|
||||
autowire: true
|
||||
resource: '../Serializer/Normalizer/'
|
||||
|
Loading…
x
Reference in New Issue
Block a user