mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-08 08:49:52 +00:00
Add TempUrl signature validation to local storage
Implemented local storage-based file handling with TempUrl signature validation for upload and retrieval. Added validation checks for parameters like max file size/count, expiration, and signature integrity. Included unit tests for TempUrl signature validation and adjusted configuration for local storage.
This commit is contained in:
parent
0c628c39db
commit
999ac3af2b
@ -1,6 +1,8 @@
|
|||||||
chill_doc_store:
|
chill_doc_store:
|
||||||
openstack:
|
local_storage:
|
||||||
temp_url:
|
storage_path: '%kernel.project_dir%/var/storage'
|
||||||
temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
|
# openstack:
|
||||||
container: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' # Required
|
# temp_url:
|
||||||
temp_url_base_path: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required
|
# temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
|
||||||
|
# container: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' # Required
|
||||||
|
# temp_url_base_path: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required
|
||||||
|
@ -33,11 +33,11 @@ class TempUrlLocalStorageGenerator implements TempUrlGeneratorInterface
|
|||||||
$expiration = $this->clock->now()->getTimestamp() + min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION);
|
$expiration = $this->clock->now()->getTimestamp() + min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION);
|
||||||
|
|
||||||
return new SignedUrl(
|
return new SignedUrl(
|
||||||
$method,
|
strtoupper($method),
|
||||||
$this->urlGenerator->generate('chill_docstore_stored_object_operate', [
|
$this->urlGenerator->generate('chill_docstore_stored_object_operate', [
|
||||||
'object_name' => $object_name,
|
'object_name' => $object_name,
|
||||||
'exp' => $expiration,
|
'exp' => $expiration,
|
||||||
'sig' => $this->sign($method, $object_name, $expiration),
|
'sig' => $this->sign(strtoupper($method), $object_name, $expiration),
|
||||||
], UrlGeneratorInterface::ABSOLUTE_URL),
|
], UrlGeneratorInterface::ABSOLUTE_URL),
|
||||||
\DateTimeImmutable::createFromFormat('U', (string) $expiration),
|
\DateTimeImmutable::createFromFormat('U', (string) $expiration),
|
||||||
$object_name,
|
$object_name,
|
||||||
@ -70,6 +70,38 @@ class TempUrlLocalStorageGenerator implements TempUrlGeneratorInterface
|
|||||||
|
|
||||||
private function sign(string $method, string $object_name, int $expiration): string
|
private function sign(string $method, string $object_name, int $expiration): string
|
||||||
{
|
{
|
||||||
return hash_hmac('sha512', $method, sprintf('%s.%s.%s.%d', $method, $this->secret, $object_name, $expiration));
|
return hash('sha512', sprintf('%s.%s.%s.%d', $method, $this->secret, $object_name, $expiration));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateSignaturePost(string $signature, string $prefix, int $expiration, int $maxFileSize, int $maxFileCount): bool
|
||||||
|
{
|
||||||
|
if (15_000_000 !== $maxFileSize || 1 !== $maxFileCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->internalValidateSignature($signature, 'POST', $prefix, $expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function internalValidateSignature(string $signature, string $method, string $object_name, int $expiration): bool
|
||||||
|
{
|
||||||
|
if ($expiration < $this->clock->now()->format('U')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $object_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return $this->sign($method, $object_name, $expiration) === $signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateSignature(string $signature, string $method, string $objectName, int $expiration): bool
|
||||||
|
{
|
||||||
|
if (!in_array($method, ['GET', 'HEAD'], true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->internalValidateSignature($signature, $method, $objectName, $expiration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,11 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Controller;
|
namespace Chill\DocStoreBundle\Controller;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\StoredObjectManager;
|
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\StoredObjectManager;
|
||||||
|
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\TempUrlLocalStorageGenerator;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@ -26,6 +28,7 @@ final readonly class StoredObjectContentToLocalStorageController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private StoredObjectManager $storedObjectManager,
|
private StoredObjectManager $storedObjectManager,
|
||||||
|
private TempUrlLocalStorageGenerator $tempUrlLocalStorageGenerator,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/public/stored-object/post', name: 'chill_docstore_storedobject_post', methods: ['POST'])]
|
#[Route('/public/stored-object/post', name: 'chill_docstore_storedobject_post', methods: ['POST'])]
|
||||||
@ -34,14 +37,51 @@ final readonly class StoredObjectContentToLocalStorageController
|
|||||||
$prefix = $request->query->get('prefix', '');
|
$prefix = $request->query->get('prefix', '');
|
||||||
|
|
||||||
if ('' === $prefix) {
|
if ('' === $prefix) {
|
||||||
throw new BadRequestHttpException('prefix parameter is missing');
|
throw new BadRequestHttpException('Prefix parameter is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === $maxFileSize = $request->request->getInt('max_file_size', 0)) {
|
||||||
|
throw new BadRequestHttpException('Max file size is not set or equal to zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 !== $maxFileCount = $request->request->getInt('max_file_count', 0)) {
|
||||||
|
throw new BadRequestHttpException('Max file count is not set or equal to zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === $expiration = $request->request->getInt('expires', 0)) {
|
||||||
|
throw new BadRequestHttpException('Expiration is not set or equal to zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $signature = $request->request->get('signature', '')) {
|
||||||
|
throw new BadRequestHttpException('Signature is not set or is a blank string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->tempUrlLocalStorageGenerator->validateSignaturePost($signature, $prefix, $expiration, $maxFileSize, $maxFileCount)) {
|
||||||
|
throw new AccessDeniedHttpException('Invalid signature');
|
||||||
}
|
}
|
||||||
|
|
||||||
$keyFiles = $request->files->keys();
|
$keyFiles = $request->files->keys();
|
||||||
|
|
||||||
|
if ($maxFileCount < count($keyFiles)) {
|
||||||
|
throw new AccessDeniedHttpException('More files than max file count');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === count($keyFiles)) {
|
||||||
|
throw new BadRequestHttpException('Zero files given');
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($keyFiles as $keyFile) {
|
foreach ($keyFiles as $keyFile) {
|
||||||
/** @var UploadedFile $file */
|
/** @var UploadedFile $file */
|
||||||
$file = $request->files->get($keyFile);
|
$file = $request->files->get($keyFile);
|
||||||
|
|
||||||
|
if ($maxFileSize < strlen($file->getContent())) {
|
||||||
|
throw new AccessDeniedHttpException('File is too big');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!str_starts_with((string) $keyFile, $prefix)) {
|
||||||
|
throw new AccessDeniedHttpException('Filename does not start with signed prefix');
|
||||||
|
}
|
||||||
|
|
||||||
$this->storedObjectManager->writeContent($keyFile, $file->getContent());
|
$this->storedObjectManager->writeContent($keyFile, $file->getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,19 +91,30 @@ final readonly class StoredObjectContentToLocalStorageController
|
|||||||
#[Route('/public/stored-object/operate', name: 'chill_docstore_stored_object_operate', methods: ['GET', 'HEAD'])]
|
#[Route('/public/stored-object/operate', name: 'chill_docstore_stored_object_operate', methods: ['GET', 'HEAD'])]
|
||||||
public function contentOperate(Request $request): Response
|
public function contentOperate(Request $request): Response
|
||||||
{
|
{
|
||||||
$objectName = $request->query->get('object_name', '');
|
if ('' === $objectName = $request->query->get('object_name', '')) {
|
||||||
|
throw new BadRequestHttpException('Object name parameter is missing');
|
||||||
|
}
|
||||||
|
|
||||||
if ('' === $objectName) {
|
if (0 === $expiration = $request->query->getInt('exp', 0)) {
|
||||||
throw new BadRequestHttpException('object name parameter is missing');
|
throw new BadRequestHttpException('Expiration is not set or equal to zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $signature = $request->query->get('sig', '')) {
|
||||||
|
throw new BadRequestHttpException('Signature is not set or is a blank string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->tempUrlLocalStorageGenerator->validateSignature($signature, strtoupper($request->getMethod()), $objectName, $expiration)) {
|
||||||
|
throw new AccessDeniedHttpException('Invalid signature');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->storedObjectManager->existsContent($objectName)) {
|
if (!$this->storedObjectManager->existsContent($objectName)) {
|
||||||
throw new NotFoundHttpException('object does not exists on disk');
|
throw new NotFoundHttpException('Object does not exists on disk');
|
||||||
}
|
}
|
||||||
|
|
||||||
return match ($request->getMethod()) {
|
return match ($request->getMethod()) {
|
||||||
'GET' => new Response($this->storedObjectManager->readContent($objectName)),
|
'GET' => new Response($this->storedObjectManager->readContent($objectName)),
|
||||||
'HEAD' => new Response(''),
|
'HEAD' => new Response(''),
|
||||||
|
default => throw new BadRequestHttpException('method not supported'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\Tests\AsyncUpload\Driver\LocalStorage;
|
|||||||
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\TempUrlLocalStorageGenerator;
|
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\TempUrlLocalStorageGenerator;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Clock\MockClock;
|
use Symfony\Component\Clock\MockClock;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
|
||||||
@ -26,24 +27,26 @@ class TempUrlLocalStorageGeneratorTest extends TestCase
|
|||||||
{
|
{
|
||||||
use ProphecyTrait;
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private const SECRET = 'abc';
|
||||||
|
|
||||||
public function testGenerate(): void
|
public function testGenerate(): void
|
||||||
{
|
{
|
||||||
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||||
$urlGenerator->generate('chill_docstore_stored_object_operate', [
|
$urlGenerator->generate('chill_docstore_stored_object_operate', [
|
||||||
'object_name' => 'testABC',
|
'object_name' => $object_name = 'testABC',
|
||||||
'exp' => $exp = 1734307200 + 180,
|
'exp' => $expiration = 1734307200 + 180,
|
||||||
'sig' => '528a426aebea86ada86035e9f3299788a074b77d3f926c6c15492457f81e88cfa98b270ba489d5d7588612eadaeeb7717a57887b33932d6d449b5f441252e600',
|
'sig' => TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name, $expiration),
|
||||||
], UrlGeneratorInterface::ABSOLUTE_URL)
|
], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||||
->shouldBeCalled()
|
->shouldBeCalled()
|
||||||
->willReturn($url = 'http://example.com/public/doc-store/stored-object/operate/testABC');
|
->willReturn($url = 'http://example.com/public/doc-store/stored-object/operate/testABC');
|
||||||
|
|
||||||
$generator = $this->buildGenerator($urlGenerator->reveal());
|
$generator = $this->buildGenerator($urlGenerator->reveal());
|
||||||
|
|
||||||
$signedUrl = $generator->generate('GET', 'testABC');
|
$signedUrl = $generator->generate('GET', $object_name);
|
||||||
|
|
||||||
self::assertEquals($url, $signedUrl->url);
|
self::assertEquals($url, $signedUrl->url);
|
||||||
self::assertEquals('testABC', $signedUrl->object_name);
|
self::assertEquals($object_name, $signedUrl->object_name);
|
||||||
self::assertEquals($exp, $signedUrl->expires->getTimestamp());
|
self::assertEquals($expiration, $signedUrl->expires->getTimestamp());
|
||||||
self::assertEquals('GET', $signedUrl->method);
|
self::assertEquals('GET', $signedUrl->method);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,17 +65,174 @@ class TempUrlLocalStorageGeneratorTest extends TestCase
|
|||||||
|
|
||||||
self::assertEquals($url, $signedUrl->url);
|
self::assertEquals($url, $signedUrl->url);
|
||||||
self::assertEquals('prefixABC', $signedUrl->object_name);
|
self::assertEquals('prefixABC', $signedUrl->object_name);
|
||||||
self::assertEquals(1734307200 + 180 + 180, $signedUrl->expires->getTimestamp());
|
self::assertEquals($expiration = 1734307200 + 180 + 180, $signedUrl->expires->getTimestamp());
|
||||||
self::assertEquals('POST', $signedUrl->method);
|
self::assertEquals('POST', $signedUrl->method);
|
||||||
self::assertEquals('47e9271917c6c9149a47248d88eeadcbc1f804138e445f3406d39f6aa51b2d976c2ee6e5b1196d2bf88852b627b330596c2fbf6f31b06837e9f41cf56103a4e4', $signedUrl->signature);
|
self::assertEquals(TempUrlLocalStorageGeneratorTest::expectedSignature('POST', 'prefixABC', $expiration), $signedUrl->signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildGenerator(UrlGeneratorInterface $urlGenerator): TempUrlLocalStorageGenerator
|
private static function expectedSignature(string $method, $objectName, int $expiration): string
|
||||||
|
{
|
||||||
|
return hash('sha512', sprintf('%s.%s.%s.%d', $method, self::SECRET, $objectName, $expiration));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateValidateSignatureData
|
||||||
|
*/
|
||||||
|
public function testValidateSignature(string $signature, string $method, string $objectName, int $expiration, \DateTimeImmutable $now, bool $expected, string $message): void
|
||||||
|
{
|
||||||
|
$urlGenerator = $this->buildGenerator(clock: new MockClock($now));
|
||||||
|
|
||||||
|
self::assertEquals($expected, $urlGenerator->validateSignature($signature, $method, $objectName, $expiration), $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateValidateSignaturePostData
|
||||||
|
*/
|
||||||
|
public function testValidateSignaturePost(string $signature, int $expiration, string $objectName, int $maxFileSize, int $maxFileCount, \DateTimeImmutable $now, bool $expected, string $message): void
|
||||||
|
{
|
||||||
|
$urlGenerator = $this->buildGenerator(clock: new MockClock($now));
|
||||||
|
|
||||||
|
self::assertEquals($expected, $urlGenerator->validateSignaturePost($signature, $objectName, $expiration, $maxFileSize, $maxFileCount), $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateValidateSignaturePostData(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
$expiration,
|
||||||
|
$object_name,
|
||||||
|
15_000_000,
|
||||||
|
1,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
true,
|
||||||
|
'Valid signature',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
$expiration,
|
||||||
|
$object_name,
|
||||||
|
15_000_001,
|
||||||
|
1,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
false,
|
||||||
|
'Wrong max file size',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
$expiration,
|
||||||
|
$object_name,
|
||||||
|
15_000_000,
|
||||||
|
2,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
false,
|
||||||
|
'Wrong max file count',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
$expiration,
|
||||||
|
$object_name.'AAA',
|
||||||
|
15_000_000,
|
||||||
|
1,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
false,
|
||||||
|
'Invalid object name',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180).'A',
|
||||||
|
$expiration,
|
||||||
|
$object_name,
|
||||||
|
15_000_000,
|
||||||
|
1,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
false,
|
||||||
|
'Invalid signature',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
$expiration,
|
||||||
|
$object_name,
|
||||||
|
15_000_000,
|
||||||
|
1,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration + 1)),
|
||||||
|
false,
|
||||||
|
'Expired signature',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateValidateSignatureData(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
'GET',
|
||||||
|
$object_name,
|
||||||
|
$expiration,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
true,
|
||||||
|
'Valid signature, not expired',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('HEAD', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
'HEAD',
|
||||||
|
$object_name,
|
||||||
|
$expiration,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
true,
|
||||||
|
'Valid signature, not expired',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180).'A',
|
||||||
|
'GET',
|
||||||
|
$object_name,
|
||||||
|
$expiration,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
false,
|
||||||
|
'Invalid signature',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
'GET',
|
||||||
|
$object_name,
|
||||||
|
$expiration,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration + 1)),
|
||||||
|
false,
|
||||||
|
'Signature expired',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
'GET',
|
||||||
|
$object_name.'____',
|
||||||
|
$expiration,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
false,
|
||||||
|
'Invalid object name',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
|
||||||
|
'POST',
|
||||||
|
$object_name,
|
||||||
|
$expiration,
|
||||||
|
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
|
||||||
|
false,
|
||||||
|
'Wrong method',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildGenerator(?UrlGeneratorInterface $urlGenerator = null, ?ClockInterface $clock = null): TempUrlLocalStorageGenerator
|
||||||
{
|
{
|
||||||
return new TempUrlLocalStorageGenerator(
|
return new TempUrlLocalStorageGenerator(
|
||||||
'abc',
|
self::SECRET,
|
||||||
new MockClock('2024-12-16T00:00:00+00:00'),
|
$clock ?? new MockClock('2024-12-16T00:00:00+00:00'),
|
||||||
$urlGenerator,
|
$urlGenerator ?? $this->prophesize(UrlGeneratorInterface::class)->reveal(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,338 @@
|
|||||||
|
<?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\AsyncUpload\Driver\LocalStorage\StoredObjectManager;
|
||||||
|
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\TempUrlLocalStorageGenerator;
|
||||||
|
use Chill\DocStoreBundle\Controller\StoredObjectContentToLocalStorageController;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class StoredObjectContentToLocalStorageControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateOperateContentWithExceptionDataProvider
|
||||||
|
*/
|
||||||
|
public function testOperateContentWithException(Request $request, string $expectedException, string $expectedExceptionMessage, bool $existContent, string $readContent, bool $signatureValidity): void
|
||||||
|
{
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManager::class);
|
||||||
|
$storedObjectManager->existsContent(Argument::any())->willReturn($existContent);
|
||||||
|
$storedObjectManager->readContent(Argument::any())->willReturn($readContent);
|
||||||
|
|
||||||
|
$tempUrlLocalStorageGenerator = $this->prophesize(TempUrlLocalStorageGenerator::class);
|
||||||
|
$tempUrlLocalStorageGenerator->validateSignature(
|
||||||
|
$request->query->get('sig', ''),
|
||||||
|
$request->getMethod(),
|
||||||
|
$request->query->get('object_name', ''),
|
||||||
|
$request->query->getInt('exp', 0)
|
||||||
|
)
|
||||||
|
->willReturn($signatureValidity);
|
||||||
|
|
||||||
|
$this->expectException($expectedException);
|
||||||
|
$this->expectExceptionMessage($expectedExceptionMessage);
|
||||||
|
|
||||||
|
$controller = new StoredObjectContentToLocalStorageController(
|
||||||
|
$storedObjectManager->reveal(),
|
||||||
|
$tempUrlLocalStorageGenerator->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$controller->contentOperate($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOperateContentGetHappyScenario(): void
|
||||||
|
{
|
||||||
|
$objectName = 'testABC';
|
||||||
|
$expiration = new \DateTimeImmutable();
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManager::class);
|
||||||
|
$storedObjectManager->existsContent($objectName)->willReturn(true);
|
||||||
|
$storedObjectManager->readContent($objectName)->willReturn('123456789');
|
||||||
|
|
||||||
|
$tempUrlLocalStorageGenerator = $this->prophesize(TempUrlLocalStorageGenerator::class);
|
||||||
|
$tempUrlLocalStorageGenerator->validateSignature('signature', 'GET', $objectName, $expiration->getTimestamp())
|
||||||
|
->shouldBeCalled()
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$controller = new StoredObjectContentToLocalStorageController(
|
||||||
|
$storedObjectManager->reveal(),
|
||||||
|
$tempUrlLocalStorageGenerator->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $controller->contentOperate(new Request(['object_name' => $objectName, 'sig' => 'signature', 'exp' => $expiration->getTimestamp()]));
|
||||||
|
|
||||||
|
self::assertEquals(200, $response->getStatusCode());
|
||||||
|
self::assertEquals('123456789', $response->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOperateContentHeadHappyScenario(): void
|
||||||
|
{
|
||||||
|
$objectName = 'testABC';
|
||||||
|
$expiration = new \DateTimeImmutable();
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManager::class);
|
||||||
|
$storedObjectManager->existsContent($objectName)->willReturn(true);
|
||||||
|
$storedObjectManager->readContent($objectName)->willReturn('123456789');
|
||||||
|
|
||||||
|
$tempUrlLocalStorageGenerator = $this->prophesize(TempUrlLocalStorageGenerator::class);
|
||||||
|
$tempUrlLocalStorageGenerator->validateSignature('signature', 'HEAD', $objectName, $expiration->getTimestamp())
|
||||||
|
->shouldBeCalled()
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$controller = new StoredObjectContentToLocalStorageController(
|
||||||
|
$storedObjectManager->reveal(),
|
||||||
|
$tempUrlLocalStorageGenerator->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$request = new Request(['object_name' => $objectName, 'sig' => 'signature', 'exp' => $expiration->getTimestamp()]);
|
||||||
|
$request->setMethod('HEAD');
|
||||||
|
$response = $controller->contentOperate($request);
|
||||||
|
|
||||||
|
self::assertEquals(200, $response->getStatusCode());
|
||||||
|
self::assertEquals('', $response->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostContentHappyScenario(): void
|
||||||
|
{
|
||||||
|
$expiration = 171899000;
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManager::class);
|
||||||
|
$storedObjectManager->writeContent('filePrefix/abcSUFFIX', Argument::containingString('fake_encrypted_content'))
|
||||||
|
->shouldBeCalled();
|
||||||
|
|
||||||
|
$tempUrlGenerator = $this->prophesize(TempUrlLocalStorageGenerator::class);
|
||||||
|
$tempUrlGenerator->validateSignaturePost('signature', 'filePrefix/abc', $expiration, 15_000_000, 1)
|
||||||
|
->shouldBeCalled()
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$controller = new StoredObjectContentToLocalStorageController($storedObjectManager->reveal(), $tempUrlGenerator->reveal());
|
||||||
|
|
||||||
|
$request = new Request(
|
||||||
|
['prefix' => 'filePrefix/abc'],
|
||||||
|
['signature' => 'signature', 'expires' => $expiration, 'max_file_size' => 15_000_000, 'max_file_count' => 1],
|
||||||
|
files: [
|
||||||
|
'filePrefix/abcSUFFIX' => new UploadedFile(
|
||||||
|
__DIR__.'/data/StoredObjectContentToLocalStorageControllerTest/dummy_file',
|
||||||
|
'Document.odt',
|
||||||
|
test: true
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $controller->postContent($request);
|
||||||
|
|
||||||
|
self::assertEquals(204, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generatePostContentWithExceptionDataProvider
|
||||||
|
*/
|
||||||
|
public function testPostContentWithException(Request $request, bool $isSignatureValid, string $expectedException, string $expectedExceptionMessage): void
|
||||||
|
{
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManager::class);
|
||||||
|
$storedObjectManager->writeContent(Argument::any(), Argument::any())->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$tempUrlGenerator = $this->prophesize(TempUrlLocalStorageGenerator::class);
|
||||||
|
$tempUrlGenerator->validateSignaturePost('signature', Argument::any(), Argument::any(), Argument::any(), Argument::any())
|
||||||
|
->willReturn($isSignatureValid);
|
||||||
|
|
||||||
|
$controller = new StoredObjectContentToLocalStorageController(
|
||||||
|
$storedObjectManager->reveal(),
|
||||||
|
$tempUrlGenerator->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectException($expectedException);
|
||||||
|
$this->expectExceptionMessage($expectedExceptionMessage);
|
||||||
|
|
||||||
|
$controller->postContent($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generatePostContentWithExceptionDataProvider(): iterable
|
||||||
|
{
|
||||||
|
$query = ['prefix' => 'filePrefix/abc'];
|
||||||
|
$attributes = ['signature' => 'signature', 'expires' => 15088556855, 'max_file_size' => 15_000_000, 'max_file_count' => 1];
|
||||||
|
|
||||||
|
$request = new Request([]);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Prefix parameter is missing',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
$attrCloned = [...$attributes];
|
||||||
|
unset($attrCloned['max_file_size']);
|
||||||
|
$request = new Request($query, $attrCloned);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Max file size is not set or equal to zero',
|
||||||
|
];
|
||||||
|
|
||||||
|
$attrCloned = [...$attributes];
|
||||||
|
unset($attrCloned['max_file_count']);
|
||||||
|
$request = new Request($query, $attrCloned);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Max file count is not set or equal to zero',
|
||||||
|
];
|
||||||
|
|
||||||
|
$attrCloned = [...$attributes];
|
||||||
|
unset($attrCloned['expires']);
|
||||||
|
$request = new Request($query, $attrCloned);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Expiration is not set or equal to zero',
|
||||||
|
];
|
||||||
|
|
||||||
|
$attrCloned = [...$attributes];
|
||||||
|
unset($attrCloned['signature']);
|
||||||
|
$request = new Request($query, $attrCloned);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Signature is not set or is a blank string',
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new Request($query, $attributes);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
false,
|
||||||
|
AccessDeniedHttpException::class,
|
||||||
|
'Invalid signature',
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new Request($query, $attributes, files: []);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Zero files given',
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new Request($query, $attributes, files: [
|
||||||
|
'filePrefix/abcSUFFIX_1' => new UploadedFile(__DIR__.'/data/StoredObjectContentToLocalStorageControllerTest/dummy_file', 'Some content', test: true),
|
||||||
|
'filePrefix/abcSUFFIX_2' => new UploadedFile(__DIR__.'/data/StoredObjectContentToLocalStorageControllerTest/dummy_file', 'Some content2', test: true),
|
||||||
|
]);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
AccessDeniedHttpException::class,
|
||||||
|
'More files than max file count',
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new Request($query, [...$attributes, 'max_file_size' => 3], files: [
|
||||||
|
'filePrefix/abcSUFFIX_1' => new UploadedFile(__DIR__.'/data/StoredObjectContentToLocalStorageControllerTest/dummy_file', 'Some content', test: true),
|
||||||
|
]);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
AccessDeniedHttpException::class,
|
||||||
|
'File is too big',
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new Request($query, [...$attributes], files: [
|
||||||
|
'some/other/prefix_SUFFIX' => new UploadedFile(__DIR__.'/data/StoredObjectContentToLocalStorageControllerTest/dummy_file', 'Some content', test: true),
|
||||||
|
]);
|
||||||
|
$request->setMethod('POST');
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$request,
|
||||||
|
true,
|
||||||
|
AccessDeniedHttpException::class,
|
||||||
|
'Filename does not start with signed prefix',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateOperateContentWithExceptionDataProvider(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
new Request(['object_name' => '', 'sig' => '', 'exp' => 0]),
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Object name parameter is missing',
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
false,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => 0]),
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Expiration is not set or equal to zero',
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
false,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
|
||||||
|
BadRequestHttpException::class,
|
||||||
|
'Signature is not set or is a blank string',
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
false,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
|
||||||
|
AccessDeniedHttpException::class,
|
||||||
|
'Invalid signature',
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
false,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
yield [
|
||||||
|
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
|
||||||
|
NotFoundHttpException::class,
|
||||||
|
'Object does not exists on disk',
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
fake_encrypted_content
|
Loading…
x
Reference in New Issue
Block a user