diff --git a/core/command/encryption/changekeystorageroot.php b/core/command/encryption/changekeystorageroot.php new file mode 100644 index 0000000000..662e0a3161 --- /dev/null +++ b/core/command/encryption/changekeystorageroot.php @@ -0,0 +1,270 @@ + + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + + +namespace OC\Core\Command\Encryption; + +use OC\Encryption\Keys\Storage; +use OC\Encryption\Util; +use OC\Files\Filesystem; +use OC\Files\View; +use OCP\IConfig; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class ChangeKeyStorageRoot extends Command { + + /** @var View */ + protected $rootView; + + /** @var IUserManager */ + protected $userManager; + + /** @var IConfig */ + protected $config; + + /** @var Util */ + protected $util; + + /** @var QuestionHelper */ + protected $questionHelper; + + /** + * @param View $view + * @param IUserManager $userManager + * @param IConfig $config + * @param Util $util + * @param QuestionHelper $questionHelper + */ + public function __construct(View $view, IUserManager $userManager, IConfig $config, Util $util, QuestionHelper $questionHelper) { + parent::__construct(); + $this->rootView = $view; + $this->userManager = $userManager; + $this->config = $config; + $this->util = $util; + $this->questionHelper = $questionHelper; + } + + protected function configure() { + parent::configure(); + $this + ->setName('encryption:change-key-storage-root') + ->setDescription('Change key storage root') + ->addArgument( + 'newRoot', + InputArgument::OPTIONAL, + 'new root of the key storage relative to the data folder' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $oldRoot = $this->util->getKeyStorageRoot(); + $newRoot = $input->getArgument('newRoot'); + + if ($newRoot === null) { + $question = new ConfirmationQuestion('No storage root given, do you want to reset the key storage root to the default location? (y/n) ', false); + if (!$this->questionHelper->ask($input, $output, $question)) { + return; + } + $newRoot = ''; + } + + $oldRootDescription = $oldRoot !== '' ? $oldRoot : 'default storage location'; + $newRootDescription = $newRoot !== '' ? $newRoot : 'default storage location'; + $output->writeln("Change key storage root from $oldRootDescription to $newRootDescription"); + $success = $this->moveAllKeys($oldRoot, $newRoot, $output); + if ($success) { + $this->util->setKeyStorageRoot($newRoot); + $output->writeln(''); + $output->writeln("Key storage root successfully changed to $newRootDescription"); + } + } + + /** + * move keys to new key storage root + * + * @param string $oldRoot + * @param string $newRoot + * @param OutputInterface $output + * @return bool + * @throws \Exception + */ + protected function moveAllKeys($oldRoot, $newRoot, OutputInterface $output) { + + $output->writeln("Start to move keys:"); + + if ($this->rootView->is_dir(($oldRoot)) === false) { + $output->writeln("No old keys found: Nothing needs to be moved"); + return false; + } + + $this->prepareNewRoot($newRoot); + $this->moveSystemKeys($oldRoot, $newRoot); + $this->moveUserKeys($oldRoot, $newRoot, $output); + + return true; + } + + /** + * prepare new key storage + * + * @param string $newRoot + * @throws \Exception + */ + protected function prepareNewRoot($newRoot) { + if ($this->rootView->is_dir($newRoot) === false) { + throw new \Exception("New root folder doesn't exist. Please create the folder or check the permissions and try again."); + } + + $result = $this->rootView->file_put_contents( + $newRoot . '/' . Storage::KEY_STORAGE_MARKER, + 'ownCloud will detect this folder as key storage root only if this file exists' + ); + + if ($result === false) { + throw new \Exception("Can't write to new root folder. Please check the permissions and try again"); + } + + } + + + /** + * move system key folder + * + * @param string $oldRoot + * @param string $newRoot + */ + protected function moveSystemKeys($oldRoot, $newRoot) { + if ( + $this->rootView->is_dir($oldRoot . '/files_encryption') && + $this->targetExists($newRoot . '/files_encryption') === false + ) { + $this->rootView->rename($oldRoot . '/files_encryption', $newRoot . '/files_encryption'); + } + } + + + /** + * setup file system for the given user + * + * @param string $uid + */ + protected function setupUserFS($uid) { + \OC_Util::tearDownFS(); + \OC_Util::setupFS($uid); + } + + + /** + * iterate over each user and move the keys to the new storage + * + * @param string $oldRoot + * @param string $newRoot + * @param OutputInterface $output + */ + protected function moveUserKeys($oldRoot, $newRoot, OutputInterface $output) { + + $progress = new ProgressBar($output); + $progress->start(); + + + foreach($this->userManager->getBackends() as $backend) { + $limit = 500; + $offset = 0; + do { + $users = $backend->getUsers('', $limit, $offset); + foreach ($users as $user) { + $progress->advance(); + $this->setupUserFS($user); + $this->moveUserEncryptionFolder($user, $oldRoot, $newRoot); + } + $offset += $limit; + } while(count($users) >= $limit); + } + $progress->finish(); + } + + /** + * move user encryption folder to new root folder + * + * @param string $user + * @param string $oldRoot + * @param string $newRoot + * @throws \Exception + */ + protected function moveUserEncryptionFolder($user, $oldRoot, $newRoot) { + + if ($this->userManager->userExists($user)) { + + $source = $oldRoot . '/' . $user . '/files_encryption'; + $target = $newRoot . '/' . $user . '/files_encryption'; + if ( + $this->rootView->is_dir($source) && + $this->targetExists($target) === false + ) { + $this->prepareParentFolder($newRoot . '/' . $user); + $this->rootView->rename($source, $target); + } + } + } + + /** + * Make preparations to filesystem for saving a key file + * + * @param string $path relative to data/ + */ + protected function prepareParentFolder($path) { + $path = Filesystem::normalizePath($path); + // If the file resides within a subdirectory, create it + if ($this->rootView->file_exists($path) === false) { + $sub_dirs = explode('/', ltrim($path, '/')); + $dir = ''; + foreach ($sub_dirs as $sub_dir) { + $dir .= '/' . $sub_dir; + if ($this->rootView->file_exists($dir) === false) { + $this->rootView->mkdir($dir); + } + } + } + } + + /** + * check if target already exists + * + * @param $path + * @return bool + * @throws \Exception + */ + protected function targetExists($path) { + if ($this->rootView->file_exists($path)) { + throw new \Exception("new folder '$path' already exists"); + } + + return false; + } + +} diff --git a/core/command/encryption/showkeystorageroot.php b/core/command/encryption/showkeystorageroot.php new file mode 100644 index 0000000000..acb2e75a6a --- /dev/null +++ b/core/command/encryption/showkeystorageroot.php @@ -0,0 +1,58 @@ + + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + + +namespace OC\Core\Command\Encryption; + +use OC\Encryption\Util; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ShowKeyStorageRoot extends Command{ + + /** @var Util */ + protected $util; + + /** + * @param Util $util + */ + public function __construct(Util $util) { + parent::__construct(); + $this->util = $util; + } + + protected function configure() { + parent::configure(); + $this + ->setName('encryption:show-key-storage-root') + ->setDescription('Show current key storage root'); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $currentRoot = $this->util->getKeyStorageRoot(); + + $rootDescription = $currentRoot !== '' ? $currentRoot : 'default storage location (data/)'; + + $output->writeln("Current key storage root: $rootDescription"); + } + +} diff --git a/core/register_command.php b/core/register_command.php index 984e1b97f6..72c7b28e9a 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -62,6 +62,23 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\Log\Manage(\OC::$server->getConfig())); $application->add(new OC\Core\Command\Log\OwnCloud(\OC::$server->getConfig())); + $view = new \OC\Files\View(); + $util = new \OC\Encryption\Util( + $view, + \OC::$server->getUserManager(), + \OC::$server->getGroupManager(), + \OC::$server->getConfig() + ); + $application->add(new OC\Core\Command\Encryption\ChangeKeyStorageRoot( + $view, + \OC::$server->getUserManager(), + \OC::$server->getConfig(), + $util, + new \Symfony\Component\Console\Helper\QuestionHelper() + ) + ); + $application->add(new OC\Core\Command\Encryption\ShowKeyStorageRoot($util)); + $application->add(new OC\Core\Command\Maintenance\MimeTypesJS()); $application->add(new OC\Core\Command\Maintenance\Mode(\OC::$server->getConfig())); $application->add(new OC\Core\Command\Maintenance\Repair(new \OC\Repair(\OC\Repair::getRepairSteps()), \OC::$server->getConfig())); diff --git a/lib/private/encryption/keys/storage.php b/lib/private/encryption/keys/storage.php index b754462d9b..d0c094538b 100644 --- a/lib/private/encryption/keys/storage.php +++ b/lib/private/encryption/keys/storage.php @@ -27,11 +27,13 @@ namespace OC\Encryption\Keys; use OC\Encryption\Util; use OC\Files\Filesystem; use OC\Files\View; -use OCP\Encryption\Exceptions\GenericEncryptionException; use OCP\Encryption\Keys\IStorage; class Storage implements IStorage { + // hidden file which indicate that the folder is a valid key storage + const KEY_STORAGE_MARKER = '.oc_key_storage'; + /** @var View */ private $view; @@ -42,6 +44,10 @@ class Storage implements IStorage { /** @var string */ private $keys_base_dir; + // root of the key storage default is empty which means that we use the data folder + /** @var string */ + private $root_dir; + /** @var string */ private $encryption_base_dir; @@ -58,6 +64,7 @@ class Storage implements IStorage { $this->encryption_base_dir = '/files_encryption'; $this->keys_base_dir = $this->encryption_base_dir .'/keys'; + $this->root_dir = $this->util->getKeyStorageRoot(); } /** @@ -162,13 +169,13 @@ class Storage implements IStorage { protected function constructUserKeyPath($encryptionModuleId, $keyId, $uid) { if ($uid === null) { - $path = $this->encryption_base_dir . '/' . $encryptionModuleId . '/' . $keyId; + $path = $this->root_dir . '/' . $this->encryption_base_dir . '/' . $encryptionModuleId . '/' . $keyId; } else { - $path = '/' . $uid . $this->encryption_base_dir . '/' + $path = $this->root_dir . '/' . $uid . $this->encryption_base_dir . '/' . $encryptionModuleId . '/' . $uid . '.' . $keyId; } - return $path; + return \OC\Files\Filesystem::normalizePath($path); } /** @@ -227,9 +234,9 @@ class Storage implements IStorage { // in case of system wide mount points the keys are stored directly in the data directory if ($this->util->isSystemWideMountPoint($filename, $owner)) { - $keyPath = $this->keys_base_dir . $filename . '/'; + $keyPath = $this->root_dir . '/' . $this->keys_base_dir . $filename . '/'; } else { - $keyPath = '/' . $owner . $this->keys_base_dir . $filename . '/'; + $keyPath = $this->root_dir . '/' . $owner . $this->keys_base_dir . $filename . '/'; } return Filesystem::normalizePath($keyPath . $encryptionModuleId . '/', false); @@ -290,12 +297,12 @@ class Storage implements IStorage { $systemWideMountPoint = $this->util->isSystemWideMountPoint($relativePath, $owner); if ($systemWideMountPoint) { - $systemPath = $this->keys_base_dir . $relativePath . '/'; + $systemPath = $this->root_dir . '/' . $this->keys_base_dir . $relativePath . '/'; } else { - $systemPath = '/' . $owner . $this->keys_base_dir . $relativePath . '/'; + $systemPath = $this->root_dir . '/' . $owner . $this->keys_base_dir . $relativePath . '/'; } - return $systemPath; + return Filesystem::normalizePath($systemPath, false); } /** diff --git a/lib/private/encryption/manager.php b/lib/private/encryption/manager.php index 1e0a065e25..c004dfda0d 100644 --- a/lib/private/encryption/manager.php +++ b/lib/private/encryption/manager.php @@ -25,14 +25,12 @@ namespace OC\Encryption; +use OC\Encryption\Keys\Storage; use OC\Files\Filesystem; -use OC\Files\Storage\Shared; -use OC\Files\Storage\Wrapper\Encryption; use OC\Files\View; -use OC\Search\Provider\File; +use OC\ServiceUnavailableException; use OCP\Encryption\IEncryptionModule; use OCP\Encryption\IManager; -use OCP\Files\Mount\IMountPoint; use OCP\IConfig; use OCP\IL10N; use OCP\ILogger; @@ -51,16 +49,26 @@ class Manager implements IManager { /** @var Il10n */ protected $l; + /** @var View */ + protected $rootView; + + /** @var Util */ + protected $util; + /** * @param IConfig $config * @param ILogger $logger * @param IL10N $l10n + * @param View $rootView + * @param Util $util */ - public function __construct(IConfig $config, ILogger $logger, IL10N $l10n) { + public function __construct(IConfig $config, ILogger $logger, IL10N $l10n, View $rootView, Util $util) { $this->encryptionModules = array(); $this->config = $config; $this->logger = $logger; $this->l = $l10n; + $this->rootView = $rootView; + $this->util = $util; } /** @@ -82,7 +90,8 @@ class Manager implements IManager { /** * check if new encryption is ready * - * @return boolean + * @return bool + * @throws ServiceUnavailableException */ public function isReady() { // check if we are still in transit between the old and the new encryption @@ -94,6 +103,11 @@ class Manager implements IManager { $this->logger->warning($warning); return false; } + + if ($this->isKeyStorageReady() === false) { + throw new ServiceUnavailableException('Key Storage is not ready'); + } + return true; } @@ -221,6 +235,31 @@ class Manager implements IManager { \OC::$server->getGroupManager(), \OC::$server->getConfig() ); - \OC\Files\Filesystem::addStorageWrapper('oc_encryption', array($util, 'wrapStorage'), 2); + Filesystem::addStorageWrapper('oc_encryption', array($util, 'wrapStorage'), 2); } + + + /** + * check if key storage is ready + * + * @return bool + */ + protected function isKeyStorageReady() { + + $rootDir = $this->util->getKeyStorageRoot(); + + // the default root is always valid + if ($rootDir === '') { + return true; + } + + // check if key storage is mounted correctly + if ($this->rootView->file_exists($rootDir . '/' . Storage::KEY_STORAGE_MARKER)) { + return true; + } + + return false; + } + + } diff --git a/lib/private/encryption/util.php b/lib/private/encryption/util.php index d0733941a3..90ae825997 100644 --- a/lib/private/encryption/util.php +++ b/lib/private/encryption/util.php @@ -59,7 +59,7 @@ class Util { protected $blockSize = 8192; /** @var View */ - protected $view; + protected $rootView; /** @var array */ protected $ocHeaderKeys; @@ -78,13 +78,13 @@ class Util { /** * - * @param \OC\Files\View $view + * @param View $rootView * @param \OC\User\Manager $userManager * @param \OC\Group\Manager $groupManager * @param IConfig $config */ public function __construct( - \OC\Files\View $view, + View $rootView, \OC\User\Manager $userManager, \OC\Group\Manager $groupManager, IConfig $config) { @@ -93,7 +93,7 @@ class Util { self::HEADER_ENCRYPTION_MODULE_KEY ]; - $this->view = $view; + $this->rootView = $rootView; $this->userManager = $userManager; $this->groupManager = $groupManager; $this->config = $config; @@ -167,7 +167,7 @@ class Util { while ($dirList) { $dir = array_pop($dirList); - $content = $this->view->getDirectoryContent($dir); + $content = $this->rootView->getDirectoryContent($dir); foreach ($content as $c) { if ($c->getType() === 'dir') { @@ -332,10 +332,22 @@ class Util { * @return boolean */ public function isExcluded($path) { - $normalizedPath = \OC\Files\Filesystem::normalizePath($path); + $normalizedPath = Filesystem::normalizePath($path); $root = explode('/', $normalizedPath, 4); if (count($root) > 1) { + // detect alternative key storage root + $rootDir = $this->getKeyStorageRoot(); + if ($rootDir !== '' && + 0 === strpos( + Filesystem::normalizePath($path), + Filesystem::normalizePath($rootDir) + ) + ) { + return true; + } + + //detect system wide folders if (in_array($root[1], $this->excludedPaths)) { return true; @@ -363,6 +375,24 @@ class Util { return ($enabled === '1') ? true : false; } + /** + * set new key storage root + * + * @param string $root new key store root relative to the data folder + */ + public function setKeyStorageRoot($root) { + $this->config->setAppValue('core', 'encryption_key_storage_root', $root); + } + + /** + * get key storage root + * + * @return string key storage root + */ + public function getKeyStorageRoot() { + return $this->config->getAppValue('core', 'encryption_key_storage_root', ''); + } + /** * Wraps the given storage when it is not a shared storage * diff --git a/lib/private/server.php b/lib/private/server.php index a47fa2e43f..6aab3d26df 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -91,12 +91,25 @@ class Server extends SimpleContainer implements IServerContainer { }); $this->registerService('EncryptionManager', function (Server $c) { - return new Encryption\Manager($c->getConfig(), $c->getLogger(), $c->getL10N('core')); + $view = new View(); + $util = new Encryption\Util( + $view, + $c->getUserManager(), + $c->getGroupManager(), + $c->getConfig() + ); + return new Encryption\Manager( + $c->getConfig(), + $c->getLogger(), + $c->getL10N('core'), + new View(), + $util + ); }); $this->registerService('EncryptionFileHelper', function (Server $c) { - $util = new \OC\Encryption\Util( - new \OC\Files\View(), + $util = new Encryption\Util( + new View(), $c->getUserManager(), $c->getGroupManager(), $c->getConfig() @@ -105,8 +118,8 @@ class Server extends SimpleContainer implements IServerContainer { }); $this->registerService('EncryptionKeyStorage', function (Server $c) { - $view = new \OC\Files\View(); - $util = new \OC\Encryption\Util( + $view = new View(); + $util = new Encryption\Util( $view, $c->getUserManager(), $c->getGroupManager(), @@ -326,7 +339,7 @@ class Server extends SimpleContainer implements IServerContainer { $uid = $user ? $user : null; return new ClientService( $c->getConfig(), - new \OC\Security\CertificateManager($uid, new \OC\Files\View()) + new \OC\Security\CertificateManager($uid, new View()) ); }); $this->registerService('EventLogger', function (Server $c) { @@ -839,7 +852,7 @@ class Server extends SimpleContainer implements IServerContainer { } $userId = $user->getUID(); } - return new CertificateManager($userId, new \OC\Files\View()); + return new CertificateManager($userId, new View()); } /** diff --git a/tests/core/command/encryption/changekeystorageroottest.php b/tests/core/command/encryption/changekeystorageroottest.php new file mode 100644 index 0000000000..6cb52cdea9 --- /dev/null +++ b/tests/core/command/encryption/changekeystorageroottest.php @@ -0,0 +1,381 @@ + + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + + +namespace Tests\Core\Command\Encryption; + + +use OC\Core\Command\Encryption\ChangeKeyStorageRoot; +use OC\Encryption\Util; +use OC\Files\View; +use OCP\IConfig; +use OCP\IUserManager; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +class ChangeKeyStorageRootTest extends TestCase { + + /** @var ChangeKeyStorageRoot */ + protected $changeKeyStorageRoot; + + /** @var View | \PHPUnit_Framework_MockObject_MockObject */ + protected $view; + + /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */ + protected $userManager; + + /** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */ + protected $config; + + /** @var Util | \PHPUnit_Framework_MockObject_MockObject */ + protected $util; + + /** @var QuestionHelper | \PHPUnit_Framework_MockObject_MockObject */ + protected $questionHelper; + + /** @var InputInterface | \PHPUnit_Framework_MockObject_MockObject */ + protected $inputInterface; + + /** @var OutputInterface | \PHPUnit_Framework_MockObject_MockObject */ + protected $outputInterface; + + /** @var \OCP\UserInterface | \PHPUnit_Framework_MockObject_MockObject */ + protected $userInterface; + + public function setUp() { + parent::setUp(); + + $this->view = $this->getMock('\OC\Files\View'); + $this->userManager = $this->getMock('\OCP\IUserManager'); + $this->config = $this->getMock('\OCP\IConfig'); + $this->util = $this->getMockBuilder('OC\Encryption\Util')->disableOriginalConstructor()->getMock(); + $this->questionHelper = $this->getMock('Symfony\Component\Console\Helper\QuestionHelper'); + $this->inputInterface = $this->getMock('Symfony\Component\Console\Input\InputInterface'); + $this->outputInterface = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + $this->userInterface = $this->getMock('\OCP\UserInterface'); + + $outputFormaterInterface = $this->getMock('Symfony\Component\Console\Formatter\OutputFormatterInterface'); + $this->outputInterface->expects($this->any())->method('getFormatter') + ->willReturn($outputFormaterInterface); + + $this->changeKeyStorageRoot = new ChangeKeyStorageRoot( + $this->view, + $this->userManager, + $this->config, + $this->util, + $this->questionHelper + ); + + } + + /** + * @dataProvider dataTestExecute + */ + public function testExecute($newRoot, $answer, $successMoveKey) { + + $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') + ->setConstructorArgs( + [ + $this->view, + $this->userManager, + $this->config, + $this->util, + $this->questionHelper + ] + )->setMethods(['moveAllKeys'])->getMock(); + + $this->util->expects($this->once())->method('getKeyStorageRoot') + ->willReturn(''); + $this->inputInterface->expects($this->once())->method('getArgument') + ->with('newRoot')->willReturn($newRoot); + + if ($answer === true || $newRoot !== null) { + $changeKeyStorageRoot->expects($this->once())->method('moveAllKeys') + ->willReturn($successMoveKey); + } else { + $changeKeyStorageRoot->expects($this->never())->method('moveAllKeys'); + } + + if ($successMoveKey === true) { + $this->util->expects($this->once())->method('setKeyStorageRoot'); + } else { + $this->util->expects($this->never())->method('setKeyStorageRoot'); + } + + if ($newRoot === null) { + $this->questionHelper->expects($this->once())->method('ask')->willReturn($answer); + } else { + $this->questionHelper->expects($this->never())->method('ask'); + } + + $this->invokePrivate( + $changeKeyStorageRoot, + 'execute', + [$this->inputInterface, $this->outputInterface] + ); + } + + public function dataTestExecute() { + return [ + [null, true, true], + [null, true, false], + [null, false, null], + ['/newRoot', null, true], + ['/newRoot', null, false] + ]; + } + + public function testMoveAllKeys() { + + /** @var \OC\Core\Command\Encryption\ChangeKeyStorageRoot $changeKeyStorageRoot */ + $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') + ->setConstructorArgs( + [ + $this->view, + $this->userManager, + $this->config, + $this->util, + $this->questionHelper + ] + )->setMethods(['prepareNewRoot', 'moveSystemKeys', 'moveUserKeys'])->getMock(); + + $changeKeyStorageRoot->expects($this->at(0))->method('prepareNewRoot')->with('newRoot'); + $changeKeyStorageRoot->expects($this->at(1))->method('moveSystemKeys')->with('oldRoot', 'newRoot'); + $changeKeyStorageRoot->expects($this->at(2))->method('moveUserKeys')->with('oldRoot', 'newRoot', $this->outputInterface); + + $this->invokePrivate($changeKeyStorageRoot, 'moveAllKeys', ['oldRoot', 'newRoot', $this->outputInterface]); + + } + + public function testPrepareNewRoot() { + $this->view->expects($this->once())->method('is_dir')->with('newRoot') + ->willReturn(true); + + $this->view->expects($this->once())->method('file_put_contents') + ->with('newRoot/' . \OC\Encryption\Keys\Storage::KEY_STORAGE_MARKER, + 'ownCloud will detect this folder as key storage root only if this file exists'); + + $this->invokePrivate($this->changeKeyStorageRoot, 'prepareNewRoot', ['newRoot']); + } + + /** + * @dataProvider dataTestPrepareNewRootException + * @expectedException \Exception + * + * @param bool $dirExists + * @param bool $couldCreateFile + */ + public function testPrepareNewRootException($dirExists, $couldCreateFile) { + $this->view->expects($this->once())->method('is_dir')->with('newRoot') + ->willReturn($dirExists); + $this->view->expects($this->any())->method('file_put_contents')->willReturn($couldCreateFile); + + $this->invokePrivate($this->changeKeyStorageRoot, 'prepareNewRoot', ['newRoot']); + } + + public function dataTestPrepareNewRootException() { + return [ + [true, false], + [false, true] + ]; + } + + /** + * @dataProvider dataTestMoveSystemKeys + * + * @param bool $dirExists + * @param bool $targetExists + * @param bool $executeRename + */ + public function testMoveSystemKeys($dirExists, $targetExists, $executeRename) { + + $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') + ->setConstructorArgs( + [ + $this->view, + $this->userManager, + $this->config, + $this->util, + $this->questionHelper + ] + )->setMethods(['targetExists'])->getMock(); + + $this->view->expects($this->once())->method('is_dir') + ->with('oldRoot/files_encryption')->willReturn($dirExists); + $changeKeyStorageRoot->expects($this->any())->method('targetExists') + ->with('newRoot/files_encryption')->willReturn($targetExists); + + if ($executeRename) { + $this->view->expects($this->once())->method('rename') + ->with('oldRoot/files_encryption', 'newRoot/files_encryption'); + } else { + $this->view->expects($this->never())->method('rename'); + } + + $this->invokePrivate($changeKeyStorageRoot, 'moveSystemKeys', ['oldRoot', 'newRoot']); + + } + + public function dataTestMoveSystemKeys() { + return [ + [true, false, true], + [false, true, false], + [true, true, false], + [false, false, false] + ]; + } + + + public function testMoveUserKeys() { + + $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') + ->setConstructorArgs( + [ + $this->view, + $this->userManager, + $this->config, + $this->util, + $this->questionHelper + ] + )->setMethods(['setupUserFS', 'moveUserEncryptionFolder'])->getMock(); + + $this->userManager->expects($this->once())->method('getBackends') + ->willReturn([$this->userInterface]); + $this->userInterface->expects($this->once())->method('getUsers') + ->willReturn(['user1', 'user2']); + $changeKeyStorageRoot->expects($this->exactly(2))->method('setupUserFS'); + $changeKeyStorageRoot->expects($this->exactly(2))->method('moveUserEncryptionFolder'); + + $this->invokePrivate($changeKeyStorageRoot, 'moveUserKeys', ['oldRoot', 'newRoot', $this->outputInterface]); + } + + /** + * @dataProvider dataTestMoveUserEncryptionFolder + * + * @param bool $userExists + * @param bool $isDir + * @param bool $targetExists + * @param bool $shouldRename + */ + public function testMoveUserEncryptionFolder($userExists, $isDir, $targetExists, $shouldRename) { + + $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') + ->setConstructorArgs( + [ + $this->view, + $this->userManager, + $this->config, + $this->util, + $this->questionHelper + ] + )->setMethods(['targetExists', 'prepareParentFolder'])->getMock(); + + $this->userManager->expects($this->once())->method('userExists') + ->willReturn($userExists); + $this->view->expects($this->any())->method('is_dir') + ->willReturn($isDir); + $changeKeyStorageRoot->expects($this->any())->method('targetExists') + ->willReturn($targetExists); + + if ($shouldRename) { + $changeKeyStorageRoot->expects($this->once())->method('prepareParentFolder') + ->with('newRoot/user1'); + $this->view->expects($this->once())->method('rename') + ->with('oldRoot/user1/files_encryption', 'newRoot/user1/files_encryption'); + } else { + $changeKeyStorageRoot->expects($this->never())->method('prepareParentFolder'); + $this->view->expects($this->never())->method('rename'); + } + + $this->invokePrivate($changeKeyStorageRoot, 'moveUserEncryptionFolder', ['user1', 'oldRoot', 'newRoot']); + + } + + public function dataTestMoveUserEncryptionFolder() { + return [ + [true, true, false, true], + [true, false, true, false], + [false, true, true, false], + [false, false, true, false], + [false, true, false, false], + [false, true, true, false], + [false, false, false, false] + ]; + } + + + /** + * @dataProvider dataTestPrepareParentFolder + */ + public function testPrepareParentFolder($path, $pathExists) { + $this->view->expects($this->any())->method('file_exists') + ->willReturnCallback( + function($fileExistsPath) use ($path, $pathExists) { + if ($path === $fileExistsPath) { + return $pathExists; + } + return false; + } + ); + + if ($pathExists === false) { + $subDirs = explode('/', ltrim($path, '/')); + $this->view->expects($this->exactly(count($subDirs)))->method('mkdir'); + } else { + $this->view->expects($this->never())->method('mkdir'); + } + + $this->invokePrivate( + $this->changeKeyStorageRoot, + 'prepareParentFolder', + [$path] + ); + } + + public function dataTestPrepareParentFolder() { + return [ + ['/user/folder/sub_folder/keystorage', true], + ['/user/folder/sub_folder/keystorage', false] + ]; + } + + public function testTargetExists() { + $this->view->expects($this->once())->method('file_exists')->with('path') + ->willReturn(false); + + $this->assertFalse( + $this->invokePrivate($this->changeKeyStorageRoot, 'targetExists', ['path']) + ); + } + + /** + * @expectedException \Exception + */ + public function testTargetExistsException() { + $this->view->expects($this->once())->method('file_exists')->with('path') + ->willReturn(true); + + $this->invokePrivate($this->changeKeyStorageRoot, 'targetExists', ['path']); + } + +} diff --git a/tests/lib/encryption/keys/storage.php b/tests/lib/encryption/keys/storage.php index 2f3aa3527b..b5b91f886a 100644 --- a/tests/lib/encryption/keys/storage.php +++ b/tests/lib/encryption/keys/storage.php @@ -37,6 +37,9 @@ class StorageTest extends TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $view; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $config; + public function setUp() { parent::setUp(); @@ -48,6 +51,10 @@ class StorageTest extends TestCase { ->disableOriginalConstructor() ->getMock(); + $this->config = $this->getMockBuilder('OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + $this->storage = new Storage($this->view, $this->util); } @@ -88,7 +95,7 @@ class StorageTest extends TestCase { * @param bool $originalKeyExists * @param string $expectedKeyContent */ - public function testGetFileKey2($path, $strippedPartialName, $originalKeyExists, $expectedKeyContent) { + public function testGetFileKey($path, $strippedPartialName, $originalKeyExists, $expectedKeyContent) { $this->util->expects($this->any()) ->method('getUidAndFilename') ->willReturnMap([ @@ -414,9 +421,12 @@ class StorageTest extends TestCase { * * @param string $path * @param boolean $systemWideMountPoint + * @param string $storageRoot * @param string $expected */ - public function testGetPathToKeys($path, $systemWideMountPoint, $expected) { + public function testGetPathToKeys($path, $systemWideMountPoint, $storageRoot, $expected) { + + $this->invokePrivate($this->storage, 'root_dir', [$storageRoot]); $this->util->expects($this->any()) ->method('getUidAndFilename') @@ -431,10 +441,12 @@ class StorageTest extends TestCase { } public function dataTestGetPathToKeys() { - return array( - array('/user1/files/source.txt', false, '/user1/files_encryption/keys/files/source.txt/'), - array('/user1/files/source.txt', true, '/files_encryption/keys/files/source.txt/') - ); + return [ + ['/user1/files/source.txt', false, '', '/user1/files_encryption/keys/files/source.txt/'], + ['/user1/files/source.txt', true, '', '/files_encryption/keys/files/source.txt/'], + ['/user1/files/source.txt', false, 'storageRoot', '/storageRoot/user1/files_encryption/keys/files/source.txt/'], + ['/user1/files/source.txt', true, 'storageRoot', '/storageRoot/files_encryption/keys/files/source.txt/'], + ]; } public function testKeySetPreparation() { @@ -463,4 +475,39 @@ class StorageTest extends TestCase { $this->assertSame($expected, $args[0]); } + /** + * @dataProvider dataTestGetFileKeyDir + * + * @param bool $isSystemWideMountPoint + * @param string $storageRoot + * @param string $expected + */ + public function testGetFileKeyDir($isSystemWideMountPoint, $storageRoot, $expected) { + + $path = '/user1/files/foo/bar.txt'; + $owner = 'user1'; + $relativePath = '/foo/bar.txt'; + + $this->invokePrivate($this->storage, 'root_dir', [$storageRoot]); + + $this->util->expects($this->once())->method('isSystemWideMountPoint') + ->willReturn($isSystemWideMountPoint); + $this->util->expects($this->once())->method('getUidAndFilename') + ->with($path)->willReturn([$owner, $relativePath]); + + $this->assertSame($expected, + $this->invokePrivate($this->storage, 'getFileKeyDir', ['OC_DEFAULT_MODULE', $path]) + ); + + } + + public function dataTestGetFileKeyDir() { + return [ + [false, '', '/user1/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], + [true, '', '/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], + [false, 'newStorageRoot', '/newStorageRoot/user1/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], + [true, 'newStorageRoot', '/newStorageRoot/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], + ]; + } + } diff --git a/tests/lib/encryption/managertest.php b/tests/lib/encryption/managertest.php index 9af7bc2c13..6355c706b6 100644 --- a/tests/lib/encryption/managertest.php +++ b/tests/lib/encryption/managertest.php @@ -19,12 +19,20 @@ class ManagerTest extends TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject */ private $l10n; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $view; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $util; + public function setUp() { parent::setUp(); $this->config = $this->getMock('\OCP\IConfig'); $this->logger = $this->getMock('\OCP\ILogger'); $this->l10n = $this->getMock('\OCP\Il10n'); - $this->manager = new Manager($this->config, $this->logger, $this->l10n); + $this->view = $this->getMock('\OC\Files\View'); + $this->util = $this->getMockBuilder('\OC\Encryption\Util')->disableOriginalConstructor()->getMock(); + $this->manager = new Manager($this->config, $this->logger, $this->l10n, $this->view, $this->util); } public function testManagerIsDisabled() { diff --git a/tests/lib/encryption/utiltest.php b/tests/lib/encryption/utiltest.php index 5aadb4e857..449326bb35 100644 --- a/tests/lib/encryption/utiltest.php +++ b/tests/lib/encryption/utiltest.php @@ -109,7 +109,11 @@ class UtilTest extends TestCase { /** * @dataProvider providePathsForTestIsExcluded */ - public function testIsExcluded($path, $expected) { + public function testIsExcluded($path, $keyStorageRoot, $expected) { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('core', 'encryption_key_storage_root', '') + ->willReturn($keyStorageRoot); $this->userManager ->expects($this->any()) ->method('userExists') @@ -122,11 +126,14 @@ class UtilTest extends TestCase { public function providePathsForTestIsExcluded() { return array( - array('/files_encryption', true), - array('files_encryption/foo.txt', true), - array('test/foo.txt', false), - array('/user1/files_encryption/foo.txt', true), - array('/user1/files/foo.txt', false), + array('/files_encryption', '', true), + array('files_encryption/foo.txt', '', true), + array('test/foo.txt', '', false), + array('/user1/files_encryption/foo.txt', '', true), + array('/user1/files/foo.txt', '', false), + array('/keyStorage/user1/files/foo.txt', 'keyStorage', true), + array('/keyStorage/files_encryption', '/keyStorage', true), + array('keyStorage/user1/files_encryption', '/keyStorage/', true), ); }