mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch '346-store-docs-on-disk' into 'master'
Resolve "Permettre de stocker les documents sur disque, localement." Closes #346 See merge request Chill-Projet/chill-bundles!774
This commit is contained in:
commit
ce3cce7b95
@ -1,4 +1,7 @@
|
||||
chill_doc_store:
|
||||
use_driver: openstack
|
||||
local_storage:
|
||||
storage_path: '%kernel.project_dir%/var/storage'
|
||||
openstack:
|
||||
temp_url:
|
||||
temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
|
||||
|
84
docs/source/installation/document-storage.rst
Normal file
84
docs/source/installation/document-storage.rst
Normal file
@ -0,0 +1,84 @@
|
||||
Document storage
|
||||
################
|
||||
|
||||
You can store document on two different ways:
|
||||
|
||||
- on disk
|
||||
- in the cloud, using object storage: currently only `openstack swift <https://docs.openstack.org/api-ref/object-store/index.html>`_ is supported.
|
||||
|
||||
Comparison
|
||||
==========
|
||||
|
||||
Storing documents within the cloud is particularily suitable for "portable" deployments, like in kubernetes, or within container
|
||||
without having to manage volumes to store documents. But you'll have to subscribe on a commercial offer.
|
||||
|
||||
Storing documents on disk is more easy to configure, but more difficult to manage: if you use container, you will have to
|
||||
manager volumes to attach documents on disk. You'll have to do some backup of the directory. If chill is load-balanced (and
|
||||
multiple instances of chill are run), you will have to find a way to share the directories in read-write mode for every instance.
|
||||
|
||||
On Disk
|
||||
=======
|
||||
|
||||
Configure Chill like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# file config/packages/chill_doc_store.yaml
|
||||
chill_doc_store:
|
||||
use_driver: local_storage
|
||||
local_storage:
|
||||
storage_path: '%kernel.project_dir%/var/storage'
|
||||
|
||||
In this configuration, documents will be stored in :code:`var/storage` within your app directory. But this path can be
|
||||
elsewhere on the disk. Be aware that the directory must be writable by the user executing the chill app (php-fpm or www-data).
|
||||
|
||||
Documents will be stored in subpathes within that directory. The files will be encrypted, the key is stored in the database.
|
||||
|
||||
In the cloud, using openstack object store
|
||||
##########################################
|
||||
|
||||
You must subscribe to a commercial offer for object store.
|
||||
|
||||
Chill use some features to allow documents to be stored in the cloud without being uploaded first to the chill server:
|
||||
|
||||
- `Form POST Middelware <https://docs.openstack.org/swift/latest/api/form_post_middleware.html>`_;
|
||||
- `Temporary URL Middelware <https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html>`_.
|
||||
|
||||
A secret key must be generated and configured, and CORS must be configured depending on the domain you will use to serve Chill.
|
||||
|
||||
At first, create a container and get the base path to the container. For instance, on OVH, if you create a container named "mychill",
|
||||
you will be able to retrieve the base path of the container within the OVH interface, like this:
|
||||
|
||||
- base_path: :code:`https://storage.gra.cloud.ovh.net/v1/AUTH_123456789/mychill/` => will be variable :code:`ASYNC_UPLOAD_TEMP_URL_BASE_PATH`
|
||||
- container: :code:`mychill` => will be variable :code:`ASYNC_UPLOAD_TEMP_URL_CONTAINER`
|
||||
|
||||
You can also generate a key, which should have at least 20 characters. This key will go in the variable :code:`ASYNC_UPLOAD_TEMP_URL_KEY`.
|
||||
|
||||
.. note::
|
||||
|
||||
See the `documentation of symfony <https://symfony.com/doc/current/configuration.html#config-env-vars>`_ on how to store variables, and how to encrypt them if needed.
|
||||
|
||||
Configure the storage like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# file config/packages/chill_doc_store.yaml
|
||||
chill_doc_store:
|
||||
use_driver: openstack
|
||||
openstack:
|
||||
temp_url:
|
||||
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
|
||||
|
||||
Chill is able to configure the container in order to store document. Grab an Openstack Token (for instance, using :code:`openstack token issue` or
|
||||
the web interface of your openstack provider), and run this command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony console async-upload:configure --os_token=OPENSTACK_TOKEN -d https://mychill.mydomain.example
|
||||
|
||||
# or, without symfony-cli
|
||||
bin/console async-upload:configure --os_token=OPENSTACK_TOKEN -d https://mychill.mydomain.example
|
||||
|
||||
|
@ -323,6 +323,7 @@ Going further
|
||||
:maxdepth: 2
|
||||
|
||||
prod.rst
|
||||
document-storage.rst
|
||||
load-addresses.rst
|
||||
prod-calendar-sms-sending.rst
|
||||
msgraph-configure.rst
|
||||
|
@ -0,0 +1,227 @@
|
||||
<?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\AsyncUpload\Driver\LocalStorage;
|
||||
|
||||
use Base64Url\Base64Url;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
|
||||
class StoredObjectManager implements StoredObjectManagerInterface
|
||||
{
|
||||
private readonly string $baseDir;
|
||||
|
||||
private readonly Filesystem $filesystem;
|
||||
|
||||
public function __construct(
|
||||
ParameterBagInterface $parameterBag,
|
||||
private readonly KeyGenerator $keyGenerator,
|
||||
) {
|
||||
$this->baseDir = $parameterBag->get('chill_doc_store')['local_storage']['storage_path'];
|
||||
$this->filesystem = new Filesystem();
|
||||
}
|
||||
|
||||
public function getLastModified(StoredObject|StoredObjectVersion $document): \DateTimeInterface
|
||||
{
|
||||
$version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;
|
||||
|
||||
if (null === $version) {
|
||||
throw StoredObjectManagerException::storedObjectDoesNotContainsVersion();
|
||||
}
|
||||
|
||||
$path = $this->buildPath($version->getFilename());
|
||||
|
||||
if (false === $ts = filemtime($path)) {
|
||||
throw StoredObjectManagerException::unableToReadDocumentOnDisk($path);
|
||||
}
|
||||
|
||||
return \DateTimeImmutable::createFromFormat('U', (string) $ts);
|
||||
}
|
||||
|
||||
public function getContentLength(StoredObject|StoredObjectVersion $document): int
|
||||
{
|
||||
return strlen($this->read($document));
|
||||
}
|
||||
|
||||
public function exists(StoredObject|StoredObjectVersion $document): bool
|
||||
{
|
||||
$version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;
|
||||
|
||||
if (null === $version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->existsContent($version->getFilename());
|
||||
}
|
||||
|
||||
public function read(StoredObject|StoredObjectVersion $document): string
|
||||
{
|
||||
$version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;
|
||||
|
||||
if (null === $version) {
|
||||
throw StoredObjectManagerException::storedObjectDoesNotContainsVersion();
|
||||
}
|
||||
|
||||
$content = $this->readContent($version->getFilename());
|
||||
|
||||
if (!$this->isVersionEncrypted($version)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$clearData = openssl_decrypt(
|
||||
$content,
|
||||
self::ALGORITHM,
|
||||
// TODO: Why using this library and not use base64_decode() ?
|
||||
Base64Url::decode($version->getKeyInfos()['k']),
|
||||
\OPENSSL_RAW_DATA,
|
||||
pack('C*', ...$version->getIv())
|
||||
);
|
||||
|
||||
if (false === $clearData) {
|
||||
throw StoredObjectManagerException::unableToDecrypt(openssl_error_string());
|
||||
}
|
||||
|
||||
return $clearData;
|
||||
}
|
||||
|
||||
public function write(StoredObject $document, string $clearContent, ?string $contentType = null): StoredObjectVersion
|
||||
{
|
||||
$newIv = $document->isEncrypted() ? $document->getIv() : $this->keyGenerator->generateIv();
|
||||
$newKey = $document->isEncrypted() ? $document->getKeyInfos() : $this->keyGenerator->generateKey(self::ALGORITHM);
|
||||
$newType = $contentType ?? $document->getType();
|
||||
$version = $document->registerVersion(
|
||||
$newIv,
|
||||
$newKey,
|
||||
$newType
|
||||
);
|
||||
|
||||
$encryptedContent = $this->isVersionEncrypted($version)
|
||||
? openssl_encrypt(
|
||||
$clearContent,
|
||||
self::ALGORITHM,
|
||||
// TODO: Why using this library and not use base64_decode() ?
|
||||
Base64Url::decode($version->getKeyInfos()['k']),
|
||||
\OPENSSL_RAW_DATA,
|
||||
pack('C*', ...$version->getIv())
|
||||
)
|
||||
: $clearContent;
|
||||
|
||||
if (false === $encryptedContent) {
|
||||
throw StoredObjectManagerException::unableToEncryptDocument((string) openssl_error_string());
|
||||
}
|
||||
|
||||
$this->writeContent($version->getFilename(), $encryptedContent);
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
public function readContent(string $filename): string
|
||||
{
|
||||
$path = $this->buildPath($filename);
|
||||
|
||||
if (!file_exists($path)) {
|
||||
throw StoredObjectManagerException::unableToFindDocumentOnDisk($path);
|
||||
}
|
||||
|
||||
if (false === $content = file_get_contents($path)) {
|
||||
throw StoredObjectManagerException::unableToReadDocumentOnDisk($path);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function writeContent(string $filename, string $encryptedContent): void
|
||||
{
|
||||
$fullPath = $this->buildPath($filename);
|
||||
$dir = Path::getDirectory($fullPath);
|
||||
|
||||
if (!$this->filesystem->exists($dir)) {
|
||||
$this->filesystem->mkdir($dir);
|
||||
}
|
||||
|
||||
$result = file_put_contents($fullPath, $encryptedContent);
|
||||
|
||||
if (false === $result) {
|
||||
throw StoredObjectManagerException::unableToStoreDocumentOnDisk();
|
||||
}
|
||||
}
|
||||
|
||||
public function existsContent(string $filename): bool
|
||||
{
|
||||
$path = $this->buildPath($filename);
|
||||
|
||||
return $this->filesystem->exists($path);
|
||||
}
|
||||
|
||||
private function buildPath(string $filename): string
|
||||
{
|
||||
$dirs = [$this->baseDir];
|
||||
|
||||
for ($i = 0; $i < min(strlen($filename), 8); ++$i) {
|
||||
$dirs[] = $filename[$i];
|
||||
}
|
||||
|
||||
$dirs[] = $filename;
|
||||
|
||||
return Path::canonicalize(implode(DIRECTORY_SEPARATOR, $dirs));
|
||||
}
|
||||
|
||||
public function delete(StoredObjectVersion $storedObjectVersion): void
|
||||
{
|
||||
if (!$this->exists($storedObjectVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $this->buildPath($storedObjectVersion->getFilename());
|
||||
|
||||
$this->filesystem->remove($path);
|
||||
$this->removeDirectoriesRecursively(Path::getDirectory($path));
|
||||
}
|
||||
|
||||
private function removeDirectoriesRecursively(string $path): void
|
||||
{
|
||||
if ($path === $this->baseDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = scandir($path);
|
||||
|
||||
// if it does contains only "." and "..", we can remove the directory
|
||||
if (2 === count($files) && in_array('.', $files, true) && in_array('..', $files, true)) {
|
||||
$this->filesystem->remove($path);
|
||||
$this->removeDirectoriesRecursively(Path::getDirectory($path));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StoredObjectManagerException
|
||||
*/
|
||||
public function etag(StoredObject|StoredObjectVersion $document): string
|
||||
{
|
||||
return md5($this->read($document));
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
// there is no cache: nothing to do here !
|
||||
}
|
||||
|
||||
private function isVersionEncrypted(StoredObjectVersion $storedObjectVersion): bool
|
||||
{
|
||||
return $storedObjectVersion->isEncrypted();
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
<?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\AsyncUpload\Driver\LocalStorage;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class TempUrlLocalStorageGenerator implements TempUrlGeneratorInterface
|
||||
{
|
||||
private const SIGNATURE_DURATION = 180;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $secret,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
) {}
|
||||
|
||||
public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl
|
||||
{
|
||||
$expiration = $this->clock->now()->getTimestamp() + min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION);
|
||||
|
||||
return new SignedUrl(
|
||||
strtoupper($method),
|
||||
$this->urlGenerator->generate('chill_docstore_stored_object_operate', [
|
||||
'object_name' => $object_name,
|
||||
'exp' => $expiration,
|
||||
'sig' => $this->sign(strtoupper($method), $object_name, $expiration),
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL),
|
||||
\DateTimeImmutable::createFromFormat('U', (string) $expiration),
|
||||
$object_name,
|
||||
);
|
||||
}
|
||||
|
||||
public function generatePost(?int $expire_delay = null, ?int $submit_delay = null, int $max_file_count = 1, ?string $object_name = null): SignedUrlPost
|
||||
{
|
||||
$submitDelayComputed = min($submit_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION);
|
||||
$expireDelayComputed = min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION);
|
||||
$objectNameComputed = $object_name ?? StoredObject::generatePrefix();
|
||||
$expiration = $this->clock->now()->getTimestamp() + $expireDelayComputed + $submitDelayComputed;
|
||||
|
||||
return new SignedUrlPost(
|
||||
$this->urlGenerator->generate(
|
||||
'chill_docstore_storedobject_post',
|
||||
['prefix' => $objectNameComputed],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
),
|
||||
\DateTimeImmutable::createFromFormat('U', (string) $expiration),
|
||||
$objectNameComputed,
|
||||
15_000_000,
|
||||
1,
|
||||
$submitDelayComputed,
|
||||
'',
|
||||
$objectNameComputed,
|
||||
$this->sign('POST', $object_name, $expiration),
|
||||
);
|
||||
}
|
||||
|
||||
private function sign(string $method, string $object_name, int $expiration): string
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Command;
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
@ -9,13 +9,14 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Base64Url\Base64Url;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
@ -24,8 +25,6 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
{
|
||||
private const ALGORITHM = 'AES-256-CBC';
|
||||
|
||||
private array $inMemory = [];
|
||||
|
||||
public function __construct(
|
||||
@ -361,6 +360,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
|
||||
private function isVersionEncrypted(StoredObjectVersion $storedObjectVersion): bool
|
||||
{
|
||||
return ([] !== $storedObjectVersion->getKeyInfos()) && ([] !== $storedObjectVersion->getIv());
|
||||
return $storedObjectVersion->isEncrypted();
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle;
|
||||
|
||||
use Chill\DocStoreBundle\DependencyInjection\Compiler\StorageConfigurationCompilerPass;
|
||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
|
||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
|
||||
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
|
||||
@ -27,5 +28,7 @@ class ChillDocStoreBundle extends Bundle
|
||||
->addTag('chill_doc_store.generic_doc_person_provider');
|
||||
$container->registerForAutoconfiguration(GenericDocRendererInterface::class)
|
||||
->addTag('chill_doc_store.generic_doc_renderer');
|
||||
|
||||
$container->addCompilerPass(new StorageConfigurationCompilerPass());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,120 @@
|
||||
<?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\AsyncUpload\Driver\LocalStorage\StoredObjectManager;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\TempUrlLocalStorageGenerator;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller to deal with local storage operation.
|
||||
*/
|
||||
final readonly class StoredObjectContentToLocalStorageController
|
||||
{
|
||||
public function __construct(
|
||||
private StoredObjectManager $storedObjectManager,
|
||||
private TempUrlLocalStorageGenerator $tempUrlLocalStorageGenerator,
|
||||
) {}
|
||||
|
||||
#[Route('/public/stored-object/post', name: 'chill_docstore_storedobject_post', methods: ['POST'])]
|
||||
public function postContent(Request $request): Response
|
||||
{
|
||||
$prefix = $request->query->get('prefix', '');
|
||||
|
||||
if ('' === $prefix) {
|
||||
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();
|
||||
|
||||
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) {
|
||||
/** @var UploadedFile $file */
|
||||
$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());
|
||||
}
|
||||
|
||||
return new Response(status: Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
#[Route('/public/stored-object/operate', name: 'chill_docstore_stored_object_operate', methods: ['GET', 'HEAD'])]
|
||||
public function contentOperate(Request $request): Response
|
||||
{
|
||||
if ('' === $objectName = $request->query->get('object_name', '')) {
|
||||
throw new BadRequestHttpException('Object name parameter is missing');
|
||||
}
|
||||
|
||||
if (0 === $expiration = $request->query->getInt('exp', 0)) {
|
||||
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)) {
|
||||
throw new NotFoundHttpException('Object does not exists on disk');
|
||||
}
|
||||
|
||||
return match ($request->getMethod()) {
|
||||
'GET' => new Response($this->storedObjectManager->readContent($objectName)),
|
||||
'HEAD' => new Response(''),
|
||||
default => throw new BadRequestHttpException('method not supported'),
|
||||
};
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
$this->prependTwig($container);
|
||||
}
|
||||
|
||||
protected function prependAuthorization(ContainerBuilder $container)
|
||||
private function prependAuthorization(ContainerBuilder $container)
|
||||
{
|
||||
$container->prependExtensionConfig('security', [
|
||||
'role_hierarchy' => [
|
||||
@ -69,7 +69,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
]);
|
||||
}
|
||||
|
||||
protected function prependRoute(ContainerBuilder $container)
|
||||
private function prependRoute(ContainerBuilder $container)
|
||||
{
|
||||
// declare routes for task bundle
|
||||
$container->prependExtensionConfig('chill_main', [
|
||||
@ -81,7 +81,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
]);
|
||||
}
|
||||
|
||||
protected function prependTwig(ContainerBuilder $container)
|
||||
private function prependTwig(ContainerBuilder $container)
|
||||
{
|
||||
$twigConfig = [
|
||||
'form_themes' => ['@ChillDocStore/Form/fields.html.twig'],
|
||||
|
@ -0,0 +1,86 @@
|
||||
<?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\DependencyInjection\Compiler;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\TempUrlLocalStorageGenerator;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\ConfigureOpenstackObjectStorageCommand;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Controller\StoredObjectContentToLocalStorageController;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
class StorageConfigurationCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
private const SERVICES_OPENSTACK = [
|
||||
\Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\StoredObjectManager::class,
|
||||
TempUrlOpenstackGenerator::class,
|
||||
ConfigureOpenstackObjectStorageCommand::class,
|
||||
];
|
||||
|
||||
private const SERVICES_LOCAL_STORAGE = [
|
||||
\Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\StoredObjectManager::class,
|
||||
TempUrlLocalStorageGenerator::class,
|
||||
StoredObjectContentToLocalStorageController::class,
|
||||
];
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
$config = $container
|
||||
->getParameterBag()
|
||||
->resolveValue($container->getParameter('chill_doc_store'));
|
||||
|
||||
if (array_key_exists('local_storage', $config) && !array_key_exists('openstack', $config)) {
|
||||
$driver = 'local_storage';
|
||||
$this->checkUseDriverConfiguration($config['use_driver'] ?? null, $driver);
|
||||
} elseif (!array_key_exists('local_storage', $config) && array_key_exists('openstack', $config)) {
|
||||
$driver = 'openstack';
|
||||
$this->checkUseDriverConfiguration($config['use_driver'] ?? null, $driver);
|
||||
} elseif (array_key_exists('openstack', $config) && array_key_exists('local_storage', $config)) {
|
||||
$driver = $config['use_driver'] ?? null;
|
||||
if (null === $driver) {
|
||||
throw new InvalidConfigurationException('There are multiple drivers configured for chill_doc_store, set the one you want to use with the variable use_driver');
|
||||
}
|
||||
} else {
|
||||
throw new InvalidConfigurationException('No driver defined for storing document. Define one in chill_doc_store configuration');
|
||||
}
|
||||
|
||||
if ('local_storage' === $driver) {
|
||||
foreach (self::SERVICES_OPENSTACK as $service) {
|
||||
$container->removeDefinition($service);
|
||||
}
|
||||
|
||||
$container->setAlias(StoredObjectManagerInterface::class, \Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\StoredObjectManager::class);
|
||||
$container->setAlias(TempUrlGeneratorInterface::class, TempUrlLocalStorageGenerator::class);
|
||||
} else {
|
||||
foreach (self::SERVICES_LOCAL_STORAGE as $service) {
|
||||
$container->removeDefinition($service);
|
||||
}
|
||||
|
||||
$container->setAlias(StoredObjectManagerInterface::class, \Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\StoredObjectManager::class);
|
||||
$container->setAlias(TempUrlGeneratorInterface::class, TempUrlOpenstackGenerator::class);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkUseDriverConfiguration(?string $useDriver, string $driver): void
|
||||
{
|
||||
if (null === $useDriver) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($useDriver !== $driver) {
|
||||
throw new InvalidConfigurationException(sprintf('The "use_driver" configuration require a driver (%s) which is not configured. Configure this driver in order to use it.', $useDriver));
|
||||
}
|
||||
}
|
||||
}
|
@ -30,10 +30,22 @@ class Configuration implements ConfigurationInterface
|
||||
|
||||
/* @phpstan-ignore-next-line As there are inconsistencies in return types, but the code works... */
|
||||
$rootNode->children()
|
||||
->enumNode('use_driver')
|
||||
->values(['local_storage', 'openstack'])
|
||||
->info('Driver to use. Default to the single one if multiple driver are defined. Configuration will raise an error if there are multiple drivers defined, and if this key is not set')
|
||||
->end()
|
||||
->arrayNode('local_storage')
|
||||
->info('where the stored object should be stored')
|
||||
->children()
|
||||
->scalarNode('storage_path')
|
||||
->info('the folder where the stored object should be stored')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->end() // end of storage_path
|
||||
->end() // end of children
|
||||
->end() // end of local_storage
|
||||
// openstack node
|
||||
->arrayNode('openstack')
|
||||
->info('parameters to authenticate and generate temp url against the openstack object storage service')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
// openstack.temp_url
|
||||
->arrayNode('temp_url')
|
||||
|
@ -448,4 +448,12 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
{
|
||||
return $storedObject->getDeleteAt() < $now && $storedObject->getVersions()->isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if it has a current version, and if the current version is encrypted.
|
||||
*/
|
||||
public function isEncrypted(): bool
|
||||
{
|
||||
return $this->hasCurrentVersion() && $this->getCurrentVersion()->isEncrypted();
|
||||
}
|
||||
}
|
||||
|
@ -226,4 +226,9 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEncrypted(): bool
|
||||
{
|
||||
return ([] !== $this->getKeyInfos()) && ([] !== $this->getIv());
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,29 @@ final class StoredObjectManagerException extends \Exception
|
||||
{
|
||||
return new self('Unable to get content from response.', 500, $exception);
|
||||
}
|
||||
|
||||
public static function unableToStoreDocumentOnDisk(?\Throwable $exception = null): self
|
||||
{
|
||||
return new self('Unable to store document on disk.', previous: $exception);
|
||||
}
|
||||
|
||||
public static function unableToFindDocumentOnDisk(string $path): self
|
||||
{
|
||||
return new self('Unable to find document on disk at path "'.$path.'".');
|
||||
}
|
||||
|
||||
public static function unableToReadDocumentOnDisk(string $path): self
|
||||
{
|
||||
return new self('Unable to read document on disk at path "'.$path.'".');
|
||||
}
|
||||
|
||||
public static function unableToEncryptDocument(string $errors): self
|
||||
{
|
||||
return new self('Unable to encrypt document: '.$errors);
|
||||
}
|
||||
|
||||
public static function storedObjectDoesNotContainsVersion(): self
|
||||
{
|
||||
return new self('Stored object does not contains any version');
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
<?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\Service\Cryptography;
|
||||
|
||||
use Base64Url\Base64Url;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Random\Randomizer;
|
||||
|
||||
class KeyGenerator
|
||||
{
|
||||
private readonly Randomizer $randomizer;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->randomizer = new Randomizer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{alg: string, ext: bool, k: string, key_ops: list<string>, kty: string}
|
||||
*/
|
||||
public function generateKey(string $algo = StoredObjectManagerInterface::ALGORITHM): array
|
||||
{
|
||||
if (StoredObjectManagerInterface::ALGORITHM !== $algo) {
|
||||
throw new \LogicException(sprintf("Algorithm '%s' is not supported.", $algo));
|
||||
}
|
||||
|
||||
$key = $this->randomizer->getBytes(32);
|
||||
|
||||
return [
|
||||
'alg' => 'A256CBC',
|
||||
'ext' => true,
|
||||
'k' => Base64Url::encode($key),
|
||||
'key_ops' => ['encrypt', 'decrypt'],
|
||||
'kty' => 'oct',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int<0, 255>>
|
||||
*/
|
||||
public function generateIv(): array
|
||||
{
|
||||
$iv = [];
|
||||
for ($i = 0; $i < 16; ++$i) {
|
||||
$iv[] = unpack('C', $this->randomizer->getBytes(8))[1];
|
||||
}
|
||||
|
||||
return $iv;
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
interface StoredObjectManagerInterface
|
||||
{
|
||||
public const ALGORITHM = 'AES-256-CBC';
|
||||
|
||||
/**
|
||||
* @param StoredObject|StoredObjectVersion $document if a StoredObject is given, the last version will be used
|
||||
*/
|
||||
|
@ -0,0 +1,160 @@
|
||||
<?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\AsyncUpload\Driver\LocalStorage;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\StoredObjectManager;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectManagerTest extends TestCase
|
||||
{
|
||||
private const CONTENT = 'abcde';
|
||||
|
||||
public function testWrite(): StoredObjectVersion
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$manager = $this->buildStoredObjectManager();
|
||||
$version = $manager->write($storedObject, self::CONTENT);
|
||||
|
||||
self::assertSame($storedObject, $version->getStoredObject());
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWrite
|
||||
*/
|
||||
public function testRead(StoredObjectVersion $version): StoredObjectVersion
|
||||
{
|
||||
$manager = $this->buildStoredObjectManager();
|
||||
$content = $manager->read($version);
|
||||
|
||||
self::assertEquals(self::CONTENT, $content);
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testRead
|
||||
*/
|
||||
public function testExists(StoredObjectVersion $version): StoredObjectVersion
|
||||
{
|
||||
$manager = $this->buildStoredObjectManager();
|
||||
$notExisting = new StoredObject();
|
||||
$versionNotPersisted = $notExisting->registerVersion();
|
||||
|
||||
self::assertTrue($manager->exists($version));
|
||||
self::assertFalse($manager->exists($versionNotPersisted));
|
||||
self::assertFalse($manager->exists(new StoredObject()));
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Chill\DocStoreBundle\Exception\StoredObjectManagerException
|
||||
*
|
||||
* @depends testExists
|
||||
*/
|
||||
public function testEtag(StoredObjectVersion $version): StoredObjectVersion
|
||||
{
|
||||
$manager = $this->buildStoredObjectManager();
|
||||
$actual = $manager->etag($version);
|
||||
|
||||
self::assertEquals(md5(self::CONTENT), $actual);
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testEtag
|
||||
*/
|
||||
public function testGetContentLength(StoredObjectVersion $version): StoredObjectVersion
|
||||
{
|
||||
$manager = $this->buildStoredObjectManager();
|
||||
|
||||
$actual = $manager->getContentLength($version);
|
||||
|
||||
self::assertSame(5, $actual);
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Chill\DocStoreBundle\Exception\StoredObjectManagerException
|
||||
*
|
||||
* @depends testGetContentLength
|
||||
*/
|
||||
public function testGetLastModified(StoredObjectVersion $version): StoredObjectVersion
|
||||
{
|
||||
$manager = $this->buildStoredObjectManager();
|
||||
$actual = $manager->getLastModified($version);
|
||||
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $actual);
|
||||
self::assertGreaterThan((new \DateTimeImmutable('now'))->getTimestamp() - 10, $actual->getTimestamp());
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testGetLastModified
|
||||
*/
|
||||
public function testDelete(StoredObjectVersion $version): void
|
||||
{
|
||||
$manager = $this->buildStoredObjectManager();
|
||||
$manager->delete($version);
|
||||
|
||||
self::assertFalse($manager->exists($version));
|
||||
}
|
||||
|
||||
public function testDeleteDoesNotRemoveOlderVersion(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$manager = $this->buildStoredObjectManager();
|
||||
$version1 = $manager->write($storedObject, 'version1');
|
||||
$version2 = $manager->write($storedObject, 'version2');
|
||||
$version3 = $manager->write($storedObject, 'version3');
|
||||
|
||||
self::assertTrue($manager->exists($version1));
|
||||
self::assertEquals('version1', $manager->read($version1));
|
||||
self::assertTrue($manager->exists($version2));
|
||||
self::assertEquals('version2', $manager->read($version2));
|
||||
self::assertTrue($manager->exists($version3));
|
||||
self::assertEquals('version3', $manager->read($version3));
|
||||
|
||||
// we delete the intermediate version
|
||||
$manager->delete($version2);
|
||||
|
||||
self::assertFalse($manager->exists($version2));
|
||||
// we check that we are still able to download the other versions
|
||||
self::assertTrue($manager->exists($version1));
|
||||
self::assertEquals('version1', $manager->read($version1));
|
||||
self::assertTrue($manager->exists($version3));
|
||||
self::assertEquals('version3', $manager->read($version3));
|
||||
}
|
||||
|
||||
private function buildStoredObjectManager(): StoredObjectManager
|
||||
{
|
||||
return new StoredObjectManager(
|
||||
new ParameterBag(['chill_doc_store' => ['local_storage' => ['storage_path' => '/tmp/chill-local-storage-test']]]),
|
||||
new KeyGenerator(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,238 @@
|
||||
<?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\AsyncUpload\Driver\LocalStorage;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\TempUrlLocalStorageGenerator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TempUrlLocalStorageGeneratorTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private const SECRET = 'abc';
|
||||
|
||||
public function testGenerate(): void
|
||||
{
|
||||
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||
$urlGenerator->generate('chill_docstore_stored_object_operate', [
|
||||
'object_name' => $object_name = 'testABC',
|
||||
'exp' => $expiration = 1734307200 + 180,
|
||||
'sig' => TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name, $expiration),
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
->shouldBeCalled()
|
||||
->willReturn($url = 'http://example.com/public/doc-store/stored-object/operate/testABC');
|
||||
|
||||
$generator = $this->buildGenerator($urlGenerator->reveal());
|
||||
|
||||
$signedUrl = $generator->generate('GET', $object_name);
|
||||
|
||||
self::assertEquals($url, $signedUrl->url);
|
||||
self::assertEquals($object_name, $signedUrl->object_name);
|
||||
self::assertEquals($expiration, $signedUrl->expires->getTimestamp());
|
||||
self::assertEquals('GET', $signedUrl->method);
|
||||
}
|
||||
|
||||
public function testGeneratePost(): void
|
||||
{
|
||||
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||
$urlGenerator->generate('chill_docstore_storedobject_post', [
|
||||
'prefix' => 'prefixABC',
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
->shouldBeCalled()
|
||||
->willReturn($url = 'http://example.com/public/doc-store/stored-object/prefixABC');
|
||||
|
||||
$generator = $this->buildGenerator($urlGenerator->reveal());
|
||||
|
||||
$signedUrl = $generator->generatePost(object_name: 'prefixABC');
|
||||
|
||||
self::assertEquals($url, $signedUrl->url);
|
||||
self::assertEquals('prefixABC', $signedUrl->object_name);
|
||||
self::assertEquals($expiration = 1734307200 + 180 + 180, $signedUrl->expires->getTimestamp());
|
||||
self::assertEquals('POST', $signedUrl->method);
|
||||
self::assertEquals(TempUrlLocalStorageGeneratorTest::expectedSignature('POST', 'prefixABC', $expiration), $signedUrl->signature);
|
||||
}
|
||||
|
||||
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(
|
||||
self::SECRET,
|
||||
$clock ?? new MockClock('2024-12-16T00:00:00+00:00'),
|
||||
$urlGenerator ?? $this->prophesize(UrlGeneratorInterface::class)->reveal(),
|
||||
);
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AsyncUpload\Command;
|
||||
namespace Chill\DocStoreBundle\Tests\AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Command\ConfigureOpenstackObjectStorageCommand;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\ConfigureOpenstackObjectStorageCommand;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
@ -9,13 +9,13 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
namespace Chill\DocStoreBundle\Tests\AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\StoredObjectManager;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManager;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
@ -27,7 +27,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @covers \Chill\DocStoreBundle\Service\StoredObjectManager
|
||||
* @covers \Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\StoredObjectManager
|
||||
*/
|
||||
final class StoredObjectManagerTest extends TestCase
|
||||
{
|
@ -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
|
@ -0,0 +1,47 @@
|
||||
<?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\Service\Cryptography;
|
||||
|
||||
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class KeyGeneratorTest extends TestCase
|
||||
{
|
||||
public function testGenerateKey(): void
|
||||
{
|
||||
$keyGenerator = new KeyGenerator();
|
||||
|
||||
$key = $keyGenerator->generateKey();
|
||||
|
||||
self::assertNotEmpty($key['k']);
|
||||
self::assertEquals('A256CBC', $key['alg']);
|
||||
}
|
||||
|
||||
public function testGenerateIv(): void
|
||||
{
|
||||
$keyGenerator = new KeyGenerator();
|
||||
|
||||
$actual = $keyGenerator->generateIv();
|
||||
|
||||
self::assertCount(16, $actual);
|
||||
foreach ($actual as $value) {
|
||||
self::assertIsInt($value);
|
||||
self::assertGreaterThanOrEqual(0, $value);
|
||||
self::assertLessThan(256, $value);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ services:
|
||||
tags:
|
||||
- { name: doctrine.repository_service }
|
||||
|
||||
|
||||
Chill\DocStoreBundle\Security\Authorization\:
|
||||
resource: "./../Security/Authorization"
|
||||
|
||||
@ -51,11 +50,9 @@ services:
|
||||
Chill\DocStoreBundle\AsyncUpload\Driver\:
|
||||
resource: '../AsyncUpload/Driver/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage\TempUrlLocalStorageGenerator:
|
||||
arguments:
|
||||
$secret: '%kernel.secret%'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Templating\:
|
||||
resource: '../AsyncUpload/Templating/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Command\:
|
||||
resource: '../AsyncUpload/Command/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface:
|
||||
alias: Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator
|
||||
|
@ -13,7 +13,6 @@ namespace Chill\WopiBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManager;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\WopiBundle\Service\WopiConverter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@ -26,9 +25,6 @@ class ConvertController
|
||||
{
|
||||
private const LOG_PREFIX = '[convert] ';
|
||||
|
||||
/**
|
||||
* @param StoredObjectManager $storedObjectManager
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
|
Loading…
x
Reference in New Issue
Block a user