diff --git a/composer.json b/composer.json index 16e4b0fa00e59c244f33d18be3cc0ee7e3834a2c..01b2d96671848da126d974a01c80d5b0c0b6c8d0 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,8 @@ "dbp/relay-core-bundle": "^0.1.56", "fgrosse/phpasn1": "^2.0", "symfony/event-dispatcher": "^5.4", - "symfony/framework-bundle": "^5.4", "symfony/mailer": "^5.4", + "symfony/mime": "^5.4", "symfony/orm-pack": "^2.2", "web-token/jwt-checker": "^2.1", "web-token/jwt-core": "^2.1", @@ -72,5 +72,11 @@ "coverage": [ "@php -dxdebug.mode=coverage vendor/bin/phpunit --coverage-html _coverage" ] - } + }, + "repositories": [ + { + "type": "vcs", + "url": "git@gitlab.tugraz.at:dbp/relay/dbp-relay-blob-connector-filesystem-bundle.git" + } + ] } diff --git a/composer.lock b/composer.lock index c8de48a3998f3fccd8eec83f4c1a31d585c8aa82..9cfeb00cccebe905e4f84ca3b7b6e3205929e9c9 100644 --- a/composer.lock +++ b/composer.lock @@ -9191,6 +9191,78 @@ ], "time": "2022-02-24T20:20:32+00:00" }, + { + "name": "dbp/relay-blob-connector-filesystem-bundle", + "version": "dev-main", + "source": { + "type": "git", + "url": "git@gitlab.tugraz.at:dbp/relay/dbp-relay-blob-connector-filesystem-bundle.git", + "reference": "d948924db59ad807c0544f35aaa4984a7ef2a94b" + }, + "require": { + "api-platform/core": "^2.6", + "dbp/relay-blob-bundle": "dev-main", + "dbp/relay-core-bundle": "^v0.1.56", + "ext-json": "*", + "php": ">=7.3", + "symfony/framework-bundle": "^5.4" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/phpstan": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpstan/phpstan-symfony": "^1.2", + "phpunit/phpunit": "^9", + "symfony/browser-kit": "^5.4", + "symfony/http-client": "^5.4", + "symfony/monolog-bundle": "^3.7", + "symfony/phpunit-bridge": "^5.4", + "vimeo/psalm": "^4.2.1" + }, + "default-branch": true, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Dbp\\Relay\\BlobConnectorFilesystemBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Dbp\\Relay\\BlobConnectorFilesystemBundle\\Tests\\": "tests/" + } + }, + "scripts": { + "test": [ + "@php vendor/bin/phpunit" + ], + "phpstan": [ + "@php vendor/bin/phpstan analyze --ansi" + ], + "psalm": [ + "@php vendor/bin/psalm" + ], + "lint": [ + "@composer run cs", + "@composer run phpstan", + "@composer run psalm" + ], + "cs-fix": [ + "@php vendor/bin/php-cs-fixer --ansi fix" + ], + "cs": [ + "@php vendor/bin/php-cs-fixer --ansi fix --dry-run --diff" + ], + "coverage": [ + "@php -dxdebug.mode=coverage vendor/bin/phpunit --coverage-html _coverage" + ] + }, + "license": [ + "AGPL-3.0-or-later" + ], + "description": "A template bundle for the Relay API gateway", + "time": "2022-12-19T00:08:14+00:00" + }, { "name": "dnoegel/php-xdg-base-dir", "version": "v0.1.1", @@ -12310,7 +12382,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "dbp/relay-blob-connector-filesystem-bundle": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Controller/CreateFileDataAction.php b/src/Controller/CreateFileDataAction.php index da26c2051ec119175476ceffd6a7445c5233aba6..483ba927f2e8e99a552f318326089efaced4ae99 100644 --- a/src/Controller/CreateFileDataAction.php +++ b/src/Controller/CreateFileDataAction.php @@ -23,6 +23,7 @@ final class CreateFileDataAction extends BaseBlobController public function __construct(BlobService $blobService) { $this->blobService = $blobService; +// dump('CreateFileDataAction::__construct()'); } /** @@ -31,13 +32,16 @@ final class CreateFileDataAction extends BaseBlobController */ public function __invoke(Request $request): FileData { - $sig = $request->headers->get('x-dbp-signature',''); +// dump('CreateFileDataAction::invoke()'); + $sig = $request->headers->get('x-dbp-signature', ''); if (!$sig) { throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, 'Signature missing', 'blob:createFileData-missing-sig'); } - $bucketId = (string) $request->query->get('bucketID', ''); - $creationTime = (string) $request->query->get('creationTime', ''); + $bucketId = $request->query->get('bucketID', ''); + assert(is_string($bucketId)); + $creationTime = $request->query->get('creationTime', 0); $prefix = $request->query->get('prefix', ''); + assert(is_string($prefix)); if (!$bucketId || !$creationTime) { throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'Signature cannot checked', 'blob:createFileData-unset-sig-params'); @@ -50,14 +54,14 @@ final class CreateFileDataAction extends BaseBlobController $secret = $bucket->getPublicKey(); $data = DenyAccessUnlessCheckSignature::verify($secret, $sig); - dump($data); +// dump($data); // check if signed params aer equal to request params if ($data['bucketID'] !== $bucketId) { dump($data['bucketID'], $bucketId); throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'BucketId change forbidden', 'blob:bucketid-change-forbidden'); } - if ((int)$data['creationTime'] !== (int)$creationTime) { + if ((int) $data['creationTime'] !== (int) $creationTime) { dump($data['creationTime'], $creationTime); throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'Creation Time change forbidden', 'blob:creationtime-change-forbidden'); } @@ -74,6 +78,10 @@ final class CreateFileDataAction extends BaseBlobController // Set exists until time $fileData->setExistsUntil($fileData->getDateCreated()->add(new \DateInterval($fileData->getRetentionDuration()))); + // Set everything else... + $fileData->setFileName($data['fileName']); + $fileData->setNotifyEmail($data['notifyEmail'] ?? ''); + $fileData->setAdditionalMetadata($data['additionalMetadata'] ?? ''); // Use given service for bucket if (!$bucket->getService()) { @@ -82,7 +90,7 @@ final class CreateFileDataAction extends BaseBlobController /** @var ?UploadedFile $uploadedFile */ $uploadedFile = $fileData->getFile(); - $fileData->setExtension($uploadedFile->guessExtension()); + $fileData->setExtension($uploadedFile->guessExtension() ?? substr($fileData->getFileName(), -3, 3)); $hash = hash('sha256', $uploadedFile->getContent()); // check hash of file @@ -93,7 +101,7 @@ final class CreateFileDataAction extends BaseBlobController // Check quota $bucketsizeByte = (int) $this->blobService->getQuotaOfBucket($fileData->getBucketID())['bucketSize']; - $bucketQuotaByte = $fileData->getBucket()->getQuota() * 1024 *1024; // Convert mb to Byte + $bucketQuotaByte = $fileData->getBucket()->getQuota() * 1024 * 1024; // Convert mb to Byte $newBucketSizeByte = $bucketsizeByte + $fileData->getFileSize(); if ($newBucketSizeByte > $bucketQuotaByte) { $this->blobService->sendNotifyQuota($bucket); diff --git a/src/Controller/DeleteFileDatasByPrefix.php b/src/Controller/DeleteFileDatasByPrefix.php index 94f2cfd77abb43d7d7fae7315b182320813080ba..deecc53cbf270a08a81421e85408778183f4b8c5 100644 --- a/src/Controller/DeleteFileDatasByPrefix.php +++ b/src/Controller/DeleteFileDatasByPrefix.php @@ -28,9 +28,12 @@ class DeleteFileDatasByPrefix extends BaseBlobController if (!$sig) { throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, 'Signature missing', 'blob:createFileData-missing-sig'); } + $bucketId = $request->query->get('bucketID', ''); - $creationTime = $request->query->get('creationTime', ''); + assert(is_string($bucketId)); + $creationTime = $request->query->get('creationTime', 0); $prefix = $request->query->get('prefix', ''); + assert(is_string($prefix)); if (!$bucketId || !$creationTime || !$prefix) { throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'Signature cannot checked', 'blob:delete-files-per-prefix-unset-sig-params'); @@ -43,13 +46,13 @@ class DeleteFileDatasByPrefix extends BaseBlobController $secret = $bucket->getPublicKey(); $data = DenyAccessUnlessCheckSignature::verify($secret, $sig); - dump($data); +// dump($data); // check if signed params are equal to request params if ($data['bucketID'] !== $bucketId) { throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'BucketId change forbidden', 'blob:bucketid-change-forbidden'); } - if ((int)$data['creationTime'] !== (int)$creationTime) { + if ((int) $data['creationTime'] !== (int) $creationTime) { throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'Creation Time change forbidden', 'blob:creationtime-change-forbidden'); } if ($data['prefix'] !== $prefix) { diff --git a/src/DataPersister/FileDataDataPersister.php b/src/DataPersister/FileDataDataPersister.php index c75f91ac499a804d7a3e16f5182ee35306cc7a90..c68d8294fbbde3adbd61b5aec9759970499cf77f 100644 --- a/src/DataPersister/FileDataDataPersister.php +++ b/src/DataPersister/FileDataDataPersister.php @@ -28,6 +28,11 @@ class FileDataDataPersister extends AbstractController implements ContextAwareDa return $data instanceof FileData; } + /** + * @param Filedata $data + * + * @return FileData|void + */ public function persist($data, array $context = []) { // no need to check, because signature is checked by getting the data @@ -55,8 +60,6 @@ class FileDataDataPersister extends AbstractController implements ContextAwareDa /** * @param mixed $data - * @param array $context - * @return void */ public function remove($data, array $context = []): void { diff --git a/src/DataProvider/FileDataDataProvider.php b/src/DataProvider/FileDataDataProvider.php index a05a93391481e344072a2d8afeff0471009fcd20..427f34163d0fabdae9cc3290bc306f93affa8ba6 100644 --- a/src/DataProvider/FileDataDataProvider.php +++ b/src/DataProvider/FileDataDataProvider.php @@ -53,11 +53,12 @@ class FileDataDataProvider extends AbstractDataProvider protected function getFileDataById($id, array $filters): object { - $sig = $this->requestStack->getCurrentRequest()->headers->get('x-dbp-signature',''); + $sig = $this->requestStack->getCurrentRequest()->headers->get('x-dbp-signature', ''); if (!$sig) { throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, 'Signature missing', 'blob:createFileData-missing-sig'); } $bucketId = $filters['bucketID'] ?? ''; + assert(is_string($bucketId)); if (!$bucketId) { throw ApiError::withDetails(Response::HTTP_BAD_REQUEST, 'BucketID is missing', 'blob:get-files-by-prefix-missing-bucketID'); } @@ -89,11 +90,12 @@ class FileDataDataProvider extends AbstractDataProvider protected function getPage(int $currentPageNumber, int $maxNumItemsPerPage, array $filters = [], array $options = []): array { - $sig = $this->requestStack->getCurrentRequest()->headers->get('x-dbp-signature',''); + $sig = $this->requestStack->getCurrentRequest()->headers->get('x-dbp-signature', ''); if (!$sig) { throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, 'Signature missing', 'blob:createFileData-missing-sig'); } $bucketId = $filters['bucketID'] ?? ''; + assert(is_string($bucketId)); if (!$bucketId) { throw ApiError::withDetails(Response::HTTP_BAD_REQUEST, 'BucketID is missing', 'blob:get-files-by-prefix-missing-bucketID'); } @@ -125,15 +127,13 @@ class FileDataDataProvider extends AbstractDataProvider } /** - * Check dbp-signature on GET request + * Check dbp-signature on GET request. * - * @param string $secret - * @param array $filters * @throws \JsonException */ private function checkSignature(string $secret, array $filters): void { - $sig = $this->requestStack->getCurrentRequest()->headers->get('x-dbp-signature',''); + $sig = $this->requestStack->getCurrentRequest()->headers->get('x-dbp-signature', ''); if (!$sig) { throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, 'Signature missing', 'blob:createFileData-missing-sig'); } @@ -145,18 +145,17 @@ class FileDataDataProvider extends AbstractDataProvider } $data = DenyAccessUnlessCheckSignature::verify($secret, $sig); - dump($data); +// dump($data); // check if signed params aer equal to request params if ($data['bucketID'] !== $bucketId) { dump($data['bucketID'], $bucketId); throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'BucketId change forbidden', 'blob:bucketid-change-forbidden'); } - if ((int)$data['creationTime'] !== (int)$creationTime) { + if ((int) $data['creationTime'] !== (int) $creationTime) { dump($data['creationTime'], $creationTime); throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'Creation Time change forbidden', 'blob:creationtime-change-forbidden'); } // TODO check if request is NOT too old - } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 7832154486958f43fb9fd8f1526e86dde3f3b98a..bdfb1e091824704fe8fca73f4d77e447ec185b5f 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -18,8 +18,8 @@ class Configuration implements ConfigurationInterface ->children() ->scalarNode('database_url') ->isRequired() - ->cannotBeEmpty() - ->defaultValue('%env(resolve:DATABASE_URL)%') + //->cannotBeEmpty() + ->defaultValue('%env(resolve:BLOB_DATABASE_NAME)%') ->end() ->arrayNode('buckets') ->isRequired() diff --git a/src/DependencyInjection/DbpRelayBlobExtension.php b/src/DependencyInjection/DbpRelayBlobExtension.php index 97e79704e177fe0447d8432e38774490e9a0c7dd..641ce210f2dbd8cfa61f595e192d165e8819555f 100644 --- a/src/DependencyInjection/DbpRelayBlobExtension.php +++ b/src/DependencyInjection/DbpRelayBlobExtension.php @@ -50,7 +50,7 @@ class DbpRelayBlobExtension extends ConfigurableExtension implements PrependExte 'dbal' => [ 'connections' => [ 'dbp_relay_blob_bundle' => [ - 'url' => $config['database_url'] ?? '', + 'url' => $config['database_url'] ?? 'sqlite:///var/dbp_relay_blob_test.db', ], ], ], diff --git a/src/Helper/DenyAccessUnlessCheckSignature.php b/src/Helper/DenyAccessUnlessCheckSignature.php index 0a92a8363661265666629529ac5faeb0f8a45290..82ab32c77a9a9690d2760e692b9b0801185c0804 100644 --- a/src/Helper/DenyAccessUnlessCheckSignature.php +++ b/src/Helper/DenyAccessUnlessCheckSignature.php @@ -18,10 +18,11 @@ use Symfony\Component\HttpFoundation\Response; class DenyAccessUnlessCheckSignature { /** - * Create a JWS token + * Create a JWS token. + * + * @param string $secret to create the (symmetric) JWK from + * @param array $payload to create the token from * - * @param string $secret to create the (symmetric) JWK from - * @param array $payload to create the token from * @throws \JsonException */ public static function create(string $secret, array $payload): string @@ -32,11 +33,13 @@ class DenyAccessUnlessCheckSignature } /** - * Verify a JWS token + * Verify a JWS token. * * @param string $secret to create the (symmetric) JWK from - * @param string $token to verify + * @param string $token to verify + * * @return array extracted payload from token + * * @throws \JsonException * @throws ApiError */ @@ -46,6 +49,7 @@ class DenyAccessUnlessCheckSignature $payload = []; if (!self::verifyToken($jwk, $token, $payload)) { + dump(['token' => $token, 'payload' => $payload, 'secret' => $secret]); throw ApiError::withDetails(Response::HTTP_FORBIDDEN, 'Signature invalid', 'blob:signature-invalid'); } @@ -56,7 +60,6 @@ class DenyAccessUnlessCheckSignature * Create the JWK from a shared secret. * * @param string $secret to create the (symmetric) JWK from - * @return JWK */ public static function createJWK(string $secret): JWK { @@ -72,9 +75,11 @@ class DenyAccessUnlessCheckSignature /** * Generate the token. * - * @param JWK $jwk json web key + * @param JWK $jwk json web key * @param array $payload as json string to secure + * * @return string secure token + * * @throws \JsonException */ public static function generateToken(JWK $jwk, array $payload): string @@ -94,10 +99,10 @@ class DenyAccessUnlessCheckSignature /** * Verify a JWS token. * - * @param JWK $jwk - * @param string $token the JWS token as string - * @param array $payload to extract from token on success + * @param string $token the JWS token as string + * @param array $payload to extract from token on success * @return bool + * * @throws \JsonException */ public static function verifyToken(JWK $jwk, string $token, array &$payload): bool @@ -110,6 +115,9 @@ class DenyAccessUnlessCheckSignature if ($ok = $jwsVerifier->verifyWithKey($jws, $jwk, 0)) { $payload = json_decode($jws->getPayload(), true, 512, JSON_THROW_ON_ERROR); } +// $ok = $jwsVerifier->verifyWithKey($jws, $jwk, 0); +// $payload = json_decode($jws->getPayload(), true, 512, JSON_THROW_ON_ERROR); + return $ok; } } diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index d84b7f47bebc29ff37e42b2f948357aebe2b086f..966b2d7beddb9c114e31f088e058780656cb8bc5 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -2,6 +2,8 @@ services: Dbp\Relay\BlobBundle\Service\BlobService: autowire: true autoconfigure: true + arguments: + $em: '@doctrine.orm.dbp_relay_blob_bundle_entity_manager' Dbp\Relay\BlobBundle\Controller\: tags: [ 'controller.service_arguments' ] diff --git a/src/Resources/config/services_test.yaml b/src/Resources/config/services_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1a6ccd30e99345c7983d1c624439576203f73990 --- /dev/null +++ b/src/Resources/config/services_test.yaml @@ -0,0 +1,29 @@ +services: + Dbp\Relay\BlobConnectorFilesystemBundle\Cron\CleanupCronJob: + autowire: true + autoconfigure: true + + Dbp\Relay\BlobConnectorFilesystemBundle\Service\FilesystemService: + autowire: true + autoconfigure: true + public: true + + Dbp\Relay\BlobConnectorFilesystemBundle\Service\ShareLinkPersistenceService: + autowire: true + autoconfigure: true + public: true + + Dbp\Relay\BlobConnectorFilesystemBundle\Service\ConfigurationService: + autowire: true + autoconfigure: true + + Dbp\Relay\BlobConnectorFilesystemBundle\Controller\: + tags: [ 'controller.service_arguments' ] + resource: '../../../../relay-blob-connector-filesystem-bundle/src/Controller/' + autowire: true + autoconfigure: true + + Dbp\Relay\BlobBundle\Tests\DummyFileSystemService: + autowire: true + autoconfigure: true + public: true diff --git a/src/Service/BlobService.php b/src/Service/BlobService.php index 685142209658bf5cd435f39d9869573bd301b420..55a7edb61fb9719e86fcb2b67feec51117b4c244 100644 --- a/src/Service/BlobService.php +++ b/src/Service/BlobService.php @@ -7,8 +7,8 @@ namespace Dbp\Relay\BlobBundle\Service; use Dbp\Relay\BlobBundle\Entity\Bucket; use Dbp\Relay\BlobBundle\Entity\FileData; use Dbp\Relay\CoreBundle\Exception\ApiError; +use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\Persistence\ManagerRegistry; use PHPUnit\TextUI\XmlConfiguration\File; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; @@ -39,16 +39,18 @@ class BlobService */ private $datasystemService; - public function __construct(ManagerRegistry $managerRegistry, ConfigurationService $configurationService, DatasystemProviderService $datasystemService) + public function __construct(EntityManagerInterface $em, ConfigurationService $configurationService, DatasystemProviderService $datasystemService) { - $manager = $managerRegistry->getManager('dbp_relay_blob_bundle'); - assert($manager instanceof EntityManagerInterface); - $this->em = $manager; - + $this->em = $em; $this->configurationService = $configurationService; $this->datasystemService = $datasystemService; } + public function setDatasystemService(DatasystemProviderService $datasystemService): void + { + $this->datasystemService = $datasystemService; + } + public function checkConnection() { $this->em->getConnection()->connect(); @@ -126,6 +128,7 @@ class BlobService $fileData->setLastAccess($time); try { +// dump($fileData); $this->em->persist($fileData); $this->em->flush(); } catch (\Exception $e) { @@ -350,7 +353,7 @@ class BlobService private function sendEmail(array $config, array $context) { - $loader = new FilesystemLoader(dirname(__FILE__).'/../Resources/views/'); + $loader = new FilesystemLoader(__DIR__ . '/../Resources/views/'); $twig = new Environment($loader); $template = $twig->load($config['html_template']); diff --git a/tests/CurlGetTest.php b/tests/CurlGetTest.php index 7fd8560d3aad63c0e46d8280eeb963a86fc322a7..29f24b1b2b3dbbe11048c17110d53260041ad78d 100644 --- a/tests/CurlGetTest.php +++ b/tests/CurlGetTest.php @@ -1,20 +1,82 @@ -<?php - -declare(strict_types=1); +<?php declare(strict_types=1); namespace Dbp\Relay\BlobBundle\Tests; +use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase; +use Dbp\Relay\BlobBundle\Controller\CreateFileDataAction; +use Dbp\Relay\BlobBundle\Controller\DeleteFileDatasByPrefix; +use Dbp\Relay\BlobBundle\Entity\FileData; use Dbp\Relay\BlobBundle\Helper\DenyAccessUnlessCheckSignature; +use Dbp\Relay\BlobBundle\Helper\PoliciesStruct; +use Dbp\Relay\BlobBundle\Service\BlobService; +use Dbp\Relay\BlobBundle\Service\ConfigurationService; +use Dbp\Relay\BlobBundle\Service\DatasystemProviderServiceInterface; +use Dbp\Relay\CoreBundle\TestUtils\UserAuthTrait; +use Doctrine\ORM\Tools\SchemaTool; use Exception; -use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\KernelInterface; +use function uuid_is_valid; -class CurlGetTest extends TestCase +class DummyFileSystemService implements DatasystemProviderServiceInterface { + public function saveFile(FileData &$fileData): ?FileData + { + return $fileData; + } + + public function renameFile(FileData &$fileData): ?FileData + { + return $fileData; + } + + public function getLink(FileData &$fileData, PoliciesStruct $policiesStruct): ?FileData + { + return $fileData; + } + + public function removeFile(FileData &$fileData): bool + { + return true; + } +} + +class CurlGetTest extends ApiTestCase +{ + use UserAuthTrait; + + /** @var \Doctrine\ORM\EntityManagerInterface $entityManager */ + private $entityManager; + + /** + * @throws Exception + */ + protected function setUp(): void + { + /** @var KernelInterface $kernel */ + $kernel = self::bootKernel(); + + if ('test' !== $kernel->getEnvironment()) { + throw new \RuntimeException('Execution only in Test environment possible!'); + } + + $this->entityManager = $kernel->getContainer()->get('doctrine.orm.entity_manager'); + $metaData = $this->entityManager->getMetadataFactory()->getAllMetadata(); + $schemaTool = new SchemaTool($this->entityManager); + $schemaTool->updateSchema($metaData); + } + public function testGet(): void { try { - $secret = '08d848fd868d83646778b87dd0695b10f59c78e23b286e9884504d1bb43cce93'; - $bucketId = '1234'; + $client = $this->withUser('foobar'); + $configService = $client->getContainer()->get(ConfigurationService::class); + + $bucket = $configService->getBuckets()[0]; + $secret = $bucket->getPublicKey(); + $bucketId = $bucket->getIdentifier(); $creationTime = date('U'); $prefix = 'playground'; $payload = [ @@ -23,32 +85,304 @@ class CurlGetTest extends TestCase 'prefix' => $prefix, ]; - $uri = "http://127.0.0.1:8000/blob/files/?bucketID=$bucketId&prefix=$prefix&creationTime=$creationTime"; + $token = DenyAccessUnlessCheckSignature::create($secret, $payload); + + $url = "/blob/files/?bucketID=$bucketId&prefix=$prefix&creationTime=$creationTime"; + $options = [ + 'headers' => [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'x-dbp-signature' => $token, + ], + ]; + + /** @noinspection PhpInternalEntityUsedInspection */ + $client->getKernelBrowser()->followRedirects(); + + /** @var Response $response */ + $response = $client->request('GET', $url, $options); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + $this->assertArrayHasKey('hydra:view', $data); + $this->assertArrayHasKey('hydra:member', $data); + $this->assertCount(0, $data['hydra:member'], 'More files than expected'); + } catch (\Throwable $e) { + echo $e->getTraceAsString() . "\n"; + $this->fail($e->getMessage()); + } + } + + /** + * Integration test for a full life cycle: create, use and destroy a blob + * - create blob no 1 + * - get all blobs: blob no 1 is available + * - create blob no 2 + * - get all blobs: two blobs are available + * - delete all blobs for the prefix: no entries in database + * - get all blobs: no blobs available + * + * @return void + */ + public function testPostGetDelete(): void + { + try { + + $files = [ + 0 => [ + 'name' => $n = 'Test.php', + 'path' => $p = __DIR__ . '/' . $n, + 'content' => $c = file_get_contents($p), + 'hash' => hash('sha256', $c), + 'size' => strlen($c), + 'mime' => 'application/x-php', + 'retention' => 'P1W', + ], + 1 => [ + 'name' => $n = 'Kernel.php', + 'path' => $p = __DIR__ . '/' . $n, + 'content' => $c = file_get_contents($p), + 'hash' => hash('sha256', $c), + 'size' => strlen($c), + 'mime' => 'application/x-php', + 'retention' => 'P1M', + ], + ]; + + $client = $this->withUser('foobar'); + $blobService = $client->getContainer()->get(BlobService::class); + $configService = $client->getContainer()->get(ConfigurationService::class); + + $bucket = $configService->getBuckets()[0]; + $secret = $bucket->getPublicKey(); + $bucketId = $bucket->getIdentifier(); + $creationTime = date('U'); + $prefix = 'playground'; + $notifyEmail = 'eugen.neuber@tugraz.at'; + + $url = "/blob/files/?bucketID=$bucketId&prefix=$prefix&creationTime=$creationTime"; + + // ======================================================= + // POST a file + // ======================================================= + $payload = [ + 'bucketID' => $bucketId, + 'creationTime' => $creationTime, + 'prefix' => $prefix, + 'fileName' => $files[0]['name'], + 'fileHash' => $files[0]['hash'], + 'notifyEmail' => $notifyEmail, + 'retentionDuration' => $files[0]['retention'], + 'additionalMetadata' => '', + ]; + + $token = DenyAccessUnlessCheckSignature::create($secret, $payload); + + $requestPost = Request::create($url, 'POST', [], [], + [ + 'file' => new UploadedFile($files[0]['path'], $files[0]['name'], $files[0]['mime']) + ], + [ + 'HTTP_ACCEPT' => 'application/ld+json', +// 'x-dbp-signature' => $token, + 'HTTP_X_DBP_SIGNATURE' => $token, + ], + "HTTP_ACCEPT: application/ld+json\r\n" + . "HTTP_X_DBP_SIGNATURE: $token\r\n\r\n" + . "file=" . base64_encode($files[0]['content']) + . "&fileName={$files[0]['name']}&prefix=$prefix&bucketID=$bucketId" + ); + $c = new CreateFileDataAction($blobService); + try { + $fileData = $c->__invoke($requestPost); + } catch (\Throwable $e) { + echo $e->getTraceAsString() . "\n"; + $this->fail($e->getMessage()); + } + + $this->assertNotNull($fileData); + $this->assertEquals($prefix, $fileData->getPrefix(), 'File data prefix not correct.'); + $this->assertObjectHasAttribute('identifier', $fileData, 'File data has no identifier.'); + $this->assertTrue(uuid_is_valid($fileData->getIdentifier()), 'File data identifier is not a valid UUID.'); + $this->assertEquals($files[0]['name'], $fileData->getFileName(), 'File name not correct.'); + $files[0]['uuid'] = $fileData->getIdentifier(); + $files[0]['created'] = $fileData->getDateCreated(); + $files[0]['until'] = $fileData->getExistsUntil(); + $this->assertEquals( + $files[0]['created']->format('c'), + date('c', (int)$creationTime), + 'File creation time not correct.' + ); + + // ======================================================= + // GET a file + // ======================================================= + $options = [ + 'headers' => [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'x-dbp-signature' => $token, + ], + ]; + + /** @noinspection PhpInternalEntityUsedInspection */ + $client->getKernelBrowser()->followRedirects(); + + $response = $client->request('GET', $url, $options); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + $this->assertArrayHasKey('hydra:view', $data); + $this->assertArrayHasKey('hydra:member', $data); + $this->assertArrayHasKey(0, $data['hydra:member']); + $resultFile = $data['hydra:member'][0]; + $this->assertEquals($prefix, $resultFile['prefix'], 'File data prefix not correct.'); + $this->assertEquals($files[0]['name'], $resultFile['fileName'], 'File name not correct.'); + $this->assertEquals($files[0]['size'], $resultFile['fileSize'], 'File size not correct.'); + $this->assertEquals($files[0]['uuid'], $resultFile['identifier'], 'File identifier not correct.'); + $this->assertEquals($notifyEmail, $resultFile['notifyEmail'], 'File data notify email not correct.'); + $this->assertCount(1, $data['hydra:member'], 'More files than expected'); +// dump($data); + + // ======================================================= + // POST another file + // ======================================================= + $payload = [ + 'bucketID' => $bucketId, + 'creationTime' => $creationTime, + 'prefix' => $prefix, + 'fileName' => $files[1]['name'], + 'fileHash' => $files[1]['hash'], + 'notifyEmail' => $notifyEmail, + 'retentionDuration' => $files[1]['retention'], + 'additionalMetadata' => '', + ]; $token = DenyAccessUnlessCheckSignature::create($secret, $payload); -// echo "Signatur: $token\n"; + $requestPost = Request::create($url, 'POST', [], [], + [ + 'file' => new UploadedFile($files[1]['path'], $files[1]['name'], $files[1]['mime']) + ], + [ + 'HTTP_ACCEPT' => 'application/ld+json', +// 'x-dbp-signature' => $token, + 'HTTP_X_DBP_SIGNATURE' => $token, + ], + "HTTP_ACCEPT: application/ld+json\r\n" + . "HTTP_X_DBP_SIGNATURE: $token\r\n\r\n" + . "file=" . base64_encode($files[1]['content']) + . "&fileName={$files[1]['name']}&prefix=$prefix&bucketID=$bucketId" + ); + $c = new CreateFileDataAction($blobService); + try { + $fileData = $c->__invoke($requestPost); + } catch (\Throwable $e) { + echo $e->getTraceAsString() . "\n"; + $this->fail($e->getMessage()); + } + + $this->assertNotNull($fileData); + $this->assertEquals($prefix, $fileData->getPrefix(), 'File data prefix not correct.'); + $this->assertObjectHasAttribute('identifier', $fileData, 'File data has no identifier.'); + $this->assertTrue(uuid_is_valid($fileData->getIdentifier()), 'File data identifier is not a valid UUID.'); + $this->assertEquals($files[1]['name'], $fileData->getFileName(), 'File name not correct.'); + $files[1]['uuid'] = $fileData->getIdentifier(); + $files[1]['created'] = $fileData->getDateCreated(); + $files[1]['until'] = $fileData->getExistsUntil(); + dump($fileData); - $header = [ - 'x-dbp-signature: '.$token, + // ======================================================= + // GET all files + // ======================================================= + $options = [ + 'headers' => [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'x-dbp-signature' => $token, + ], ]; - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $uri); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_NOBODY, 0); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); - $result = curl_exec($ch); - $info = curl_getinfo($ch); - curl_close($ch); - $data = json_decode($result, true, 512, JSON_THROW_ON_ERROR); -// print_r($result); - - $this->assertEquals(200, $info['http_code']); + + /** @noinspection PhpInternalEntityUsedInspection */ + $client->getKernelBrowser()->followRedirects(); + + $response = $client->request('GET', $url, $options); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + $this->assertArrayHasKey('hydra:view', $data); + $this->assertArrayHasKey('hydra:member', $data); + $this->assertCount(2, $data['hydra:member'], 'More files than expected'); + foreach ($data['hydra:member'] as $resultFile) { + $found = false; + foreach ($files as $file) { + if ($file['uuid'] === $resultFile['identifier']) { + $found = true; + $this->assertEquals($prefix, $resultFile['prefix'], 'File prefix not correct.'); + $this->assertEquals($file['name'], $resultFile['fileName'], 'File name not correct.'); + $this->assertEquals($file['size'], $resultFile['fileSize'], 'File size not correct.'); + $this->assertEquals( + $file['created']->format('c'), + date('c', (int)$creationTime), + 'File creation time not correct.' + ); + $until = $file['created']->add(new \DateInterval($file['retention'])); + dump([$until->format('c'), $resultFile['existsUntil']]); + $this->assertEquals( + $until->format('c'), + $resultFile['existsUntil'], + 'File retention time not correct.' + ); + + break; + } + } + $this->assertTrue($found, 'Uploaded file not found.'); + } +// dump($data['hydra:member']); +// dump($data); + + // ======================================================= + // DELETE all files + // ======================================================= + $requestDelete = Request::create($url, 'DELETE', [], [], [], + [ + 'HTTP_ACCEPT' => 'application/ld+json', + 'HTTP_X_DBP_SIGNATURE' => $token, + ], + "HTTP_ACCEPT: application/ld+json\r\n" + . "HTTP_X_DBP_SIGNATURE: $token\r\n\r\n" + ); + $d = new DeleteFileDatasByPrefix($blobService); + try { + $d->__invoke($requestDelete); + } catch (\Throwable $e) { + echo $e->getTraceAsString() . "\n"; + $this->fail($e->getMessage()); + } + + $query = $this->entityManager->getConnection()->createQueryBuilder(); + $files = $query->select('*') + ->from('blob_files') + ->where("prefix = '$prefix' AND bucket_id = '$bucketId'") + ->fetchAllAssociativeIndexed(); + $this->assertEmpty($files, 'Files not deleted'); + + // ======================================================= + // GET all files + // ======================================================= + /** @var Response $response */ + $response = $client->request('GET', $url, $options); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); $this->assertArrayHasKey('hydra:view', $data); - } catch (Exception $e) { + $this->assertArrayHasKey('hydra:member', $data); + $this->assertCount(0, $data['hydra:member'], 'More files than expected'); + + } catch (\Throwable $e) { + echo $e->getTraceAsString() . "\n"; $this->fail($e->getMessage()); } } diff --git a/tests/Kernel.php b/tests/Kernel.php index 50044909f8cb2d991bcdad9940a2e9e33e9d0d23..8acf6f1bbc45241392da5c5f7eceb457e0a687d8 100644 --- a/tests/Kernel.php +++ b/tests/Kernel.php @@ -7,6 +7,8 @@ namespace Dbp\Relay\BlobBundle\Tests; use ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle; use Dbp\Relay\BlobBundle\DbpRelayBlobBundle; use Dbp\Relay\CoreBundle\DbpRelayCoreBundle; +use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; use Nelmio\CorsBundle\NelmioCorsBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; @@ -29,6 +31,8 @@ class Kernel extends BaseKernel yield new TwigBundle(); yield new NelmioCorsBundle(); yield new MonologBundle(); + yield new DoctrineBundle(); + yield new DoctrineMigrationsBundle(); yield new ApiPlatformBundle(); yield new DbpRelayBlobBundle(); yield new DbpRelayCoreBundle(); @@ -42,11 +46,49 @@ class Kernel extends BaseKernel protected function configureContainer(ContainerConfigurator $container, LoaderInterface $loader) { $container->import('@DbpRelayCoreBundle/Resources/config/services_test.yaml'); + $container->import('@DbpRelayBlobBundle/Resources/config/services_test.yaml'); $container->extension('framework', [ 'test' => true, 'secret' => '', ]); - $container->extension('dbp_relay_blob', []); + $container->extension('dbp_relay_blob', [ + 'database_url' => 'sqlite:///var/dbp_relay_blob_test.db', + 'buckets' => [ + 'test_bucket' => [ + 'service' => 'Dbp\Relay\BlobBundle\Tests\DummyFileSystemService', + 'bucket_id' => '1234', + 'bucket_name' => 'Test bucket', + 'public_key' => '08d848fd868d83646778b87dd0695b10f59c78e23b286e9884504d1bb43cce93', + 'path' => 'testpath', + 'quota' => 500, // in MB + 'bucket_owner' => 'tamara.steiwnender@tugraz.at', + 'max_retention_duration' => 'P1Y', + 'link_expire_time' => 'P7D', + 'policies' => [ + 'create' => true, + 'delete' => true, + 'open' => true, + 'download' => true, + 'rename' => true, + 'work' => true, + ], + 'notify_quota' => [ + 'dsn' => 'smtp:localhost', + 'from' => 'noreply@tugraz.at', + 'to' => 'tamara.steinwender@tugraz.at', + 'subject' => 'Blob notify quota', + 'html_template' => 'emails/notify-quota.html.twig', + ], + 'reporting' => [ + 'dsn' => 'smtp:localhost', + 'from' => 'noreply@tugraz.at', + 'to' => 'tamara.steinwender@tugraz.at', + 'subject' => 'Blob file deletion reporting', + 'html_template' => 'emails/reporting.html.twig', + ], + ], + ], + ]); } }