From abb6e89c5d83102c2838bd6a48b5bf6e73e9660d Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 10 Nov 2014 16:00:08 +0100 Subject: [PATCH 1/2] Add storage and cache wrappers to jail a storage to a subfolder --- lib/private/files/cache/cache.php | 2 +- lib/private/files/cache/wrapper/cachejail.php | 255 +++++++++++ .../files/cache/wrapper/cachewrapper.php | 247 +++++++++++ lib/private/files/storage/wrapper/jail.php | 413 ++++++++++++++++++ .../files/storage/wrapper/permissionsmask.php | 102 +++++ tests/lib/files/cache/cache.php | 8 +- tests/lib/files/cache/wrapper/cachejail.php | 67 +++ tests/lib/files/storage/wrapper/jail.php | 55 +++ 8 files changed, 1144 insertions(+), 5 deletions(-) create mode 100644 lib/private/files/cache/wrapper/cachejail.php create mode 100644 lib/private/files/cache/wrapper/cachewrapper.php create mode 100644 lib/private/files/storage/wrapper/jail.php create mode 100644 lib/private/files/storage/wrapper/permissionsmask.php create mode 100644 tests/lib/files/cache/wrapper/cachejail.php create mode 100644 tests/lib/files/storage/wrapper/jail.php diff --git a/lib/private/files/cache/cache.php b/lib/private/files/cache/cache.php index 2c12f83451..4157da2281 100644 --- a/lib/private/files/cache/cache.php +++ b/lib/private/files/cache/cache.php @@ -585,7 +585,7 @@ class Cache { /** * find a folder in the cache which has not been fully scanned * - * If multiply incomplete folders are in the cache, the one with the highest id will be returned, + * If multiple incomplete folders are in the cache, the one with the highest id will be returned, * use the one with the highest id gives the best result with the background scanner, since that is most * likely the folder where we stopped scanning previously * diff --git a/lib/private/files/cache/wrapper/cachejail.php b/lib/private/files/cache/wrapper/cachejail.php new file mode 100644 index 0000000000..7982293f5e --- /dev/null +++ b/lib/private/files/cache/wrapper/cachejail.php @@ -0,0 +1,255 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache\Wrapper; + +/** + * Jail to a subdirectory of the wrapped cache + */ +class CacheJail extends CacheWrapper { + /** + * @var string + */ + protected $root; + + /** + * @param \OC\Files\Cache\Cache $cache + * @param string $root + */ + public function __construct($cache, $root) { + parent::__construct($cache); + $this->root = $root; + } + + protected function getSourcePath($path) { + if ($path === '') { + return $this->root; + } else { + return $this->root . '/' . $path; + } + } + + /** + * @param string $path + * @return null|string the jailed path or null if the path is outside the jail + */ + protected function getJailedPath($path) { + $rootLength = strlen($this->root) + 1; + if ($path === $this->root) { + return ''; + } else if (substr($path, 0, $rootLength) === $this->root . '/') { + return substr($path, $rootLength); + } else { + return null; + } + } + + /** + * @param array $entry + * @return array + */ + protected function formatCacheEntry($entry) { + if (isset($entry['path'])) { + $entry['path'] = $this->getJailedPath($entry['path']); + } + return $entry; + } + + protected function filterCacheEntry($entry) { + $rootLength = strlen($this->root) + 1; + return ($entry['path'] === $this->root) or (substr($entry['path'], 0, $rootLength) === $this->root . '/'); + } + + /** + * get the stored metadata of a file or folder + * + * @param string /int $file + * @return array|false + */ + public function get($file) { + if (is_string($file) or $file == '') { + $file = $this->getSourcePath($file); + } + return parent::get($file); + } + + /** + * store meta data for a file or folder + * + * @param string $file + * @param array $data + * + * @return int file id + */ + public function put($file, array $data) { + return $this->cache->put($this->getSourcePath($file), $data); + } + + /** + * update the metadata in the cache + * + * @param int $id + * @param array $data + */ + public function update($id, array $data) { + $this->cache->update($this->getSourcePath($id), $data); + } + + /** + * get the file id for a file + * + * @param string $file + * @return int + */ + public function getId($file) { + return $this->cache->getId($this->getSourcePath($file)); + } + + /** + * get the id of the parent folder of a file + * + * @param string $file + * @return int + */ + public function getParentId($file) { + if ($file === '') { + return -1; + } else { + return $this->cache->getParentId($this->getSourcePath($file)); + } + } + + /** + * check if a file is available in the cache + * + * @param string $file + * @return bool + */ + public function inCache($file) { + return $this->cache->inCache($this->getSourcePath($file)); + } + + /** + * remove a file or folder from the cache + * + * @param string $file + */ + public function remove($file) { + $this->cache->remove($this->getSourcePath($file)); + } + + /** + * Move a file or folder in the cache + * + * @param string $source + * @param string $target + */ + public function move($source, $target) { + $this->cache->move($this->getSourcePath($source), $this->getSourcePath($target)); + } + + /** + * remove all entries for files that are stored on the storage from the cache + */ + public function clear() { + $this->cache->remove($this->root); + } + + /** + * @param string $file + * + * @return int, Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE + */ + public function getStatus($file) { + return $this->cache->getStatus($this->getSourcePath($file)); + } + + private function formatSearchResults($results) { + $results = array_filter($results, array($this, 'filterCacheEntry')); + $results = array_values($results); + return array_map(array($this, 'formatCacheEntry'), $results); + } + + /** + * search for files matching $pattern + * + * @param string $pattern + * @return array an array of file data + */ + public function search($pattern) { + $results = $this->cache->search($pattern); + return $this->formatSearchResults($results); + } + + /** + * search for files by mimetype + * + * @param string $mimetype + * @return array + */ + public function searchByMime($mimetype) { + $results = $this->cache->searchByMime($mimetype); + return $this->formatSearchResults($results); + } + + /** + * update the folder size and the size of all parent folders + * + * @param string|boolean $path + * @param array $data (optional) meta data of the folder + */ + public function correctFolderSize($path, $data = null) { + $this->cache->correctFolderSize($this->getSourcePath($path), $data); + } + + /** + * get the size of a folder and set it in the cache + * + * @param string $path + * @param array $entry (optional) meta data of the folder + * @return int + */ + public function calculateFolderSize($path, $entry = null) { + return $this->cache->calculateFolderSize($this->getSourcePath($path), $entry); + } + + /** + * get all file ids on the files on the storage + * + * @return int[] + */ + public function getAll() { + // not supported + return array(); + } + + /** + * find a folder in the cache which has not been fully scanned + * + * If multiply incomplete folders are in the cache, the one with the highest id will be returned, + * use the one with the highest id gives the best result with the background scanner, since that is most + * likely the folder where we stopped scanning previously + * + * @return string|bool the path of the folder or false when no folder matched + */ + public function getIncomplete() { + // not supported + return false; + } + + /** + * get the path of a file on this storage by it's id + * + * @param int $id + * @return string|null + */ + public function getPathById($id) { + $path = $this->cache->getPathById($id); + return $this->getJailedPath($path); + } +} diff --git a/lib/private/files/cache/wrapper/cachewrapper.php b/lib/private/files/cache/wrapper/cachewrapper.php new file mode 100644 index 0000000000..040358ec65 --- /dev/null +++ b/lib/private/files/cache/wrapper/cachewrapper.php @@ -0,0 +1,247 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache\Wrapper; + +use OC\Files\Cache\Cache; + +class CacheWrapper extends Cache { + /** + * @var \OC\Files\Cache\Cache + */ + protected $cache; + + /** + * @param \OC\Files\Cache\Cache $cache + */ + public function __construct($cache) { + $this->cache = $cache; + } + + /** + * Make it easy for wrappers to modify every returned cache entry + * + * @param array $entry + * @return array + */ + protected function formatCacheEntry($entry) { + return $entry; + } + + /** + * get the stored metadata of a file or folder + * + * @param string /int $file + * @return array|false + */ + public function get($file) { + $result = $this->cache->get($file); + if ($result) { + $result = $this->formatCacheEntry($result); + } + return $result; + } + + /** + * get the metadata of all files stored in $folder + * + * @param string $folder + * @return array + */ + public function getFolderContents($folder) { + // cant do a simple $this->cache->.... call here since getFolderContentsById needs to be called on this + // and not the wrapped cache + $fileId = $this->getId($folder); + return $this->getFolderContentsById($fileId); + } + + /** + * get the metadata of all files stored in $folder + * + * @param int $fileId the file id of the folder + * @return array + */ + public function getFolderContentsById($fileId) { + $results = $this->cache->getFolderContentsById($fileId); + return array_map(array($this, 'formatCacheEntry'), $results); + } + + /** + * store meta data for a file or folder + * + * @param string $file + * @param array $data + * + * @return int file id + */ + public function put($file, array $data) { + return $this->cache->put($file, $data); + } + + /** + * update the metadata in the cache + * + * @param int $id + * @param array $data + */ + public function update($id, array $data) { + $this->cache->update($id, $data); + } + + /** + * get the file id for a file + * + * @param string $file + * @return int + */ + public function getId($file) { + return $this->cache->getId($file); + } + + /** + * get the id of the parent folder of a file + * + * @param string $file + * @return int + */ + public function getParentId($file) { + return $this->cache->getParentId($file); + } + + /** + * check if a file is available in the cache + * + * @param string $file + * @return bool + */ + public function inCache($file) { + return $this->cache->inCache($file); + } + + /** + * remove a file or folder from the cache + * + * @param string $file + */ + public function remove($file) { + $this->cache->remove($file); + } + + /** + * Move a file or folder in the cache + * + * @param string $source + * @param string $target + */ + public function move($source, $target) { + $this->cache->move($source, $target); + } + + /** + * remove all entries for files that are stored on the storage from the cache + */ + public function clear() { + $this->cache->clear(); + } + + /** + * @param string $file + * + * @return int, Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE + */ + public function getStatus($file) { + return $this->cache->getStatus($file); + } + + /** + * search for files matching $pattern + * + * @param string $pattern + * @return array an array of file data + */ + public function search($pattern) { + $results = $this->cache->search($pattern); + return array_map(array($this, 'formatCacheEntry'), $results); + } + + /** + * search for files by mimetype + * + * @param string $mimetype + * @return array + */ + public function searchByMime($mimetype) { + $results = $this->cache->searchByMime($mimetype); + return array_map(array($this, 'formatCacheEntry'), $results); + } + + /** + * update the folder size and the size of all parent folders + * + * @param string|boolean $path + * @param array $data (optional) meta data of the folder + */ + public function correctFolderSize($path, $data = null) { + $this->cache->correctFolderSize($path, $data); + } + + /** + * get the size of a folder and set it in the cache + * + * @param string $path + * @param array $entry (optional) meta data of the folder + * @return int + */ + public function calculateFolderSize($path, $entry = null) { + return $this->cache->calculateFolderSize($path, $entry); + } + + /** + * get all file ids on the files on the storage + * + * @return int[] + */ + public function getAll() { + return $this->cache->getAll(); + } + + /** + * find a folder in the cache which has not been fully scanned + * + * If multiple incomplete folders are in the cache, the one with the highest id will be returned, + * use the one with the highest id gives the best result with the background scanner, since that is most + * likely the folder where we stopped scanning previously + * + * @return string|bool the path of the folder or false when no folder matched + */ + public function getIncomplete() { + return $this->cache->getIncomplete(); + } + + /** + * get the path of a file on this storage by it's id + * + * @param int $id + * @return string|null + */ + public function getPathById($id) { + return $this->cache->getPathById($id); + } + + /** + * get the storage id of the storage for a file and the internal path of the file + * unlike getPathById this does not limit the search to files on this storage and + * instead does a global search in the cache table + * + * @param int $id + * @return array, first element holding the storage id, second the path + */ + static public function getById($id) { + return parent::getById($id); + } +} diff --git a/lib/private/files/storage/wrapper/jail.php b/lib/private/files/storage/wrapper/jail.php new file mode 100644 index 0000000000..22b9676575 --- /dev/null +++ b/lib/private/files/storage/wrapper/jail.php @@ -0,0 +1,413 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage\Wrapper; + +use OC\Files\Cache\Wrapper\CacheJail; + +/** + * Jail to a subdirectory of the wrapped storage + * + * This restricts access to a subfolder of the wrapped storage with the subfolder becoming the root folder new storage + */ +class Jail extends Wrapper { + /** + * @var string + */ + protected $rootPath; + + /** + * @param array $arguments ['storage' => $storage, 'mask' => $root] + * + * $storage: The storage that will be wrapper + * $root: The folder in the wrapped storage that will become the root folder of the wrapped storage + */ + public function __construct($arguments) { + parent::__construct($arguments); + $this->rootPath = $arguments['root']; + } + + protected function getSourcePath($path) { + if ($path === '') { + return $this->rootPath; + } else { + return $this->rootPath . '/' . $path; + } + } + + public function getId() { + return 'link:' . parent::getId() . ':' . $this->rootPath; + } + + /** + * see http://php.net/manual/en/function.mkdir.php + * + * @param string $path + * @return bool + */ + public function mkdir($path) { + return $this->storage->mkdir($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.rmdir.php + * + * @param string $path + * @return bool + */ + public function rmdir($path) { + return $this->storage->rmdir($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.opendir.php + * + * @param string $path + * @return resource + */ + public function opendir($path) { + return $this->storage->opendir($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.is_dir.php + * + * @param string $path + * @return bool + */ + public function is_dir($path) { + return $this->storage->is_dir($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.is_file.php + * + * @param string $path + * @return bool + */ + public function is_file($path) { + return $this->storage->is_file($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.stat.php + * only the following keys are required in the result: size and mtime + * + * @param string $path + * @return array + */ + public function stat($path) { + return $this->storage->stat($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.filetype.php + * + * @param string $path + * @return bool + */ + public function filetype($path) { + return $this->storage->filetype($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.filesize.php + * The result for filesize when called on a folder is required to be 0 + * + * @param string $path + * @return int + */ + public function filesize($path) { + return $this->storage->filesize($this->getSourcePath($path)); + } + + /** + * check if a file can be created in $path + * + * @param string $path + * @return bool + */ + public function isCreatable($path) { + return $this->storage->isCreatable($this->getSourcePath($path)); + } + + /** + * check if a file can be read + * + * @param string $path + * @return bool + */ + public function isReadable($path) { + return $this->storage->isReadable($this->getSourcePath($path)); + } + + /** + * check if a file can be written to + * + * @param string $path + * @return bool + */ + public function isUpdatable($path) { + return $this->storage->isUpdatable($this->getSourcePath($path)); + } + + /** + * check if a file can be deleted + * + * @param string $path + * @return bool + */ + public function isDeletable($path) { + return $this->storage->isDeletable($this->getSourcePath($path)); + } + + /** + * check if a file can be shared + * + * @param string $path + * @return bool + */ + public function isSharable($path) { + return $this->storage->isSharable($this->getSourcePath($path)); + } + + /** + * get the full permissions of a path. + * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php + * + * @param string $path + * @return int + */ + public function getPermissions($path) { + return $this->storage->getPermissions($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.file_exists.php + * + * @param string $path + * @return bool + */ + public function file_exists($path) { + return $this->storage->file_exists($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.filemtime.php + * + * @param string $path + * @return int + */ + public function filemtime($path) { + return $this->storage->filemtime($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.file_get_contents.php + * + * @param string $path + * @return string + */ + public function file_get_contents($path) { + return $this->storage->file_get_contents($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.file_put_contents.php + * + * @param string $path + * @param string $data + * @return bool + */ + public function file_put_contents($path, $data) { + return $this->storage->file_put_contents($this->getSourcePath($path), $data); + } + + /** + * see http://php.net/manual/en/function.unlink.php + * + * @param string $path + * @return bool + */ + public function unlink($path) { + return $this->storage->unlink($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.rename.php + * + * @param string $path1 + * @param string $path2 + * @return bool + */ + public function rename($path1, $path2) { + return $this->storage->rename($this->getSourcePath($path1), $this->getSourcePath($path2)); + } + + /** + * see http://php.net/manual/en/function.copy.php + * + * @param string $path1 + * @param string $path2 + * @return bool + */ + public function copy($path1, $path2) { + return $this->storage->copy($this->getSourcePath($path1), $this->getSourcePath($path2)); + } + + /** + * see http://php.net/manual/en/function.fopen.php + * + * @param string $path + * @param string $mode + * @return resource + */ + public function fopen($path, $mode) { + return $this->storage->fopen($this->getSourcePath($path), $mode); + } + + /** + * get the mimetype for a file or folder + * The mimetype for a folder is required to be "httpd/unix-directory" + * + * @param string $path + * @return string + */ + public function getMimeType($path) { + return $this->storage->getMimeType($this->getSourcePath($path)); + } + + /** + * see http://php.net/manual/en/function.hash.php + * + * @param string $type + * @param string $path + * @param bool $raw + * @return string + */ + public function hash($type, $path, $raw = false) { + return $this->storage->hash($type, $this->getSourcePath($path), $raw); + } + + /** + * see http://php.net/manual/en/function.free_space.php + * + * @param string $path + * @return int + */ + public function free_space($path) { + return $this->storage->free_space($this->getSourcePath($path)); + } + + /** + * search for occurrences of $query in file names + * + * @param string $query + * @return array + */ + public function search($query) { + return $this->storage->search($query); + } + + /** + * see http://php.net/manual/en/function.touch.php + * If the backend does not support the operation, false should be returned + * + * @param string $path + * @param int $mtime + * @return bool + */ + public function touch($path, $mtime = null) { + return $this->storage->touch($this->getSourcePath($path), $mtime); + } + + /** + * get the path to a local version of the file. + * The local version of the file can be temporary and doesn't have to be persistent across requests + * + * @param string $path + * @return string + */ + public function getLocalFile($path) { + return $this->storage->getLocalFile($this->getSourcePath($path)); + } + + /** + * get the path to a local version of the folder. + * The local version of the folder can be temporary and doesn't have to be persistent across requests + * + * @param string $path + * @return string + */ + public function getLocalFolder($path) { + return $this->storage->getLocalFolder($this->getSourcePath($path)); + } + + /** + * check if a file or folder has been updated since $time + * + * @param string $path + * @param int $time + * @return bool + * + * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed. + * returning true for other changes in the folder is optional + */ + public function hasUpdated($path, $time) { + return $this->storage->hasUpdated($this->getSourcePath($path), $time); + } + + /** + * get a cache instance for the storage + * + * @param string $path + * @param \OC\Files\Storage\Storage (optional) the storage to pass to the cache + * @return \OC\Files\Cache\Cache + */ + public function getCache($path = '', $storage = null) { + if (!$storage) { + $storage = $this; + } + $sourceCache = $this->storage->getCache($this->getSourcePath($path), $storage); + return new CacheJail($sourceCache, $this->rootPath); + } + + /** + * get the user id of the owner of a file or folder + * + * @param string $path + * @return string + */ + public function getOwner($path) { + return $this->storage->getOwner($this->getSourcePath($path)); + } + + /** + * get a watcher instance for the cache + * + * @param string $path + * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher + * @return \OC\Files\Cache\Watcher + */ + public function getWatcher($path = '', $storage = null) { + if (!$storage) { + $storage = $this; + } + return $this->storage->getWatcher($this->getSourcePath($path), $storage); + } + + /** + * get the ETag for a file or folder + * + * @param string $path + * @return string + */ + public function getETag($path) { + return $this->storage->getETag($this->getSourcePath($path)); + } +} diff --git a/lib/private/files/storage/wrapper/permissionsmask.php b/lib/private/files/storage/wrapper/permissionsmask.php new file mode 100644 index 0000000000..be5cb6bbaa --- /dev/null +++ b/lib/private/files/storage/wrapper/permissionsmask.php @@ -0,0 +1,102 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage\Wrapper; + +use OC\Files\Cache\Wrapper\CachePermissionsMask; + +/** + * Mask the permissions of a storage + * + * Note that the read permissions cant be masked + */ +class PermissionsMask extends Wrapper { + /** + * @var int + */ + private $mask; + + public function __construct($arguments) { + parent::__construct($arguments); + $this->mask = $arguments['mask']; + } + + private function checkMask($permissions) { + return ($this->mask & $permissions) === $permissions; + } + + public function isUpdatable($path) { + return $this->checkMask(\OCP\PERMISSION_UPDATE) and parent::isUpdatable($path); + } + + public function isCreatable($path) { + return $this->checkMask(\OCP\PERMISSION_CREATE) and parent::isCreatable($path); + } + + public function isDeletable($path) { + return $this->checkMask(\OCP\PERMISSION_DELETE) and parent::isDeletable($path); + } + + public function getPermissions($path) { + return $this->storage->getPermissions($path) & $this->mask; + } + + public function rename($path1, $path2) { + return $this->checkMask(\OCP\PERMISSION_UPDATE) and parent::rename($path1, $path2); + } + + public function copy($path1, $path2) { + return $this->checkMask(\OCP\PERMISSION_CREATE) and parent::copy($path1, $path2); + } + + public function touch($path, $mtime = null) { + $permissions = $this->file_exists($path) ? \OCP\PERMISSION_UPDATE : \OCP\PERMISSION_CREATE; + return $this->checkMask($permissions) and parent::touch($path, $mtime); + } + + public function mkdir($path) { + return $this->checkMask(\OCP\PERMISSION_CREATE) and parent::mkdir($path); + } + + public function rmdir($path) { + return $this->checkMask(\OCP\PERMISSION_DELETE) and parent::rmdir($path); + } + + public function unlink($path) { + return $this->checkMask(\OCP\PERMISSION_DELETE) and parent::unlink($path); + } + + public function file_put_contents($path, $data) { + $permissions = $this->file_exists($path) ? \OCP\PERMISSION_UPDATE : \OCP\PERMISSION_CREATE; + return $this->checkMask($permissions) and parent::file_put_contents($path, $data); + } + + public function fopen($path, $mode) { + if ($mode === 'r' or $mode === 'rb') { + return parent::fopen($path, $mode); + } else { + $permissions = $this->file_exists($path) ? \OCP\PERMISSION_UPDATE : \OCP\PERMISSION_CREATE; + return $this->checkMask($permissions) ? parent::fopen($path, $mode) : false; + } + } + + /** + * get a cache instance for the storage + * + * @param string $path + * @param \OC\Files\Storage\Storage (optional) the storage to pass to the cache + * @return \OC\Files\Cache\Cache + */ + public function getCache($path = '', $storage = null) { + if (!$storage) { + $storage = $this; + } + $sourceCache = parent::getCache($path, $storage); + return new CachePermissionsMask($sourceCache, $this->mask); + } +} diff --git a/tests/lib/files/cache/cache.php b/tests/lib/files/cache/cache.php index 02c45a1657..7360e9885c 100644 --- a/tests/lib/files/cache/cache.php +++ b/tests/lib/files/cache/cache.php @@ -20,20 +20,20 @@ class Cache extends \Test\TestCase { /** * @var \OC\Files\Storage\Temporary $storage ; */ - private $storage; + protected $storage; /** * @var \OC\Files\Storage\Temporary $storage2 ; */ - private $storage2; + protected $storage2; /** * @var \OC\Files\Cache\Cache $cache */ - private $cache; + protected $cache; /** * @var \OC\Files\Cache\Cache $cache2 */ - private $cache2; + protected $cache2; public function testSimple() { $file1 = 'foo'; diff --git a/tests/lib/files/cache/wrapper/cachejail.php b/tests/lib/files/cache/wrapper/cachejail.php new file mode 100644 index 0000000000..13f3dc8858 --- /dev/null +++ b/tests/lib/files/cache/wrapper/cachejail.php @@ -0,0 +1,67 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Test\Files\Cache\Wrapper; + +use Test\Files\Cache\Cache; + +class CacheJail extends Cache { + /** + * @var \OC\Files\Cache\Cache $sourceCache + */ + protected $sourceCache; + + public function setUp() { + parent::setUp(); + $this->storage->mkdir('foo'); + $this->sourceCache = $this->cache; + $this->cache = new \OC\Files\Cache\Wrapper\CacheJail($this->sourceCache, 'foo'); + } + + function testSearchOutsideJail() { + $file1 = 'foo/foobar'; + $file2 = 'folder/foobar'; + $data1 = array('size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'); + + $this->sourceCache->put($file1, $data1); + $this->sourceCache->put($file2, $data1); + + $this->assertCount(2, $this->sourceCache->search('%foobar')); + + $result = $this->cache->search('%foobar%'); + $this->assertCount(1, $result); + $this->assertEquals('foobar', $result[0]['path']); + } + + function testClearKeepEntriesOutsideJail() { + $file1 = 'foo/foobar'; + $file2 = 'foo/foobar/asd'; + $file3 = 'folder/foobar'; + $data1 = array('size' => 100, 'mtime' => 50, 'mimetype' => 'httpd/unix-directory'); + + $this->sourceCache->put('foo', $data1); + $this->sourceCache->put($file1, $data1); + $this->sourceCache->put($file2, $data1); + $this->sourceCache->put($file3, $data1); + + $this->cache->clear(); + + $this->assertFalse($this->cache->inCache('foobar')); + $this->assertTrue($this->sourceCache->inCache('folder/foobar')); + } + + function testGetById() { + //not supported + $this->assertTrue(true); + } + + function testGetIncomplete() { + //not supported + $this->assertTrue(true); + } +} diff --git a/tests/lib/files/storage/wrapper/jail.php b/tests/lib/files/storage/wrapper/jail.php new file mode 100644 index 0000000000..270ce750ec --- /dev/null +++ b/tests/lib/files/storage/wrapper/jail.php @@ -0,0 +1,55 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Test\Files\Storage\Wrapper; + +class Jail extends \Test\Files\Storage\Storage { + /** + * @var string tmpDir + */ + private $tmpDir; + + /** + * @var \OC\Files\Storage\Temporary + */ + private $sourceStorage; + + public function setUp() { + parent::setUp(); + $this->sourceStorage = new \OC\Files\Storage\Temporary(array()); + $this->sourceStorage->mkdir('foo'); + $this->instance = new \OC\Files\Storage\Wrapper\Jail(array( + 'storage' => $this->sourceStorage, + 'root' => 'foo' + )); + } + + public function tearDown() { + // test that nothing outside our jail is touched + $contents = array(); + $dh = $this->sourceStorage->opendir(''); + while ($file = readdir($dh)) { + if ($file !== '.' and $file !== '..') { + $contents[] = $file; + } + } + $this->assertEquals(array('foo'), $contents); + $this->sourceStorage->cleanUp(); + parent::tearDown(); + } + + public function testMkDirRooted() { + $this->instance->mkdir('bar'); + $this->assertTrue($this->sourceStorage->is_dir('foo/bar')); + } + + public function testFilePutContentsRooted() { + $this->instance->file_put_contents('bar', 'asd'); + $this->assertEquals('asd', $this->sourceStorage->file_get_contents('foo/bar')); + } +} From 33b64868d7b65e751bd8d729ce69d6f46e6c3d8d Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 10 Nov 2014 16:00:25 +0100 Subject: [PATCH 2/2] Add storage and cache wrappers to apply a permissions mask to a storage --- .../cache/wrapper/cachepermissionsmask.php | 32 ++++++ .../files/storage/wrapper/permissionsmask.php | 33 ++++-- .../cache/wrapper/cachepermissionsmask.php | 94 ++++++++++++++++ .../files/storage/wrapper/permissionsmask.php | 105 ++++++++++++++++++ 4 files changed, 252 insertions(+), 12 deletions(-) create mode 100644 lib/private/files/cache/wrapper/cachepermissionsmask.php create mode 100644 tests/lib/files/cache/wrapper/cachepermissionsmask.php create mode 100644 tests/lib/files/storage/wrapper/permissionsmask.php diff --git a/lib/private/files/cache/wrapper/cachepermissionsmask.php b/lib/private/files/cache/wrapper/cachepermissionsmask.php new file mode 100644 index 0000000000..6ce6a4ebc4 --- /dev/null +++ b/lib/private/files/cache/wrapper/cachepermissionsmask.php @@ -0,0 +1,32 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache\Wrapper; + +class CachePermissionsMask extends CacheWrapper { + /** + * @var int + */ + protected $mask; + + /** + * @param \OC\Files\Cache\Cache $cache + * @param int $mask + */ + public function __construct($cache, $mask) { + parent::__construct($cache); + $this->mask = $mask; + } + + protected function formatCacheEntry($entry) { + if (isset($entry['permissions'])) { + $entry['permissions'] &= $this->mask; + } + return $entry; + } +} diff --git a/lib/private/files/storage/wrapper/permissionsmask.php b/lib/private/files/storage/wrapper/permissionsmask.php index be5cb6bbaa..955cb54591 100644 --- a/lib/private/files/storage/wrapper/permissionsmask.php +++ b/lib/private/files/storage/wrapper/permissionsmask.php @@ -9,18 +9,27 @@ namespace OC\Files\Storage\Wrapper; use OC\Files\Cache\Wrapper\CachePermissionsMask; +use OCP\Constants; /** * Mask the permissions of a storage * + * This can be used to restrict update, create, delete and/or share permissions of a storage + * * Note that the read permissions cant be masked */ class PermissionsMask extends Wrapper { /** - * @var int + * @var int the permissions bits we want to keep */ private $mask; + /** + * @param array $arguments ['storage' => $storage, 'mask' => $mask] + * + * $storage: The storage the permissions mask should be applied on + * $mask: The permission bits that should be kept, a combination of the \OCP\Constant::PERMISSION_ constants + */ public function __construct($arguments) { parent::__construct($arguments); $this->mask = $arguments['mask']; @@ -31,15 +40,15 @@ class PermissionsMask extends Wrapper { } public function isUpdatable($path) { - return $this->checkMask(\OCP\PERMISSION_UPDATE) and parent::isUpdatable($path); + return $this->checkMask(Constants::PERMISSION_UPDATE) and parent::isUpdatable($path); } public function isCreatable($path) { - return $this->checkMask(\OCP\PERMISSION_CREATE) and parent::isCreatable($path); + return $this->checkMask(Constants::PERMISSION_CREATE) and parent::isCreatable($path); } public function isDeletable($path) { - return $this->checkMask(\OCP\PERMISSION_DELETE) and parent::isDeletable($path); + return $this->checkMask(Constants::PERMISSION_DELETE) and parent::isDeletable($path); } public function getPermissions($path) { @@ -47,32 +56,32 @@ class PermissionsMask extends Wrapper { } public function rename($path1, $path2) { - return $this->checkMask(\OCP\PERMISSION_UPDATE) and parent::rename($path1, $path2); + return $this->checkMask(Constants::PERMISSION_UPDATE) and parent::rename($path1, $path2); } public function copy($path1, $path2) { - return $this->checkMask(\OCP\PERMISSION_CREATE) and parent::copy($path1, $path2); + return $this->checkMask(Constants::PERMISSION_CREATE) and parent::copy($path1, $path2); } public function touch($path, $mtime = null) { - $permissions = $this->file_exists($path) ? \OCP\PERMISSION_UPDATE : \OCP\PERMISSION_CREATE; + $permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE; return $this->checkMask($permissions) and parent::touch($path, $mtime); } public function mkdir($path) { - return $this->checkMask(\OCP\PERMISSION_CREATE) and parent::mkdir($path); + return $this->checkMask(Constants::PERMISSION_CREATE) and parent::mkdir($path); } public function rmdir($path) { - return $this->checkMask(\OCP\PERMISSION_DELETE) and parent::rmdir($path); + return $this->checkMask(Constants::PERMISSION_DELETE) and parent::rmdir($path); } public function unlink($path) { - return $this->checkMask(\OCP\PERMISSION_DELETE) and parent::unlink($path); + return $this->checkMask(Constants::PERMISSION_DELETE) and parent::unlink($path); } public function file_put_contents($path, $data) { - $permissions = $this->file_exists($path) ? \OCP\PERMISSION_UPDATE : \OCP\PERMISSION_CREATE; + $permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE; return $this->checkMask($permissions) and parent::file_put_contents($path, $data); } @@ -80,7 +89,7 @@ class PermissionsMask extends Wrapper { if ($mode === 'r' or $mode === 'rb') { return parent::fopen($path, $mode); } else { - $permissions = $this->file_exists($path) ? \OCP\PERMISSION_UPDATE : \OCP\PERMISSION_CREATE; + $permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE; return $this->checkMask($permissions) ? parent::fopen($path, $mode) : false; } } diff --git a/tests/lib/files/cache/wrapper/cachepermissionsmask.php b/tests/lib/files/cache/wrapper/cachepermissionsmask.php new file mode 100644 index 0000000000..72fd22741d --- /dev/null +++ b/tests/lib/files/cache/wrapper/cachepermissionsmask.php @@ -0,0 +1,94 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Test\Files\Cache\Wrapper; + +use OCP\Constants; +use Test\Files\Cache\Cache; + +class CachePermissionsMask extends Cache { + /** + * @var \OC\Files\Cache\Cache $sourceCache + */ + protected $sourceCache; + + public function setUp() { + parent::setUp(); + $this->storage->mkdir('foo'); + $this->sourceCache = $this->cache; + $this->cache = $this->getMaskedCached(Constants::PERMISSION_ALL); + } + + protected function getMaskedCached($mask) { + return new \OC\Files\Cache\Wrapper\CachePermissionsMask($this->sourceCache, $mask); + } + + public function maskProvider() { + return array( + array(Constants::PERMISSION_ALL), + array(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE), + array(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE), + array(Constants::PERMISSION_READ) + ); + } + + /** + * @dataProvider maskProvider + * @param int $mask + */ + public function testGetMasked($mask) { + $cache = $this->getMaskedCached($mask); + $data = array('size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'permissions' => Constants::PERMISSION_ALL); + $this->sourceCache->put('foo', $data); + $result = $cache->get('foo'); + $this->assertEquals($mask, $result['permissions']); + + $data = array('size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE); + $this->sourceCache->put('bar', $data); + $result = $cache->get('bar'); + $this->assertEquals($mask & ~Constants::PERMISSION_DELETE, $result['permissions']); + } + + /** + * @dataProvider maskProvider + * @param int $mask + */ + public function testGetFolderContentMasked($mask) { + $this->storage->mkdir('foo'); + $this->storage->file_put_contents('foo/bar', 'asd'); + $this->storage->file_put_contents('foo/asd', 'bar'); + $this->storage->getScanner()->scan(''); + + $cache = $this->getMaskedCached($mask); + $files = $cache->getFolderContents('foo'); + $this->assertCount(2, $files); + + foreach ($files as $file) { + $this->assertEquals($mask & ~Constants::PERMISSION_CREATE, $file['permissions']); + } + } + + /** + * @dataProvider maskProvider + * @param int $mask + */ + public function testSearchMasked($mask) { + $this->storage->mkdir('foo'); + $this->storage->file_put_contents('foo/bar', 'asd'); + $this->storage->file_put_contents('foo/foobar', 'bar'); + $this->storage->getScanner()->scan(''); + + $cache = $this->getMaskedCached($mask); + $files = $cache->search('%bar'); + $this->assertCount(2, $files); + + foreach ($files as $file) { + $this->assertEquals($mask & ~Constants::PERMISSION_CREATE, $file['permissions']); + } + } +} diff --git a/tests/lib/files/storage/wrapper/permissionsmask.php b/tests/lib/files/storage/wrapper/permissionsmask.php new file mode 100644 index 0000000000..7e8c387677 --- /dev/null +++ b/tests/lib/files/storage/wrapper/permissionsmask.php @@ -0,0 +1,105 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Test\Files\Storage\Wrapper; + +use OCP\Constants; + +class PermissionsMask extends \Test\Files\Storage\Storage { + + /** + * @var \OC\Files\Storage\Temporary + */ + private $sourceStorage; + + public function setUp() { + parent::setUp(); + $this->sourceStorage = new \OC\Files\Storage\Temporary(array()); + $this->instance = $this->getMaskedStorage(Constants::PERMISSION_ALL); + } + + public function tearDown() { + $this->sourceStorage->cleanUp(); + parent::tearDown(); + } + + protected function getMaskedStorage($mask) { + return new \OC\Files\Storage\Wrapper\PermissionsMask(array( + 'storage' => $this->sourceStorage, + 'mask' => $mask + )); + } + + public function testMkdirNoCreate() { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); + $this->assertFalse($storage->mkdir('foo')); + $this->assertFalse($storage->file_exists('foo')); + } + + public function testRmdirNoDelete() { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE); + $this->assertTrue($storage->mkdir('foo')); + $this->assertTrue($storage->file_exists('foo')); + $this->assertFalse($storage->rmdir('foo')); + $this->assertTrue($storage->file_exists('foo')); + } + + public function testTouchNewFileNoCreate() { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); + $this->assertFalse($storage->touch('foo')); + $this->assertFalse($storage->file_exists('foo')); + } + + public function testTouchNewFileNoUpdate() { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertTrue($storage->touch('foo')); + $this->assertTrue($storage->file_exists('foo')); + } + + public function testTouchExistingFileNoUpdate() { + $this->sourceStorage->touch('foo'); + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertFalse($storage->touch('foo')); + } + + public function testUnlinkNoDelete() { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE); + $this->assertTrue($storage->touch('foo')); + $this->assertTrue($storage->file_exists('foo')); + $this->assertFalse($storage->unlink('foo')); + $this->assertTrue($storage->file_exists('foo')); + } + + public function testPutContentsNewFileNoUpdate() { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertTrue($storage->file_put_contents('foo', 'bar')); + $this->assertEquals('bar', $storage->file_get_contents('foo')); + } + + public function testPutContentsNewFileNoCreate() { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); + $this->assertFalse($storage->file_put_contents('foo', 'bar')); + } + + public function testPutContentsExistingFileNoUpdate() { + $this->sourceStorage->touch('foo'); + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertFalse($storage->file_put_contents('foo', 'bar')); + } + + public function testFopenExistingFileNoUpdate() { + $this->sourceStorage->touch('foo'); + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertFalse($storage->fopen('foo', 'w')); + } + + public function testFopenNewFileNoCreate() { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); + $this->assertFalse($storage->fopen('foo', 'w')); + } +}