diff --git a/appinfo/routes.php b/appinfo/routes.php index aa2cdd25..506e2398 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -26,20 +26,20 @@ return [ 'ocs' => [ - ['name' => 'RequestHandler#setPrivateKey', 'url' => '/api/v1/private-key', 'verb' => 'POST'], - ['name' => 'RequestHandler#getPrivateKey', 'url' => '/api/v1/private-key', 'verb' => 'GET'], - ['name' => 'RequestHandler#deletePrivateKey', 'url' => '/api/v1/private-key', 'verb' => 'DELETE'], - ['name' => 'RequestHandler#createPublicKey', 'url' => '/api/v1/public-key', 'verb' => 'POST'], - ['name' => 'RequestHandler#getPublicKeys', 'url' => '/api/v1/public-key', 'verb' => 'GET'], - ['name' => 'RequestHandler#deletePublicKey', 'url' => '/api/v1/public-key', 'verb' => 'DELETE'], - ['name' => 'RequestHandler#getPublicServerKey', 'url' => '/api/v1/server-key', 'verb' => 'GET'], - ['name' => 'RequestHandler#setMetaData', 'url' => '/api/v1/meta-data/{id}', 'verb' => 'POST'], - ['name' => 'RequestHandler#getMetaData', 'url' => '/api/v1/meta-data/{id}', 'verb' => 'GET'], - ['name' => 'RequestHandler#updateMetaData', 'url' => '/api/v1/meta-data/{id}', 'verb' => 'PUT'], - ['name' => 'RequestHandler#deleteMetaData', 'url' => '/api/v1/meta-data/{id}', 'verb' => 'DELETE'], - ['name' => 'RequestHandler#setEncryptionFlag', 'url' => '/api/v1/encrypted/{id}', 'verb' => 'PUT'], - ['name' => 'RequestHandler#removeEncryptionFlag', 'url' => '/api/v1/encrypted/{id}', 'verb' => 'DELETE'], - ['name' => 'RequestHandler#lockFolder', 'url' => '/api/v1/lock/{id}', 'verb' => 'POST'], - ['name' => 'RequestHandler#unlockFolder', 'url' => '/api/v1/lock/{id}', 'verb' => 'DELETE'], + ['name' => 'Key#setPrivateKey', 'url' => '/api/v1/private-key', 'verb' => 'POST'], + ['name' => 'Key#getPrivateKey', 'url' => '/api/v1/private-key', 'verb' => 'GET'], + ['name' => 'Key#deletePrivateKey', 'url' => '/api/v1/private-key', 'verb' => 'DELETE'], + ['name' => 'Key#createPublicKey', 'url' => '/api/v1/public-key', 'verb' => 'POST'], + ['name' => 'Key#getPublicKeys', 'url' => '/api/v1/public-key', 'verb' => 'GET'], + ['name' => 'Key#deletePublicKey', 'url' => '/api/v1/public-key', 'verb' => 'DELETE'], + ['name' => 'Key#getPublicServerKey', 'url' => '/api/v1/server-key', 'verb' => 'GET'], + ['name' => 'MetaData#setMetaData', 'url' => '/api/v1/meta-data/{id}', 'verb' => 'POST'], + ['name' => 'MetaData#getMetaData', 'url' => '/api/v1/meta-data/{id}', 'verb' => 'GET'], + ['name' => 'MetaData#updateMetaData', 'url' => '/api/v1/meta-data/{id}', 'verb' => 'PUT'], + ['name' => 'MetaData#deleteMetaData', 'url' => '/api/v1/meta-data/{id}', 'verb' => 'DELETE'], + ['name' => 'Encryption#setEncryptionFlag', 'url' => '/api/v1/encrypted/{id}', 'verb' => 'PUT'], + ['name' => 'Encryption#removeEncryptionFlag', 'url' => '/api/v1/encrypted/{id}', 'verb' => 'DELETE'], + ['name' => 'Locking#lockFolder', 'url' => '/api/v1/lock/{id}', 'verb' => 'POST'], + ['name' => 'Locking#unlockFolder', 'url' => '/api/v1/lock/{id}', 'verb' => 'DELETE'], ], ]; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 74b312b8..2c7491fd 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -32,6 +32,7 @@ use OCA\EndToEndEncryption\IMetaDataStorage; use OCA\EndToEndEncryption\KeyStorage; use OCA\EndToEndEncryption\MetaDataStorage; +use OCA\EndToEndEncryption\Middleware\UserAgentCheckMiddleware; use OCA\EndToEndEncryption\UserManager; use OCA\Files_Trashbin\Events\MoveToTrashEvent; use OCA\Files_Versions\Events\CreateVersionEvent; @@ -55,6 +56,7 @@ public function __construct(array $urlParams = []) { $container->registerAlias(IMetaDataStorage::class, MetaDataStorage::class); $container->registerCapability(Capabilities::class); + $container->registerMiddleWare(UserAgentCheckMiddleware::class); } public function registerEvents():void { diff --git a/lib/Controller/EncryptionController.php b/lib/Controller/EncryptionController.php new file mode 100644 index 00000000..27a1b617 --- /dev/null +++ b/lib/Controller/EncryptionController.php @@ -0,0 +1,132 @@ + + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Bjoern Schiessle + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Controller; + +use OCA\EndToEndEncryption\EncryptionManager; +use OCA\EndToEndEncryption\IMetaDataStorage; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\Files\NotFoundException; +use OCP\ILogger; +use OCP\IRequest; + +/** + * Class EncryptionController + * + * @package OCA\EndToEndEncryption\Controller + */ +class EncryptionController extends OCSController { + + /** @var string */ + private $userId; + + /** @var IMetaDataStorage */ + private $metaDataStorage; + + /** @var EncryptionManager */ + private $manager; + + /** @var ILogger */ + private $logger; + + /** + * RequestHandlerController constructor. + * + * @param string $AppName + * @param IRequest $request + * @param string $userId + * @param IMetaDataStorage $metaDataStorage + * @param EncryptionManager $manager + * @param ILogger $logger + */ + public function __construct($AppName, + IRequest $request, + $userId, + IMetaDataStorage $metaDataStorage, + EncryptionManager $manager, + ILogger $logger) { + parent::__construct($AppName, $request); + $this->userId = $userId; + $this->metaDataStorage = $metaDataStorage; + $this->manager = $manager; + $this->logger = $logger; + } + + /** + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * set encryption flag for folder + * + * + * @param int $id file ID + * @return DataResponse + * + * @throws OCSNotFoundException + */ + public function setEncryptionFlag(int $id): DataResponse { + try { + $this->manager->setEncryptionFlag($id); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($e->getMessage()); + } + + return new DataResponse(); + } + + /** + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * set encryption flag for folder + * + * + * @param int $id file ID + * @return DataResponse + * + * @throws OCSNotFoundException + */ + public function removeEncryptionFlag(int $id): DataResponse { + try { + $this->manager->removeEncryptionFlag($id); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($e->getMessage()); + } + + try { + $this->metaDataStorage->deleteMetaData($this->userId, $id); + } catch (\Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + } + + return new DataResponse(); + } +} diff --git a/lib/Controller/KeyController.php b/lib/Controller/KeyController.php new file mode 100644 index 00000000..a905ea60 --- /dev/null +++ b/lib/Controller/KeyController.php @@ -0,0 +1,316 @@ + + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Bjoern Schiessle + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Controller; + +use OCA\EndToEndEncryption\Exceptions\KeyExistsException; +use OCA\EndToEndEncryption\IKeyStorage; +use OCA\EndToEndEncryption\SignatureHandler; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\Files\ForbiddenException; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IRequest; +use \Exception; +use \BadMethodCallException; + +class KeyController extends OCSController { + + /** @var string */ + private $userId; + + /** @var IKeyStorage */ + private $keyStorage; + + /** @var SignatureHandler */ + private $signatureHandler; + + /** @var ILogger */ + private $logger; + + /** @var IL10N */ + private $l10n; + + /** + * RequestHandlerController constructor. + * + * @param string $AppName + * @param IRequest $request + * @param string $userId + * @param IKeyStorage $keyStorage + * @param SignatureHandler $signatureHandler + * @param ILogger $logger + * @param IL10N $l10n + */ + public function __construct($AppName, + IRequest $request, + $userId, + IKeyStorage $keyStorage, + SignatureHandler $signatureHandler, + ILogger $logger, + IL10N $l10n + ) { + parent::__construct($AppName, $request); + $this->userId = $userId; + $this->keyStorage = $keyStorage; + $this->signatureHandler = $signatureHandler; + $this->logger = $logger; + $this->l10n = $l10n; + } + + /** + * get private key + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @return DataResponse + * + * @throws OCSBadRequestException + * @throws OCSForbiddenException + * @throws OCSNotFoundException + */ + public function getPrivateKey(): DataResponse { + try { + $privateKey = $this->keyStorage->getPrivateKey($this->userId); + return new DataResponse(['private-key' => $privateKey]); + } catch (ForbiddenException $e) { + throw new OCSForbiddenException($this->l10n->t('This is someone else\'s private key')); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($this->l10n->t('Could not find the private key of the user %s', [$this->userId])); + } catch (Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Internal error')); + } + } + + /** + * delete the users private key + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @return DataResponse + * + * @throws OCSBadRequestException + * @throws OCSForbiddenException + * @throws OCSNotFoundException + */ + public function deletePrivateKey(): DataResponse { + try { + $this->keyStorage->deletePrivateKey($this->userId); + return new DataResponse(); + } catch (NotPermittedException $e) { + throw new OCSForbiddenException($this->l10n->t('You are not allowed to delete this private key')); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($this->l10n->t('Could not find the private key belonging to the user %s', [$this->userId])); + } catch (Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Internal error')); + } + } + + + /** + * set private key + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @param string $privateKey + * @return DataResponse + * + * @throws OCSBadRequestException + */ + public function setPrivateKey(string $privateKey): DataResponse { + try { + $this->keyStorage->setPrivateKey($privateKey, $this->userId); + } catch (KeyExistsException $e) { + return new DataResponse([], Http::STATUS_CONFLICT); + } catch (Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Internal error')); + } + + return new DataResponse(['private-key' => $privateKey]); + } + + /** + * get public key + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @param string $users a json encoded list of users + * @return DataResponse + * + * @throws OCSBadRequestException + * @throws OCSNotFoundException + */ + public function getPublicKeys(string $users = ''): DataResponse { + $usersArray = $this->jsonDecode($users); + + $result = ['public-keys' => []]; + foreach ($usersArray as $uid) { + try { + $publicKey = $this->keyStorage->getPublicKey($uid); + $result['public-keys'][$uid] = $publicKey; + } catch (NotFoundException $e) { + throw new OCSNotFoundException($this->l10n->t('Could not find the public key belonging to the user %s', [$uid])); + } catch (Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Internal error')); + } + } + + return new DataResponse($result); + } + + /** + * create public key, store it on the server and return it to the user + * + * if no public key exists and the request contains a valid certificate + * from the currently logged in user we will create one + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @param string $csr request to create a valid public key + * @return DataResponse + * + * @throws OCSForbiddenException + * @throws OCSBadRequestException + */ + public function createPublicKey(string $csr): DataResponse { + if ($this->keyStorage->publicKeyExists($this->userId)) { + return new DataResponse([], Http::STATUS_CONFLICT); + } + + try { + $subject = openssl_csr_get_subject($csr); + $publicKey = $this->signatureHandler->sign($csr); + } catch (BadMethodCallException $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($e->getMessage()); + } catch (Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Internal error')); + } + + $cn = isset($subject['CN']) ? $subject['CN'] : ''; + if ($cn !== $this->userId) { + throw new OCSForbiddenException($this->l10n->t('Common name (CN) does not match the current user')); + } + + $this->keyStorage->setPublicKey($publicKey, $this->userId); + + return new DataResponse(['public-key' => $publicKey]); + } + + /** + * delete the users public key + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @return DataResponse + * + * @throws OCSForbiddenException + * @throws OCSBadRequestException + * @throws OCSNotFoundException + */ + public function deletePublicKey(): ?DataResponse { + try { + $this->keyStorage->deletePublicKey($this->userId); + return new DataResponse(); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($this->l10n->t('Could not find the public key belonging to %s', [$this->userId])); + } catch (NotPermittedException $e) { + throw new OCSForbiddenException($this->l10n->t('This is not your public key to delete')); + } catch (Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Internal error')); + } + } + + + /** + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * get the public server key so that the clients can verify the + * signature of the users public keys + * + * @return DataResponse + * + * @throws OCSBadRequestException + */ + public function getPublicServerKey(): DataResponse { + try { + $publicKey = $this->signatureHandler->getPublicServerKey(); + } catch (Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Internal error')); + } + + return new DataResponse(['public-key' => $publicKey]); + } + + /** + * decode JSON-encoded userlist and return an array + * add the currently logged in user if the user isn't part of the list + * + * @param string $users JSON-encoded userlist + * @return array + * @throws OCSBadRequestException + */ + private function jsonDecode(string $users): array { + $usersArray = []; + if (!empty($users)) { + // TODO - use JSON_THROW_ON_ERROR once we require PHP 7.3 + $usersArray = \json_decode($users, true); + if ($usersArray === null) { + throw new OCSBadRequestException($this->l10n->t('Can not decode userlist')); + } + } + + if (!in_array($this->userId, $usersArray, true)) { + $usersArray[] = $this->userId; + } + + return $usersArray; + } +} diff --git a/lib/Controller/LockingController.php b/lib/Controller/LockingController.php new file mode 100644 index 00000000..ad6c640f --- /dev/null +++ b/lib/Controller/LockingController.php @@ -0,0 +1,159 @@ + + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Bjoern Schiessle + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Controller; + +use OC\User\NoUserException; +use OCA\EndToEndEncryption\Exceptions\FileLockedException; +use OCA\EndToEndEncryption\Exceptions\FileNotLockedException; +use OCA\EndToEndEncryption\FileService; +use OCA\EndToEndEncryption\IMetaDataStorage; +use OCA\EndToEndEncryption\LockManager; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\IRequest; + +class LockingController extends OCSController { + + /** @var string */ + private $userId; + + /** @var IMetaDataStorage */ + private $metaDataStorage; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var FileService */ + private $fileService; + + /** @var LockManager */ + private $lockManager; + + /** @var IL10N */ + private $l10n; + + /** + * RequestHandlerController constructor. + * + * @param string $AppName + * @param IRequest $request + * @param string $userId + * @param IMetaDataStorage $metaDataStorage + * @param LockManager $lockManager + * @param IRootFolder $rootFolder + * @param FileService $fileService + * @param IL10N $l10n + */ + public function __construct($AppName, + IRequest $request, + $userId, + IMetaDataStorage $metaDataStorage, + LockManager $lockManager, + IRootFolder $rootFolder, + FileService $fileService, + IL10N $l10n + ) { + parent::__construct($AppName, $request); + $this->userId = $userId; + $this->metaDataStorage = $metaDataStorage; + $this->rootFolder = $rootFolder; + $this->fileService = $fileService; + $this->lockManager = $lockManager; + $this->l10n = $l10n; + } + + /** + * lock folder + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @param int $id file ID + * + * @return DataResponse + * @throws OCSForbiddenException + */ + public function lockFolder(int $id): DataResponse { + $e2eToken = $this->request->getParam('e2e-token', ''); + + $newToken = $this->lockManager->lockFile($id, $e2eToken); + if ($newToken === null) { + throw new OCSForbiddenException($this->l10n->t('File already locked')); + } + return new DataResponse(['e2e-token' => $newToken]); + } + + + /** + * unlock folder + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @param int $id file ID + * + * @return DataResponse + * @throws OCSForbiddenException + * @throws OCSNotFoundException + */ + public function unlockFolder(int $id): DataResponse { + $token = $this->request->getHeader('e2e-token'); + + try { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + } catch (NoUserException $e) { + throw new OCSForbiddenException($this->l10n->t('You are not allowed to remove the lock')); + } + + $nodes = $userFolder->getById($id); + if (!isset($nodes[0]) || !$nodes[0] instanceof Folder) { + throw new OCSForbiddenException($this->l10n->t('You are not allowed to remove the lock')); + } + + $this->fileService->finalizeChanges($nodes[0]); + $this->metaDataStorage->saveIntermediateFile($this->userId, $id); + $this->metaDataStorage->deleteIntermediateFile($this->userId, $id); + + try { + $this->lockManager->unlockFile($id, $token); + } catch (FileLockedException $e) { + throw new OCSForbiddenException($this->l10n->t('You are not allowed to remove the lock')); + } catch (FileNotLockedException $e) { + throw new OCSNotFoundException($this->l10n->t('File not locked')); + } + + return new DataResponse(); + } +} diff --git a/lib/Controller/MetaDataController.php b/lib/Controller/MetaDataController.php new file mode 100644 index 00000000..a7f1df6d --- /dev/null +++ b/lib/Controller/MetaDataController.php @@ -0,0 +1,202 @@ + + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Bjoern Schiessle + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Controller; + +use OCA\EndToEndEncryption\Exceptions\MetaDataExistsException; +use OCA\EndToEndEncryption\Exceptions\MissingMetaDataException; +use OCA\EndToEndEncryption\IMetaDataStorage; +use OCA\EndToEndEncryption\LockManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IRequest; + +class MetaDataController extends OCSController { + + /** @var string */ + private $userId; + + /** @var IMetaDataStorage */ + private $metaDataStorage; + + /** @var ILogger */ + private $logger; + + /** @var LockManager */ + private $lockManager; + + /** @var IL10N */ + private $l10n; + + /** + * RequestHandlerController constructor. + * + * @param string $AppName + * @param IRequest $request + * @param string $userId + * @param IMetaDataStorage $metaDataStorage + * @param LockManager $lockManager + * @param ILogger $logger + * @param IL10N $l10n + */ + public function __construct($AppName, + IRequest $request, + $userId, + IMetaDataStorage $metaDataStorage, + LockManager $lockManager, + ILogger $logger, + IL10N $l10n + ) { + parent::__construct($AppName, $request); + $this->userId = $userId; + $this->metaDataStorage = $metaDataStorage; + $this->logger = $logger; + $this->lockManager = $lockManager; + $this->l10n = $l10n; + } + + /** + * get metadata + * + * @NoAdminRequired + * @E2ERestrictUserAgent + * + * @param int $id file id + * @return DataResponse + * + * @throws OCSNotFoundException + * @throws OCSBadRequestException + */ + public function getMetaData(int $id): DataResponse { + try { + $metaData = $this->metaDataStorage->getMetaData($this->userId, $id); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($this->l10n->t('Could not find metadata for "%s"', [$id])); + } catch (\Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Can\'t read metadata')); + } + return new DataResponse(['meta-data' => $metaData]); + } + + /** + * set metadata + * + * @NoAdminRequired + * + * @param int $id file id + * @param string $metaData + * @return DataResponse + * + * @throws OCSNotFoundException + * @throws OCSBadRequestException + */ + public function setMetaData(int $id, string $metaData): DataResponse { + try { + $this->metaDataStorage->setMetaDataIntoIntermediateFile($this->userId, $id, $metaData); + } catch (MetaDataExistsException $e) { + return new DataResponse([], Http::STATUS_CONFLICT); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($e->getMessage()); + } catch (\Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Can\'t store metadata')); + } + + return new DataResponse(['meta-data' => $metaData]); + } + + /** + * update metadata + * + * @NoAdminRequired + * + * @param int $id file id + * @param string $metaData + * + * @return DataResponse + * @throws OCSForbiddenException + * @throws OCSBadRequestException + * @throws OCSNotFoundException + */ + public function updateMetaData(int $id, string $metaData): DataResponse { + $e2eToken = $this->request->getParam('e2e-token'); + + if ($this->lockManager->isLocked($id, $e2eToken)) { + throw new OCSForbiddenException($this->l10n->t('You are not allowed to edit the file, make sure to first lock it, and then send the right token')); + } + + try { + $this->metaDataStorage->updateMetaDataIntoIntermediateFile($this->userId, $id, $metaData); + } catch (MissingMetaDataException $e) { + throw new OCSNotFoundException($this->l10n->t('Metadata-file doesn\'t exist')); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($e->getMessage()); + } catch (\Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Can\'t store metadata')); + } + + return new DataResponse(['meta-data' => $metaData]); + } + + /** + * delete metadata + * + * @NoAdminRequired + * + * @param int $id file id + * @return DataResponse + * + * @throws OCSForbiddenException + * @throws OCSNotFoundException + * @throws OCSBadRequestException + */ + public function deleteMetaData(int $id): DataResponse { + try { + $this->metaDataStorage->deleteMetaData($this->userId, $id); + } catch (NotFoundException $e) { + throw new OCSNotFoundException($this->l10n->t('Could not find metadata for "%s"', [$id])); + } catch (NotPermittedException $e) { + throw new OCSForbiddenException($this->l10n->t('Only the owner can delete the metadata-file')); + } catch (\Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + throw new OCSBadRequestException($this->l10n->t('Can\'t delete metadata')); + } + return new DataResponse(); + } +} diff --git a/lib/Controller/RequestHandlerController.php b/lib/Controller/RequestHandlerController.php deleted file mode 100644 index 16e8b46f..00000000 --- a/lib/Controller/RequestHandlerController.php +++ /dev/null @@ -1,582 +0,0 @@ - - * - * @author Bjoern Schiessle - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\EndToEndEncryption\Controller; - -use BadMethodCallException; -use Exception; -use OC\Files\Node\Folder; -use OCA\EndToEndEncryption\EncryptionManager; -use OCA\EndToEndEncryption\Exceptions\FileLockedException; -use OCA\EndToEndEncryption\Exceptions\FileNotLockedException; -use OCA\EndToEndEncryption\Exceptions\KeyExistsException; -use OCA\EndToEndEncryption\Exceptions\MetaDataExistsException; -use OCA\EndToEndEncryption\Exceptions\MissingMetaDataException; -use OCA\EndToEndEncryption\FileService; -use OCA\EndToEndEncryption\IKeyStorage; -use OCA\EndToEndEncryption\IMetaDataStorage; -use OCA\EndToEndEncryption\LockManager; -use OCA\EndToEndEncryption\SignatureHandler; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\OCS\OCSBadRequestException; -use OCP\AppFramework\OCS\OCSForbiddenException; -use OCP\AppFramework\OCS\OCSNotFoundException; -use OCP\AppFramework\OCSController; -use OCP\Files\ForbiddenException; -use OCP\Files\IRootFolder; -use OCP\Files\NotFoundException; -use OCP\Files\NotPermittedException; -use OCP\IL10N; -use OCP\ILogger; -use OCP\IRequest; -use function in_array; -use function json_decode; - -/** - * Class RequestHandlerController - * - * handle API calls from the client to the server - * - * @package OCA\EndToEndEncryption\Controller - */ -class RequestHandlerController extends OCSController { - - /** @var string */ - private $userId; - - /** @var IKeyStorage */ - private $keyStorage; - - /** @var IMetaDataStorage */ - private $metaDataStorage; - - /** @var SignatureHandler */ - private $signatureHandler; - - /** @var EncryptionManager */ - private $manager; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var FileService */ - private $fileService; - - /** @var ILogger */ - private $logger; - - /** @var LockManager */ - private $lockManager; - - /** @var IL10N */ - private $l; - - /** - * RequestHandlerController constructor. - * - * @param string $AppName - * @param IRequest $request - * @param string $UserId - * @param IKeyStorage $keyStorage - * @param IMetaDataStorage $metaDataStorage - * @param SignatureHandler $signatureHandler - * @param EncryptionManager $manager - * @param LockManager $lockManager - * @param IRootFolder $rootFolder - * @param FileService $fileService - * @param ILogger $logger - * @param IL10N $l - */ - public function __construct($AppName, - IRequest $request, - $UserId, - IKeyStorage $keyStorage, - IMetaDataStorage $metaDataStorage, - SignatureHandler $signatureHandler, - EncryptionManager $manager, - LockManager $lockManager, - IRootFolder $rootFolder, - FileService $fileService, - ILogger $logger, - IL10N $l - ) { - parent::__construct($AppName, $request); - $this->userId = $UserId; - $this->keyStorage = $keyStorage; - $this->metaDataStorage = $metaDataStorage; - $this->signatureHandler = $signatureHandler; - $this->manager = $manager; - $this->rootFolder = $rootFolder; - $this->fileService = $fileService; - $this->logger = $logger; - $this->lockManager = $lockManager; - $this->l = $l; - } - - /** - * get private key - * - * @NoAdminRequired - * - * @return DataResponse - * - * @throws OCSBadRequestException - * @throws OCSForbiddenException - * @throws OCSNotFoundException - */ - public function getPrivateKey(): DataResponse { - try { - $privateKey = $this->keyStorage->getPrivateKey($this->userId); - return new DataResponse(['private-key' => $privateKey]); - } catch (ForbiddenException $e) { - throw new OCSForbiddenException($this->l->t('This is someone else\'s private key')); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t('Could not find the private key of the user %s', [$this->userId])); - } catch (Exception $e) { - $error = 'Can\'t get private key: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t('Internal error')); - } - } - - /** - * delete the users private key - * - * @NoAdminRequired - * - * @return DataResponse - * - * @throws OCSBadRequestException - * @throws OCSForbiddenException - * @throws OCSNotFoundException - */ - public function deletePrivateKey(): DataResponse { - try { - $this->keyStorage->deletePrivateKey($this->userId); - return new DataResponse(); - } catch (NotPermittedException $e) { - throw new OCSForbiddenException($this->l->t('You are not allowed to delete this private key')); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t('Could not find the private key belonging to the user %s', [$this->userId])); - } catch (Exception $e) { - $error = 'Can\'t find private key: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t('Internal error')); - } - } - - - /** - * set private key - * - * @NoAdminRequired - * - * @param string $privateKey - * @return DataResponse - * - * @throws OCSBadRequestException - */ - public function setPrivateKey(string $privateKey): DataResponse { - try { - $this->keyStorage->setPrivateKey($privateKey, $this->userId); - } catch (KeyExistsException $e) { - return new DataResponse([], Http::STATUS_CONFLICT); - } catch (Exception $e) { - $error = 'Can\'t store private key: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t('internal error')); - } - - return new DataResponse(['private-key' => $privateKey]); - } - - /** - * get public key - * - * @NoAdminRequired - * - * @param string $users a json encoded list of users - * @return DataResponse - * - * @throws OCSBadRequestException - * @throws OCSNotFoundException - */ - public function getPublicKeys(string $users = ''): DataResponse { - $usersArray = $this->jsonDecode($users); - - $result = ['public-keys' => []]; - foreach ($usersArray as $uid) { - try { - $publicKey = $this->keyStorage->getPublicKey($uid); - $result['public-keys'][$uid] = $publicKey; - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t('Could not find the public key belonging to the user %s', [$uid])); - } catch (Exception $e) { - $error = 'Can\'t get public keys: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t('Internal error')); - } - } - - return new DataResponse($result); - } - - /** - * create public key, store it on the server and return it to the user - * - * if no public key exists and the request contains a valid certificate - * from the currently logged in user we will create one - * - * @NoAdminRequired - * - * @param string $csr request to create a valid public key - * @return DataResponse - * - * @throws OCSForbiddenException - * @throws OCSBadRequestException - */ - public function createPublicKey(string $csr): DataResponse { - if ($this->keyStorage->publicKeyExists($this->userId)) { - return new DataResponse([], Http::STATUS_CONFLICT); - } - - try { - $subject = openssl_csr_get_subject($csr); - $publicKey = $this->signatureHandler->sign($csr); - } catch (BadMethodCallException $e) { - $error = 'Can\'t create public key: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t($e->getMessage())); - } catch (Exception $e) { - $error = 'Can\'t create public key: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t('Internal error')); - } - - $cn = isset($subject['CN']) ? $subject['CN'] : ''; - if ($cn !== $this->userId) { - throw new OCSForbiddenException($this->l->t('Common name (CN) does not match the current user')); - } - - $this->keyStorage->setPublicKey($publicKey, $this->userId); - - return new DataResponse(['public-key' => $publicKey]); - } - - /** - * delete the users public key - * - * @NoAdminRequired - * - * @return DataResponse - * - * @throws OCSForbiddenException - * @throws OCSBadRequestException - * @throws OCSNotFoundException - */ - public function deletePublicKey(): ?DataResponse { - try { - $this->keyStorage->deletePublicKey($this->userId); - return new DataResponse(); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t('Could not find the public key belonging to %s', [$this->userId])); - } catch (NotPermittedException $e) { - throw new OCSForbiddenException($this->l->t('This is not your private key to delete')); - } catch (Exception $e) { - $error = 'Can\'t delete public keys: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t('Internal error')); - } - } - - - /** - * get metadata - * - * @NoAdminRequired - * - * @param int $id file id - * @return DataResponse - * - * @throws OCSNotFoundException - * @throws OCSBadRequestException - */ - public function getMetaData(int $id): DataResponse { - try { - $metaData = $this->metaDataStorage->getMetaData($this->userId, $id); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t('Could not find metadata for "%s"', [$id])); - } catch (Exception $e) { - $error = 'Can\'t read metadata: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t("Can\'t read metadata")); - } - return new DataResponse(['meta-data' => $metaData]); - } - - /** - * set metadata - * - * @NoAdminRequired - * - * @param int $id file id - * @param string $metaData - * @return DataResponse - * - * @throws OCSNotFoundException - * @throws OCSBadRequestException - */ - public function setMetaData(int $id, string $metaData): DataResponse { - try { - $this->metaDataStorage->setMetaDataIntoIntermediateFile($this->userId, $id, $metaData); - } catch (MetaDataExistsException $e) { - return new DataResponse([], Http::STATUS_CONFLICT); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t($e->getMessage())); - } catch (Exception $e) { - $error = 'Can\'t store metadata: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t("Can\'t store metadata")); - } - - return new DataResponse(['meta-data' => $metaData]); - } - - /** - * update metadata - * - * @NoAdminRequired - * - * @param int $id file id - * @param string $metaData - * - * @return DataResponse - * @throws OCSForbiddenException - * @throws OCSBadRequestException - * @throws OCSNotFoundException - */ - public function updateMetaData(int $id, string $metaData): DataResponse { - $e2eToken = $this->request->getParam('e2e-token'); - - if ($this->lockManager->isLocked($id, $e2eToken)) { - throw new OCSForbiddenException($this->l->t('You are not allowed to edit the file, make sure to first lock it, and then send the right token')); - } - - try { - $this->metaDataStorage->updateMetaDataIntoIntermediateFile($this->userId, $id, $metaData); - } catch (MissingMetaDataException $e) { - throw new OCSNotFoundException($this->l->t("Metadata-file doesn\'t exist")); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t($e->getMessage())); - } catch (Exception $e) { - $error = 'Can\'t store metadata: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t("Can\'t store metadata")); - } - - return new DataResponse(['meta-data' => $metaData]); - } - - /** - * delete metadata - * - * @NoAdminRequired - * - * @param int $id file id - * @return DataResponse - * - * @throws OCSForbiddenException - * @throws OCSNotFoundException - * @throws OCSBadRequestException - */ - public function deleteMetaData(int $id): DataResponse { - try { - $this->metaDataStorage->deleteMetaData($this->userId, $id); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t('Could not find metadata for "%s"', [$id])); - } catch (NotPermittedException $e) { - throw new OCSForbiddenException($this->l->t('Only the owner can delete the metadata-file')); - } catch (Exception $e) { - $error = 'Internal server error: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t("Can\'t delete metadata")); - } - return new DataResponse(); - } - - - /** - * @NoAdminRequired - * - * get the public server key so that the clients can verify the - * signature of the users public keys - * - * @return DataResponse - * - * @throws OCSBadRequestException - */ - public function getPublicServerKey(): DataResponse { - try { - $publicKey = $this->signatureHandler->getPublicServerKey(); - } catch (Exception $e) { - $error = 'Can\'t read server wide public key: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - throw new OCSBadRequestException($this->l->t('Internal error')); - } - - return new DataResponse(['public-key' => $publicKey]); - } - - /** - * @NoAdminRequired - * - * set encryption flag for folder - * - * @param int $id file ID - * @return DataResponse - * - * @throws OCSNotFoundException - */ - public function setEncryptionFlag(int $id): DataResponse { - try { - $this->manager->setEncryptionFlag($id); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t($e->getMessage())); - } - - return new DataResponse(); - } - - /** - * @NoAdminRequired - * - * set encryption flag for folder - * - * @param int $id file ID - * @return DataResponse - * - * @throws OCSNotFoundException - */ - public function removeEncryptionFlag(int $id): DataResponse { - try { - $this->manager->removeEncryptionFlag($id); - } catch (NotFoundException $e) { - throw new OCSNotFoundException($this->l->t($e->getMessage())); - } - - try { - $this->metaDataStorage->deleteMetaData($this->userId, $id); - } catch (Exception $e) { - $error = 'Internal server error: ' . $e->getMessage(); - $this->logger->error($error, ['app' => 'end_to_end_encryption']); - } - - return new DataResponse(); - } - - /** - * lock folder - * - * @NoAdminRequired - * - * @param int $id file ID - * - * @return DataResponse - * @throws OCSForbiddenException - */ - public function lockFolder(int $id): DataResponse { - $e2eToken = $this->request->getParam('e2e-token', ''); - - $newToken = $this->lockManager->lockFile($id, $e2eToken); - if ($newToken === null) { - throw new OCSForbiddenException($this->l->t('File already locked')); - } - return new DataResponse(['e2e-token' => $newToken]); - } - - - /** - * unlock folder - * - * @NoAdminRequired - * - * @param int $id file ID - * - * @return DataResponse - * @throws OCSNotFoundException - * @throws OCSForbiddenException - */ - public function unlockFolder(int $id): DataResponse { - $token = $this->request->getHeader('e2e-token'); - - $userView = $this->rootFolder->getUserFolder($this->userId); - $nodes = $userView->getById($id); - if (!isset($nodes[0]) || !$nodes[0] instanceof Folder) { - throw new OCSForbiddenException($this->l->t('You are not allowed to remove the lock')); - } - - $this->fileService->finalizeChanges($nodes[0]); - $this->metaDataStorage->saveIntermediateFile($this->userId, $id); - $this->metaDataStorage->deleteIntermediateFile($this->userId, $id); - - try { - $this->lockManager->unlockFile($id, $token); - } catch (FileLockedException $e) { - throw new OCSForbiddenException($this->l->t('You are not allowed to remove the lock')); - } catch (FileNotLockedException $e) { - throw new OCSNotFoundException($this->l->t('File not locked')); - } - - return new DataResponse(); - } - - /** - * decode JSON-encoded userlist and return an array - * add the currently logged in user if the user isn't part of the list - * - * @param string $users JSON-encoded userlist - * @return array - * @throws OCSBadRequestException - */ - private function jsonDecode(string $users): array { - $usersArray = []; - if (!empty($users)) { - // TODO - use JSON_THROW_ON_ERROR once we require PHP 7.3 - $usersArray = json_decode($users, true); - if ($usersArray === null) { - throw new OCSBadRequestException($this->l->t('Can not decode userlist')); - } - } - - if (!in_array($this->userId, $usersArray, true)) { - $usersArray[] = $this->userId; - } - - return $usersArray; - } -} diff --git a/lib/Middleware/UserAgentCheckMiddleware.php b/lib/Middleware/UserAgentCheckMiddleware.php new file mode 100644 index 00000000..ed04c02a --- /dev/null +++ b/lib/Middleware/UserAgentCheckMiddleware.php @@ -0,0 +1,81 @@ + + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Middleware; + +use OCA\EndToEndEncryption\UserAgentManager; +use OCP\AppFramework\Middleware; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\Utility\IControllerMethodReflector; +use OCP\IRequest; + +/** + * Class UserAgentCheckMiddleware + * + * @package OCA\EndToEndEncryption\Middleware + */ +class UserAgentCheckMiddleware extends Middleware { + + /** @var IControllerMethodReflector */ + private $reflector; + + /** @var IRequest */ + private $request; + + /** @var UserAgentManager */ + private $userAgentManager; + + /** + * UserAgentCheckMiddleware constructor. + * + * @param IControllerMethodReflector $reflector + * @param IRequest $request + * @param UserAgentManager $userAgentManager + */ + public function __construct(IControllerMethodReflector $reflector, + IRequest $request, + UserAgentManager $userAgentManager) { + $this->reflector = $reflector; + $this->request = $request; + $this->userAgentManager = $userAgentManager; + } + + /** + * @param \OCP\AppFramework\Controller $controller + * @param string $methodName + * @throws OCSForbiddenException + */ + public function beforeController($controller, $methodName): void { + parent::beforeController($controller, $methodName); + + $userAgent = $this->request->getHeader('user-agent'); + if ($this->reflector->hasAnnotation('E2ERestrictUserAgent') + && !$this->userAgentManager->supportsEndToEndEncryption($userAgent)) { + throw new OCSForbiddenException('Client "' . $userAgent . '" is not allowed to access end-to-end encrypted content.'); + } + } +} diff --git a/tests/Unit/Controller/EncryptionControllerTest.php b/tests/Unit/Controller/EncryptionControllerTest.php new file mode 100644 index 00000000..ece6ef4d --- /dev/null +++ b/tests/Unit/Controller/EncryptionControllerTest.php @@ -0,0 +1,155 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Tests\Controller; + +use OCA\EndToEndEncryption\Controller\EncryptionController; +use OCA\EndToEndEncryption\EncryptionManager; +use OCA\EndToEndEncryption\IMetaDataStorage; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\Files\NotFoundException; +use OCP\ILogger; +use OCP\IRequest; +use Test\TestCase; + +class EncryptionControllerTest extends TestCase { + + /** @var string */ + private $appName; + + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + + /** @var string */ + private $userId; + + /** @var IMetaDataStorage|\PHPUnit\Framework\MockObject\MockObject */ + private $metaDataStorage; + + /** @var EncryptionManager|\PHPUnit\Framework\MockObject\MockObject */ + private $encryptionManager; + + /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + + /** @var EncryptionController */ + private $controller; + + protected function setUp(): void { + parent::setUp(); + + $this->appName = 'end_to_end_encryption'; + $this->request = $this->createMock(IRequest::class); + $this->userId = 'john.doe'; + $this->metaDataStorage = $this->createMock(IMetaDataStorage::class); + $this->encryptionManager = $this->createMock(EncryptionManager::class); + $this->logger = $this->createMock(ILogger::class); + + $this->controller = new EncryptionController($this->appName, + $this->request, + $this->userId, + $this->metaDataStorage, + $this->encryptionManager, + $this->logger); + } + + public function testSetEncryptionFlag(): void { + $fileId = 42; + + $this->encryptionManager->expects($this->once()) + ->method('setEncryptionFlag') + ->with($fileId); + + $response = $this->controller->setEncryptionFlag($fileId); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([], $response->getData()); + } + + public function testSetEncryptionFlagWithException(): void { + $fileId = 42; + + $this->encryptionManager->expects($this->once()) + ->method('setEncryptionFlag') + ->with($fileId) + ->willThrowException(new NotFoundException('Exception Message')); + + $this->expectException(OCSNotFoundException::class); + $this->expectExceptionMessage('Exception Message'); + + $this->controller->setEncryptionFlag($fileId); + } + + public function testRemoveEncryptionFlag(): void { + $fileId = 42; + + $this->encryptionManager->expects($this->once()) + ->method('removeEncryptionFlag') + ->with($fileId); + $this->metaDataStorage->expects($this->once()) + ->method('deleteMetaData') + ->with($this->userId, $fileId); + + $response = $this->controller->removeEncryptionFlag($fileId); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([], $response->getData()); + } + + public function testRemoveEncryptionEncryptionManagerException(): void { + $fileId = 42; + + $this->encryptionManager->expects($this->once()) + ->method('removeEncryptionFlag') + ->with($fileId) + ->willThrowException(new NotFoundException('Exception Message')); + $this->metaDataStorage->expects($this->never()) + ->method('deleteMetaData') + ->with($this->userId, $fileId); + + $this->expectException(OCSNotFoundException::class); + $this->expectExceptionMessage('Exception Message'); + + $this->controller->removeEncryptionFlag($fileId); + } + + public function testRemoveEncryptionMetaDataException(): void { + $fileId = 42; + + $this->encryptionManager->expects($this->once()) + ->method('removeEncryptionFlag') + ->with($fileId); + $exception = new \Exception(); + $this->metaDataStorage->expects($this->once()) + ->method('deleteMetaData') + ->with($this->userId, $fileId) + ->willThrowException($exception); + + $this->logger->expects($this->once()) + ->method('logException') + ->with($exception, ['app' => $this->appName]); + + $response = $this->controller->removeEncryptionFlag($fileId); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([], $response->getData()); + } +} diff --git a/tests/Unit/Controller/KeyControllerTest.php b/tests/Unit/Controller/KeyControllerTest.php new file mode 100644 index 00000000..d5d7ca70 --- /dev/null +++ b/tests/Unit/Controller/KeyControllerTest.php @@ -0,0 +1,550 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Tests\Controller; + +use OCA\EndToEndEncryption\Controller\KeyController; +use OCA\EndToEndEncryption\Exceptions\KeyExistsException; +use OCA\EndToEndEncryption\IKeyStorage; +use OCA\EndToEndEncryption\SignatureHandler; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\Files\ForbiddenException; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IRequest; +use Test\TestCase; + +class KeyControllerTest extends TestCase { + + /** @var string */ + private $appName; + + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + + /** @var string */ + private $userId; + + /** @var IKeyStorage|\PHPUnit\Framework\MockObject\MockObject */ + private $keyStorage; + + /** @var SignatureHandler|\PHPUnit\Framework\MockObject\MockObject */ + private $signatureHandler; + + /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + private $l10n; + + /** @var KeyController */ + private $controller; + + /** @var string valid CSR (CN set to "admin") */ + private $validCSR = "-----BEGIN CERTIFICATE REQUEST----- +MIIC7jCCAdYCAQAwgagxCzAJBgNVBAYTAlVLMREwDwYDVQQIDAhTb21lcnNldDEU +MBIGA1UEBwwLR2xhc3RvbmJ1cnkxHzAdBgNVBAoMFlRoZSBCcmFpbiBSb29tIExp +bWl0ZWQxHzAdBgNVBAsMFlBIUCBEb2N1bWVudGF0aW9uIFRlYW0xDjAMBgNVBAMM +BWFkbWluMR4wHAYJKoZIhvcNAQkBFg93ZXpAZXhhbXBsZS5jb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHWU8rWlK3lud/r5OQoilxypgIzbBf5pqM +H0rpYwFv3uctnK5Lt3M+WY45XdJt98Pq8eQ0AbyAf3IuhnpF+X2Ej3QnCenZ0H+B +J6/mZXdo9f7IXa2wH5LtA2cmm1XWQWubN/Jzr9psq+kxbocyGTQhNGeeB2OPcgyl +73eddJNIbFVlNEzbdcBNNsSwKcB+LP/JyJ9e1HZ4af6CHdX2SG1HvO+dICdEuO2E +mC9lM896MJFWwNns5mx453Y1FmxFmAi1zQAAP+AZ5Taqy6yCzqJ9Y4/FDRi1NC5V +stnu9REuPYSS8YgsJwQE/DUd+I+UonkcDfac8PIH5p5YHpMq0ChvAgMBAAGgADAN +BgkqhkiG9w0BAQUFAAOCAQEAh8YVAsAcPR5v7kv96UtkVI4xK6R9BdmVsnisxTpm +g9JVbfji7kpxbSgXfRSozTG3bl9ynrck39/2SoFQGSGrW2iV+drclftSk+uBFb1F +iXYEWJxYSz2CcUeijoBrBsarfmODgOHzmgXmCoOToz2DkdtM7g9INWkC06Do4pTQ +fqA3PS2td1gWqQCQthF9IWOCIxNI16lokVTgNCZKewXsn9Bjm3hsLLeJU9jBXyVN +w7829dr37SuA2kQb86aVpqdL50v3HjCclXd7PfWiYqajuHaIsokBV5ly2IdQo4Cz +AYzYQFPtjsDZ4Tju4VZKM4YpF2GwQgT7zhzDBvywGPqvfw== +-----END CERTIFICATE REQUEST----- +"; + + /** @var string */ + private $invalidCSR = "-----BEGIN CERTIFICATE REQUEST-----\nMIIC7jCCAdYCAQAwgagxCzAJBgNVBAYTAlVLMREwDwYDVQQIDAhTb21lcnNldDEU\nMBIGA1UEBwwLR2xhc3RvbmJ1cnkxHzAdBgNVBAoMFlRoZSBCcmFpbiBSb29tIExp\nbWl0ZWQxHzAdBgNVBAsMFlBIUCBEb2N1bWVudGF0aW9uIFRlYW0xDjAMBgNVBAMM\nBWFkbWluMR4wHAYJKoZIhvcNAQkBFg93ZXpAZXhhbXBsZS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHWU8rWlK3lud%2Fr5OQoilxypgIzbBf5pqM\nH0rpYwFv3uctnK5Lt3M%2BWY45XdJt98Pq8eQ0AbyAf3IuhnpF%2BX2Ej3QnCenZ0H%2BB\nJ6%2FmZXdo9f7IXa2wH5LtA2cmm1XWQWubN%2FJzr9psq%2BkxbocyGTQhNGeeB2OPcgyl\n73eddJNIbFVlNEzbdcBNNsSwKcB%2BLP%2FJyJ9e1HZ4af6CHdX2SG1HvO%2BdICdEuO2E\nmC9lM896MJFWwNns5mx453Y1FmxFmAi1zQAAP%2BAZ5Taqy6yCzqJ9Y4%2FFDRi1NC5V\nstnu9REuPYSS8YgsJwQE%2FDUd%2BI%2BUonkcDfac8PIH5p5YHpMq0ChvAgMBAAGgADAN\nBgkqhkiG9w0BAQUFAAOCAQEAh8YVAsAcPR5v7kv96UtkVI4xK6R9BdmVsnisxTpm\ng9JVbfji7kpxbSgXfRSozTG3bl9ynrck39%2F2SoFQGSGrW2iV%2BdrclftSk%2BuBFb1F\niXYEWJxYSz2CcUeijoBrBsarfmODgOHzmgXmCoOToz2DkdtM7g9INWkC06Do4pTQ\nfqA3PS2td1gWqQCQthF9IWOCIxNI16lokVTgNCZKewXsn9Bjm3hsLLeJU9jBXyVN\nw7829dr37SuA2kQb86aVpqdL50v3HjCclXd7PfWiYqajuHaIsokBV5ly2IdQo4Cz\nAYzYQFPtjsDZ4Tju4VZKM4YpF2GwQgT7zhzDBvywGPqvfw%3D%3D\n-----END+CERTIFICATE+REQUEST-----\n"; + + + protected function setUp(): void { + parent::setUp(); + + $this->appName = 'end_to_end_encryption'; + $this->request = $this->createMock(IRequest::class); + $this->userId = 'admin'; + $this->keyStorage = $this->createMock(IKeyStorage::class); + $this->signatureHandler = $this->createMock(SignatureHandler::class); + $this->logger = $this->createMock(ILogger::class); + $this->l10n = $this->createMock(IL10N::class); + + $this->controller = new KeyController($this->appName, + $this->request, + $this->userId, + $this->keyStorage, + $this->signatureHandler, + $this->logger, + $this->l10n); + } + + /** + * @param \Exception|null $keyStorageException + * @param string|null $expectedException + * @param string|null $expectedExceptionMessage + * @param bool $expectLogger + * + * @dataProvider getPrivateKeyDataProvider + */ + public function testGetPrivateKey(?\Exception $keyStorageException, + ?string $expectedException, + ?string $expectedExceptionMessage, + bool $expectLogger): void { + $privateKey = 'MY-SECRET-PRIVATE-KEY'; + if ($keyStorageException) { + $this->keyStorage->expects($this->once()) + ->method('getPrivateKey') + ->with('admin') + ->willThrowException($keyStorageException); + } else { + $this->keyStorage->expects($this->once()) + ->method('getPrivateKey') + ->with('admin') + ->willReturn($privateKey); + } + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('logException') + ->with($keyStorageException, ['app' => $this->appName]); + } + + if ($expectedException) { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->getPrivateKey(); + } else { + $response = $this->controller->getPrivateKey(); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([ + 'private-key' => $privateKey, + ], $response->getData()); + } + } + + public function getPrivateKeyDataProvider(): array { + return [ + [null, null, null, false], + [new ForbiddenException('', false), OCSForbiddenException::class, 'This is someone else\'s private key', false], + [new NotFoundException(), OCSNotFoundException::class, 'Could not find the private key of the user admin', false], + [new \Exception(), OCSBadRequestException::class, 'Internal error', true], + ]; + } + + /** + * @param \Exception|null $keyStorageException + * @param string|null $expectedException + * @param string|null $expectedExceptionMessage + * @param bool $expectLogger + * + * @dataProvider deletePrivateKeyDataProvider + */ + public function testDeletePrivateKey(?\Exception $keyStorageException, + ?string $expectedException, + ?string $expectedExceptionMessage, + bool $expectLogger): void { + if ($keyStorageException) { + $this->keyStorage->expects($this->once()) + ->method('deletePrivateKey') + ->with('admin') + ->willThrowException($keyStorageException); + } else { + $this->keyStorage->expects($this->once()) + ->method('deletePrivateKey') + ->with('admin'); + } + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('logException') + ->with($keyStorageException, ['app' => $this->appName]); + } + + if ($expectedException) { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->deletePrivateKey(); + } else { + $response = $this->controller->deletePrivateKey(); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([], $response->getData()); + } + } + + public function deletePrivateKeyDataProvider(): array { + return [ + [null, null, null, false], + [new NotPermittedException(), OCSForbiddenException::class, 'You are not allowed to delete this private key', false], + [new NotFoundException(), OCSNotFoundException::class, 'Could not find the private key belonging to the user admin', false], + [new \Exception(), OCSBadRequestException::class, 'Internal error', true], + ]; + } + + /** + * @param \Exception|null $keyStorageException + * @param string|null $expectedException + * @param string|null $expectedExceptionMessage + * @param bool $expectLogger + * @param array $expectedData + * @param int $expectedStatusCode + * + * @dataProvider setPrivateKeyDataProvider + */ + public function testSetPrivateKey(?\Exception $keyStorageException, + ?string $expectedException, + ?string $expectedExceptionMessage, + bool $expectLogger, + ?array $expectedData, + ?int $expectedStatusCode): void { + $privateKey = 'MY-SECRET-PRIVATE-KEY'; + if ($keyStorageException) { + $this->keyStorage->expects($this->once()) + ->method('setPrivateKey') + ->with($privateKey, 'admin') + ->willThrowException($keyStorageException); + } else { + $this->keyStorage->expects($this->once()) + ->method('setPrivateKey') + ->with($privateKey, 'admin') + ->willReturn($privateKey); + } + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('logException') + ->with($keyStorageException, ['app' => $this->appName]); + } + + if ($expectedException) { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->setPrivateKey($privateKey); + } else { + $response = $this->controller->setPrivateKey($privateKey); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals($expectedData, $response->getData()); + $this->assertEquals($expectedStatusCode, $response->getStatus()); + } + } + + public function setPrivateKeyDataProvider(): array { + return [ + [null, null, null, false, ['private-key' => 'MY-SECRET-PRIVATE-KEY'], 200], + [new KeyExistsException(), null, null, false, [], 409], + [new \Exception(), OCSBadRequestException::class, 'Internal error', true, null, null], + ]; + } + + public function testGetPublicKeys(): void { + $users = '["user1","user2","user3"]'; + + $this->keyStorage->expects($this->exactly(4)) + ->method('getPublicKey') + ->willReturnMap([ + ['user1', 'USER1-PUBLIC-KEY'], + ['user2', 'USER2-PUBLIC-KEY'], + ['user3', 'USER3-PUBLIC-KEY'], + ['admin', 'JOHN.DOE-PUBLIC-KEY'], + ]); + + $response = $this->controller->getPublicKeys($users); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([ + 'public-keys' => [ + 'user1' => 'USER1-PUBLIC-KEY', + 'user2' => 'USER2-PUBLIC-KEY', + 'user3' => 'USER3-PUBLIC-KEY', + 'admin' => 'JOHN.DOE-PUBLIC-KEY', + ] + ], $response->getData()); + } + + public function testGetPublicKeysInvalidJSON(): void { + $users = 'INVALID-JSON'; + + $this->keyStorage->expects($this->never()) + ->method('getPublicKey'); + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Can not decode userlist'); + + $this->controller->getPublicKeys($users); + } + + public function testGetPublicKeysNotFoundException(): void { + $users = '["user1","user2","user3"]'; + + $this->keyStorage->expects($this->once()) + ->method('getPublicKey') + ->willThrowException(new NotFoundException()); + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->expectException(OCSNotFoundException::class); + $this->expectExceptionMessage('Could not find the public key belonging to the user user1'); + + $this->controller->getPublicKeys($users); + } + + public function testGetPublicKeysGenericException(): void { + $users = '["user1","user2","user3"]'; + + $exception = new \Exception(); + $this->keyStorage->expects($this->once()) + ->method('getPublicKey') + ->willThrowException($exception); + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->logger->expects($this->once()) + ->method('logException') + ->with($exception, ['app' => $this->appName]); + + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Internal error'); + + $this->controller->getPublicKeys($users); + } + + public function testCreatePublicKeySuccessful(): void { + $this->keyStorage->expects($this->once()) + ->method('publicKeyExists') + ->with('admin') + ->willReturn(false); + + $this->signatureHandler->expects($this->once()) + ->method('sign') + ->with($this->validCSR) + ->willReturn('MY-PUBLIC-KEY'); + + $this->keyStorage->expects($this->once()) + ->method('setPublicKey') + ->with('MY-PUBLIC-KEY', 'admin'); + + $response = $this->controller->createPublicKey($this->validCSR); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([ + 'public-key' => 'MY-PUBLIC-KEY', + ], $response->getData()); + } + + public function testCreatePublicKeyInvalidCN(): void { + $controller = new KeyController($this->appName, + $this->request, + 'user123', + $this->keyStorage, + $this->signatureHandler, + $this->logger, + $this->l10n); + + $this->keyStorage->expects($this->once()) + ->method('publicKeyExists') + ->with('user123') + ->willReturn(false); + + $this->signatureHandler->expects($this->once()) + ->method('sign') + ->with($this->validCSR) + ->willReturn('MY-PUBLIC-KEY'); + + $this->keyStorage->expects($this->never()) + ->method('setPublicKey'); + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->expectException(OCSForbiddenException::class); + $this->expectExceptionMessage('Common name (CN) does not match the current user'); + + $controller->createPublicKey($this->validCSR); + } + + public function testCreatePublicKeyInvalidCSR(): void { + $this->keyStorage->expects($this->once()) + ->method('publicKeyExists') + ->with('admin') + ->willReturn(false); + + $this->signatureHandler->expects($this->once()) + ->method('sign') + ->with($this->invalidCSR) + ->willThrowException(new \BadMethodCallException()); + + $this->expectException(OCSBadRequestException::class); + + $this->controller->createPublicKey($this->invalidCSR); + } + + public function testCreatePublicKeyConflict(): void { + $this->keyStorage->expects($this->once()) + ->method('publicKeyExists') + ->with('admin') + ->willReturn(true); + + $response = $this->controller->createPublicKey($this->validCSR); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([], $response->getData()); + $this->assertEquals(409, $response->getStatus()); + } + + /** + * @param \Exception|null $keyStorageException + * @param string|null $expectedException + * @param string|null $expectedExceptionMessage + * @param bool $expectLogger + * + * @dataProvider deletePublicKeyDataProvider + */ + public function testDeletePublicKey(?\Exception $keyStorageException, + ?string $expectedException, + ?string $expectedExceptionMessage, + bool $expectLogger): void { + if ($keyStorageException) { + $this->keyStorage->expects($this->once()) + ->method('deletePublicKey') + ->with('admin') + ->willThrowException($keyStorageException); + } else { + $this->keyStorage->expects($this->once()) + ->method('deletePublicKey') + ->with('admin'); + } + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('logException') + ->with($keyStorageException, ['app' => $this->appName]); + } + + if ($expectedException) { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->deletePublicKey(); + } else { + $response = $this->controller->deletePublicKey(); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([], $response->getData()); + } + } + + public function deletePublicKeyDataProvider(): array { + return [ + [null, null, null, false], + [new NotFoundException(), OCSNotFoundException::class, 'Could not find the public key belonging to admin', false], + [new NotPermittedException(), OCSForbiddenException::class, 'This is not your public key to delete', false], + [new \Exception(), OCSBadRequestException::class, 'Internal error', true], + ]; + } + + public function testGetPublicServerKey(): void { + $publicKey = 'PUBLIC-KEY'; + $this->signatureHandler->expects($this->once()) + ->method('getPublicServerKey') + ->wilLReturn($publicKey); + + $response = $this->controller->getPublicServerKey(); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([ + 'public-key' => $publicKey, + ], $response->getData()); + } + + public function testGetPublicServerKeyException(): void { + $exception = new \Exception(); + $this->signatureHandler->expects($this->once()) + ->method('getPublicServerKey') + ->willThrowException($exception); + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->logger->expects($this->once()) + ->method('logException') + ->willReturn($exception); + + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Internal error'); + + $this->controller->getPublicServerKey(); + } +} diff --git a/tests/Unit/Controller/LockingControllerTest.php b/tests/Unit/Controller/LockingControllerTest.php new file mode 100644 index 00000000..3c182a2e --- /dev/null +++ b/tests/Unit/Controller/LockingControllerTest.php @@ -0,0 +1,235 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Tests\Controller; + +use OC\User\NoUserException; +use OCA\EndToEndEncryption\Controller\LockingController; +use OCA\EndToEndEncryption\Exceptions\FileLockedException; +use OCA\EndToEndEncryption\Exceptions\FileNotLockedException; +use OCA\EndToEndEncryption\FileService; +use OCA\EndToEndEncryption\IMetaDataStorage; +use OCA\EndToEndEncryption\LockManager; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\IRequest; +use Test\TestCase; + +class LockingControllerTest extends TestCase { + + + /** @var string */ + private $appName; + + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + + /** @var string */ + private $userId; + + /** @var IMetaDataStorage|\PHPUnit\Framework\MockObject\MockObject */ + private $metaDataStorage; + + /** @var LockManager|\PHPUnit\Framework\MockObject\MockObject */ + private $lockManager; + + /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */ + private $rootFolder; + + /** @var FileService|\PHPUnit\Framework\MockObject\MockObject */ + private $fileService; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + private $l10n; + + /** @var LockingController */ + private $controller; + + protected function setUp(): void { + parent::setUp(); + + $this->appName = 'end_to_end_encryption'; + $this->request = $this->createMock(IRequest::class); + $this->userId = 'john.doe'; + $this->metaDataStorage = $this->createMock(IMetaDataStorage::class); + $this->lockManager = $this->createMock(LockManager::class); + $this->rootFolder = $this->createMock(IRootFolder::class); + $this->fileService = $this->createMock(FileService::class); + $this->l10n = $this->createMock(IL10N::class); + + $this->controller = new LockingController($this->appName, + $this->request, + $this->userId, + $this->metaDataStorage, + $this->lockManager, + $this->rootFolder, + $this->fileService, + $this->l10n); + } + + public function testLockFolder(): void { + $fileId = 42; + $sendE2E = ''; + $this->request->expects($this->once()) + ->method('getParam') + ->with('e2e-token', '') + ->willReturn(''); + + $this->lockManager->expects($this->once()) + ->method('lockFile') + ->with($fileId, $sendE2E) + ->willReturn('new-token'); + + $response = $this->controller->lockFolder($fileId); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([ + 'e2e-token' => 'new-token', + ], $response->getData()); + } + + public function testLockFolderException(): void { + $fileId = 42; + $sendE2E = ''; + $this->request->expects($this->once()) + ->method('getParam') + ->with('e2e-token', '') + ->willReturn(''); + + $this->lockManager->expects($this->once()) + ->method('lockFile') + ->with($fileId, $sendE2E) + ->willReturn(null); + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->expectException(OCSForbiddenException::class); + $this->expectExceptionMessage('File already locked'); + + $this->controller->lockFolder($fileId); + } + + /** + * @param bool $getUserFolderThrows + * @param bool $userFolderReturnsNodes + * @param \Exception|null $unlockException + * @param string|null $expectedExceptionClass + * @param string|null $expectedExceptionMessage + * + * @dataProvider unlockFolderDataProvider + */ + public function testUnlockFolder(bool $getUserFolderThrows, + bool $userFolderReturnsNodes, + ?\Exception $unlockException, + ?string $expectedExceptionClass, + ?string $expectedExceptionMessage): void { + $fileId = 42; + $sendE2E = 'e2e-token'; + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->request->expects($this->once()) + ->method('getHeader') + ->with('e2e-token') + ->willReturn($sendE2E); + + if ($getUserFolderThrows) { + $this->rootFolder->expects($this->once()) + ->method('getUserFolder') + ->with('john.doe') + ->willThrowException(new NoUserException()); + } else { + $userFolder = $this->createMock(Folder::class); + $this->rootFolder->expects($this->once()) + ->method('getUserFolder') + ->with('john.doe') + ->willReturn($userFolder); + + if (!$userFolderReturnsNodes) { + $userFolder->expects($this->once()) + ->method('getById') + ->with($fileId) + ->willReturn([]); + } else { + $node = $this->createMock(Folder::class); + $userFolder->expects($this->once()) + ->method('getById') + ->with($fileId) + ->willReturn([$node]); + + $this->fileService->expects($this->once()) + ->method('finalizeChanges') + ->with($node); + $this->metaDataStorage->expects($this->once()) + ->method('saveIntermediateFile') + ->with('john.doe', $fileId); + $this->metaDataStorage->expects($this->once()) + ->method('deleteIntermediateFile') + ->with('john.doe', $fileId); + + if ($unlockException) { + $this->lockManager->expects($this->once()) + ->method('unlockFile') + ->with($fileId, $sendE2E) + ->willThrowException($unlockException); + } else { + $this->lockManager->expects($this->once()) + ->method('unlockFile') + ->with($fileId, $sendE2E); + } + } + } + + if ($expectedExceptionClass) { + $this->expectException($expectedExceptionClass); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->unlockFolder($fileId); + } else { + $response = $this->controller->unlockFolder($fileId); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([], $response->getData()); + } + } + + public function unlockFolderDataProvider(): array { + return [ + [false, true, null, null, null], + [true, false, null, OCSForbiddenException::class, 'You are not allowed to remove the lock'], + [false, false, null, OCSForbiddenException::class, 'You are not allowed to remove the lock'], + [false, true, new FileLockedException(), OCSForbiddenException::class, 'You are not allowed to remove the lock'], + [false, true, new FileNotLockedException(), OCSNotFoundException::class, 'File not locked'] + ]; + } +} diff --git a/tests/Unit/Controller/MetaDataControllerTest.php b/tests/Unit/Controller/MetaDataControllerTest.php new file mode 100644 index 00000000..5f0e5a10 --- /dev/null +++ b/tests/Unit/Controller/MetaDataControllerTest.php @@ -0,0 +1,345 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Tests\Controller; + +use OCA\EndToEndEncryption\Controller\MetaDataController; +use OCA\EndToEndEncryption\Exceptions\MetaDataExistsException; +use OCA\EndToEndEncryption\Exceptions\MissingMetaDataException; +use OCA\EndToEndEncryption\IMetaDataStorage; +use OCA\EndToEndEncryption\LockManager; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IRequest; +use Test\TestCase; + +class MetaDataControllerTest extends TestCase { + + + /** @var string */ + private $appName; + + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + + /** @var string */ + private $userId; + + /** @var IMetaDataStorage|\PHPUnit\Framework\MockObject\MockObject */ + private $metaDataStorage; + + /** @var LockManager|\PHPUnit\Framework\MockObject\MockObject */ + private $lockManager; + + /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + private $l10n; + + /** @var MetaDataController */ + private $controller; + + + protected function setUp(): void { + parent::setUp(); + + $this->appName = 'end_to_end_encryption'; + $this->request = $this->createMock(IRequest::class); + $this->userId = 'john.doe'; + $this->metaDataStorage = $this->createMock(IMetaDataStorage::class); + $this->lockManager = $this->createMock(LockManager::class); + $this->logger = $this->createMock(ILogger::class); + $this->l10n = $this->createMock(IL10N::class); + + $this->controller = new MetaDataController($this->appName, + $this->request, + $this->userId, + $this->metaDataStorage, + $this->lockManager, + $this->logger, + $this->l10n); + } + + /** + * @param \Exception|null $metaDataStorageException + * @param string|null $expectedException + * @param string|null $expectedExceptionMessage + * @param bool $expectLogger + * + * @dataProvider getMetaDataDataProvider + */ + public function testGetMetaData(?\Exception $metaDataStorageException, + ?string $expectedException, + ?string $expectedExceptionMessage, + bool $expectLogger): void { + $fileId = 42; + $metaData = 'JSON-ENCODED-META-DATA'; + if ($metaDataStorageException) { + $this->metaDataStorage->expects($this->once()) + ->method('getMetaData') + ->with('john.doe', $fileId) + ->willThrowException($metaDataStorageException); + } else { + $this->metaDataStorage->expects($this->once()) + ->method('getMetaData') + ->with('john.doe', $fileId) + ->willReturn($metaData); + } + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('logException') + ->with($metaDataStorageException, ['app' => $this->appName]); + } + + if ($expectedException) { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->getMetaData($fileId); + } else { + $response = $this->controller->getMetaData($fileId); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([ + 'meta-data' => $metaData + ], $response->getData()); + } + } + + public function getMetaDataDataProvider(): array { + return [ + [null, null, null, false], + [new NotFoundException(), OCSNotFoundException::class, 'Could not find metadata for "42"', false], + [new \Exception(), OCSBadRequestException::class, 'Can\'t read metadata', true], + ]; + } + + /** + * @param \Exception|null $metaDataStorageException + * @param string|null $expectedException + * @param string|null $expectedExceptionMessage + * @param bool $expectLogger + * @param array|null $expectedResponseData + * @param int|null $expectedResponseCode + * + * @dataProvider setMetaDataDataProvider + */ + public function testSetMetaData(?\Exception $metaDataStorageException, + ?string $expectedException, + ?string $expectedExceptionMessage, + bool $expectLogger, + ?array $expectedResponseData, + ?int $expectedResponseCode): void { + $fileId = 42; + $metaData = 'JSON-ENCODED-META-DATA'; + if ($metaDataStorageException) { + $this->metaDataStorage->expects($this->once()) + ->method('setMetaDataIntoIntermediateFile') + ->with('john.doe', $fileId, $metaData) + ->willThrowException($metaDataStorageException); + } else { + $this->metaDataStorage->expects($this->once()) + ->method('setMetaDataIntoIntermediateFile') + ->with('john.doe', $fileId, $metaData); + } + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('logException') + ->with($metaDataStorageException, ['app' => $this->appName]); + } + + if ($expectedException) { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->setMetaData($fileId, $metaData); + } else { + $response = $this->controller->setMetaData($fileId, $metaData); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals($expectedResponseData, $response->getData()); + $this->assertEquals($expectedResponseCode, $response->getStatus()); + } + } + + public function setMetaDataDataProvider(): array { + return [ + [null, null, null, false, ['meta-data' => 'JSON-ENCODED-META-DATA'], 200], + [new MetaDataExistsException(), null, null, false, [], 409], + [new NotFoundException('Exception message'), OCSNotFoundException::class, 'Exception message', false, null, null], + [new \Exception(), OCSBadRequestException::class, 'Can\'t store metadata', true, null, null], + ]; + } + + /** + * @param bool $isLocked + * @param \Exception|null $metaDataStorageException + * @param string|null $expectedException + * @param string|null $expectedExceptionMessage + * @param bool $expectLogger + * + * @dataProvider updateMetaDataDataProvider + */ + public function testUpdateMetaData(bool $isLocked, + ?\Exception $metaDataStorageException, + ?string $expectedException, + ?string $expectedExceptionMessage, + bool $expectLogger): void { + $fileId = 42; + $sendToken = 'sendE2EToken'; + $metaData = 'JSON-ENCODED-META-DATA'; + $this->request->expects($this->once()) + ->method('getParam') + ->with('e2e-token') + ->willReturn($sendToken); + + $this->lockManager->expects($this->once()) + ->method('isLocked') + ->with($fileId, $sendToken) + ->willReturn($isLocked); + + if (!$isLocked) { + if ($metaDataStorageException) { + $this->metaDataStorage->expects($this->once()) + ->method('updateMetaDataIntoIntermediateFile') + ->with('john.doe', $fileId, $metaData) + ->willThrowException($metaDataStorageException); + } else { + $this->metaDataStorage->expects($this->once()) + ->method('updateMetaDataIntoIntermediateFile') + ->with('john.doe', $fileId, $metaData); + } + } + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('logException') + ->with($metaDataStorageException, ['app' => $this->appName]); + } + + if ($expectedException) { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->updateMetaData($fileId, $metaData); + } else { + $response = $this->controller->updateMetaData($fileId, $metaData); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([ + 'meta-data' => $metaData, + ], $response->getData()); + } + } + + public function updateMetaDataDataProvider(): array { + return [ + [false, null, null, null, false], + [true, null, OCSForbiddenException::class, 'You are not allowed to edit the file, make sure to first lock it, and then send the right token', false], + [false, new MissingMetaDataException(), OCSNotFoundException::class, 'Metadata-file doesn\'t exist', false], + [false, new NotFoundException('Exception Message'), OCSNotFoundException::class, 'Exception Message', false], + [false, new \Exception(), OCSBadRequestException::class, 'Can\'t store metadata', true], + ]; + } + + /** + * @param \Exception|null $metaDataStorageException + * @param string|null $expectedException + * @param string|null $expectedExceptionMessage + * @param bool $expectLogger + * + * @dataProvider deleteMetaDataDataProvider + */ + public function testDeleteMetaData(?\Exception $metaDataStorageException, + ?string $expectedException, + ?string $expectedExceptionMessage, + bool $expectLogger): void { + $fileId = 42; + if ($metaDataStorageException) { + $this->metaDataStorage->expects($this->once()) + ->method('deleteMetaData') + ->with('john.doe', $fileId) + ->willThrowException($metaDataStorageException); + } else { + $this->metaDataStorage->expects($this->once()) + ->method('deleteMetaData') + ->with('john.doe', $fileId); + } + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(static function ($string, $args) { + return vsprintf($string, $args); + }); + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('logException') + ->with($metaDataStorageException, ['app' => $this->appName]); + } + + if ($expectedException) { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->controller->deleteMetaData($fileId); + } else { + $response = $this->controller->deleteMetaData($fileId); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertEquals([], $response->getData()); + } + } + + public function deleteMetaDataDataProvider(): array { + return [ + [null, null, null, false], + [new NotFoundException(), OCSNotFoundException::class, 'Could not find metadata for "42"', false], + [new NotPermittedException(), OCSForbiddenException::class, 'Only the owner can delete the metadata-file', false], + [new \Exception(), OCSBadRequestException::class, 'Can\'t delete metadata', true], + ]; + } +} diff --git a/tests/Unit/Controller/RequestHandlerControllerTest.php b/tests/Unit/Controller/RequestHandlerControllerTest.php deleted file mode 100644 index a2b6cfea..00000000 --- a/tests/Unit/Controller/RequestHandlerControllerTest.php +++ /dev/null @@ -1,173 +0,0 @@ - - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - - -namespace OCA\EndToEndEncryption\Tests\Controller; - -use BadMethodCallException; -use OCA\EndToEndEncryption\Controller\RequestHandlerController; -use OCA\EndToEndEncryption\EncryptionManager; -use OCA\EndToEndEncryption\FileService; -use OCA\EndToEndEncryption\IKeyStorage; -use OCA\EndToEndEncryption\IMetaDataStorage; -use OCA\EndToEndEncryption\LockManager; -use OCA\EndToEndEncryption\SignatureHandler; -use OCP\AppFramework\Http; -use OCP\AppFramework\OCS\OCSBadRequestException; -use OCP\AppFramework\OCS\OCSForbiddenException; -use OCP\Files\IRootFolder; -use OCP\IL10N; -use OCP\ILogger; -use OCP\IRequest; -use PHPUnit_Framework_MockObject_MockObject; -use Test\TestCase; - -class RequestHandlerControllerTest extends TestCase { - - /** @var IRequest|PHPUnit_Framework_MockObject_MockObject */ - private $request; - - /** @var IKeyStorage|PHPUnit_Framework_MockObject_MockObject */ - private $keyStorage; - - /** @var IMetaDataStorage|\PHPUnit\Framework\MockObject\MockObject */ - private $metaDataStorage; - - /** @var SignatureHandler|PHPUnit_Framework_MockObject_MockObject */ - private $signatureHandler; - - /** @var EncryptionManager|PHPUnit_Framework_MockObject_MockObject */ - private $encryptionManager; - - /** @var LockManager|PHPUnit_Framework_MockObject_MockObject */ - private $lockManager; - - /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */ - private $rootFolder; - - /** @var FileService|\PHPUnit\Framework\MockObject\MockObject */ - private $fileService; - - /** @var ILogger|PHPUnit_Framework_MockObject_MockObject */ - private $logger; - - /** @var IL10N|PHPUnit_Framework_MockObject_MockObject */ - private $l10n; - - /** @var string valid CSR (CN set to "admin") */ - private $validCSR = "-----BEGIN CERTIFICATE REQUEST----- -MIIC7jCCAdYCAQAwgagxCzAJBgNVBAYTAlVLMREwDwYDVQQIDAhTb21lcnNldDEU -MBIGA1UEBwwLR2xhc3RvbmJ1cnkxHzAdBgNVBAoMFlRoZSBCcmFpbiBSb29tIExp -bWl0ZWQxHzAdBgNVBAsMFlBIUCBEb2N1bWVudGF0aW9uIFRlYW0xDjAMBgNVBAMM -BWFkbWluMR4wHAYJKoZIhvcNAQkBFg93ZXpAZXhhbXBsZS5jb20wggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHWU8rWlK3lud/r5OQoilxypgIzbBf5pqM -H0rpYwFv3uctnK5Lt3M+WY45XdJt98Pq8eQ0AbyAf3IuhnpF+X2Ej3QnCenZ0H+B -J6/mZXdo9f7IXa2wH5LtA2cmm1XWQWubN/Jzr9psq+kxbocyGTQhNGeeB2OPcgyl -73eddJNIbFVlNEzbdcBNNsSwKcB+LP/JyJ9e1HZ4af6CHdX2SG1HvO+dICdEuO2E -mC9lM896MJFWwNns5mx453Y1FmxFmAi1zQAAP+AZ5Taqy6yCzqJ9Y4/FDRi1NC5V -stnu9REuPYSS8YgsJwQE/DUd+I+UonkcDfac8PIH5p5YHpMq0ChvAgMBAAGgADAN -BgkqhkiG9w0BAQUFAAOCAQEAh8YVAsAcPR5v7kv96UtkVI4xK6R9BdmVsnisxTpm -g9JVbfji7kpxbSgXfRSozTG3bl9ynrck39/2SoFQGSGrW2iV+drclftSk+uBFb1F -iXYEWJxYSz2CcUeijoBrBsarfmODgOHzmgXmCoOToz2DkdtM7g9INWkC06Do4pTQ -fqA3PS2td1gWqQCQthF9IWOCIxNI16lokVTgNCZKewXsn9Bjm3hsLLeJU9jBXyVN -w7829dr37SuA2kQb86aVpqdL50v3HjCclXd7PfWiYqajuHaIsokBV5ly2IdQo4Cz -AYzYQFPtjsDZ4Tju4VZKM4YpF2GwQgT7zhzDBvywGPqvfw== ------END CERTIFICATE REQUEST----- -"; - - /** @var string */ - private $invalidCSR = "-----BEGIN CERTIFICATE REQUEST-----\nMIIC7jCCAdYCAQAwgagxCzAJBgNVBAYTAlVLMREwDwYDVQQIDAhTb21lcnNldDEU\nMBIGA1UEBwwLR2xhc3RvbmJ1cnkxHzAdBgNVBAoMFlRoZSBCcmFpbiBSb29tIExp\nbWl0ZWQxHzAdBgNVBAsMFlBIUCBEb2N1bWVudGF0aW9uIFRlYW0xDjAMBgNVBAMM\nBWFkbWluMR4wHAYJKoZIhvcNAQkBFg93ZXpAZXhhbXBsZS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHWU8rWlK3lud%2Fr5OQoilxypgIzbBf5pqM\nH0rpYwFv3uctnK5Lt3M%2BWY45XdJt98Pq8eQ0AbyAf3IuhnpF%2BX2Ej3QnCenZ0H%2BB\nJ6%2FmZXdo9f7IXa2wH5LtA2cmm1XWQWubN%2FJzr9psq%2BkxbocyGTQhNGeeB2OPcgyl\n73eddJNIbFVlNEzbdcBNNsSwKcB%2BLP%2FJyJ9e1HZ4af6CHdX2SG1HvO%2BdICdEuO2E\nmC9lM896MJFWwNns5mx453Y1FmxFmAi1zQAAP%2BAZ5Taqy6yCzqJ9Y4%2FFDRi1NC5V\nstnu9REuPYSS8YgsJwQE%2FDUd%2BI%2BUonkcDfac8PIH5p5YHpMq0ChvAgMBAAGgADAN\nBgkqhkiG9w0BAQUFAAOCAQEAh8YVAsAcPR5v7kv96UtkVI4xK6R9BdmVsnisxTpm\ng9JVbfji7kpxbSgXfRSozTG3bl9ynrck39%2F2SoFQGSGrW2iV%2BdrclftSk%2BuBFb1F\niXYEWJxYSz2CcUeijoBrBsarfmODgOHzmgXmCoOToz2DkdtM7g9INWkC06Do4pTQ\nfqA3PS2td1gWqQCQthF9IWOCIxNI16lokVTgNCZKewXsn9Bjm3hsLLeJU9jBXyVN\nw7829dr37SuA2kQb86aVpqdL50v3HjCclXd7PfWiYqajuHaIsokBV5ly2IdQo4Cz\nAYzYQFPtjsDZ4Tju4VZKM4YpF2GwQgT7zhzDBvywGPqvfw%3D%3D\n-----END+CERTIFICATE+REQUEST-----\n"; - - protected function setUp(): void { - parent::setUp(); - - $this->request = $this->createMock(IRequest::class); - $this->keyStorage = $this->getMockBuilder(IKeyStorage::class) - ->disableOriginalConstructor()->getMock(); - $this->metaDataStorage = $this->createMock(IMetaDataStorage::class); - $this->signatureHandler = $this->getMockBuilder(SignatureHandler::class) - ->disableOriginalConstructor()->getMock(); - $this->encryptionManager = $this->getMockBuilder(EncryptionManager::class) - ->disableOriginalConstructor()->getMock(); - $this->lockManager = $this->getMockBuilder(LockManager::class) - ->disableOriginalConstructor()->getMock(); - $this->rootFolder = $this->createMock(IRootFolder::class); - $this->fileService = $this->createMock(FileService::class); - $this->logger = $this->createMock(ILogger::class); - $this->l10n = $this->createMock(IL10N::class); - } - - /** - * get controller - * - * @param $uid user who calls the controller - * @return RequestHandlerController - */ - private function getController($uid): RequestHandlerController { - return new RequestHandlerController( - 'e2e', - $this->request, - $uid, - $this->keyStorage, - $this->metaDataStorage, - $this->signatureHandler, - $this->encryptionManager, - $this->lockManager, - $this->rootFolder, - $this->fileService, - $this->logger, - $this->l10n - ); - } - - /** - * test public key, valid crs and valid cn - */ - public function testCreatePublicKeySuccessful(): void { - $controller = $this->getController('admin'); - $result = $controller->createPublicKey($this->validCSR); - $this->assertSame(Http::STATUS_OK, $result->getStatus()); - } - - /** - * test public key, valid crs but invalid cn - */ - public function testCreatePublicKeyInvalidCN(): void { - $this->expectException(OCSForbiddenException::class); - - $controller = $this->getController('user1'); - $controller->createPublicKey($this->validCSR); - } - - /** - * test public key, invalid crs - */ - public function testCreatePublicKeyInvalidCSR(): void { - $this->expectException(OCSBadRequestException::class); - - $controller = $this->getController('admin'); - $this->signatureHandler->expects($this->once())->method('sign') - ->with($this->invalidCSR)->willThrowException(new BadMethodCallException()); - $controller->createPublicKey($this->invalidCSR); - } -} diff --git a/tests/Unit/Middleware/UserAgentCheckMiddlewareTest.php b/tests/Unit/Middleware/UserAgentCheckMiddlewareTest.php new file mode 100644 index 00000000..2e9ec2db --- /dev/null +++ b/tests/Unit/Middleware/UserAgentCheckMiddlewareTest.php @@ -0,0 +1,96 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EndToEndEncryption\Tests\Unit\Middleware; + +use OCA\EndToEndEncryption\Middleware\UserAgentCheckMiddleware; +use OCA\EndToEndEncryption\UserAgentManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\Utility\IControllerMethodReflector; +use OCP\IRequest; +use Test\TestCase; + +class UserAgentCheckMiddlewareTest extends TestCase { + + /** @var IControllerMethodReflector|\PHPUnit\Framework\MockObject\MockObject */ + private $reflector; + + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + + /** @var UserAgentManager|\PHPUnit\Framework\MockObject\MockObject */ + private $userAgentManager; + + /** @var UserAgentCheckMiddleware */ + private $middleware; + + protected function setUp(): void { + parent::setUp(); + + $this->reflector = $this->createMock(IControllerMethodReflector::class); + $this->request = $this->createMock(IRequest::class); + $this->userAgentManager = $this->createMock(UserAgentManager::class); + + $this->middleware = new UserAgentCheckMiddleware($this->reflector, $this->request, $this->userAgentManager); + } + + /** + * @param bool $hasAnnotation + * @param bool $supportsE2E + * @param bool $expectException + * + * @dataProvider beforeControllerDataProvider + */ + public function testBeforeController(bool $hasAnnotation, bool $supportsE2E, bool $expectException) { + $this->request->expects($this->once()) + ->method('getHeader') + ->with('user-agent') + ->willReturn('user-agent-string'); + + $this->reflector->method('hasAnnotation') + ->with('E2ERestrictUserAgent') + ->willReturn($hasAnnotation); + $this->userAgentManager->method('supportsEndToEndEncryption') + ->with('user-agent-string') + ->willReturn($supportsE2E); + + if ($expectException) { + $this->expectException(OCSForbiddenException::class); + $this->expectExceptionMessage('Client "user-agent-string" is not allowed to access end-to-end encrypted content.'); + } + + $controller = $this->createMock(Controller::class); + + $this->middleware->beforeController($controller, 'methodName'); + } + + public function beforeControllerDataProvider(): array { + return [ + [false, false, false], + [false, true, false], + [true, false, true], + [true, true, false], + ]; + } +}