files_external: allow to register config handlers for flexible placeholders

* BackendService (directly accessable via \OC_Server) offers registerConfigHandler
* SimpleSubstitutionTrait brings reusable logic for simple string replacments
* internal $user replacement mechanism was migrated

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
This commit is contained in:
Arthur Schiwon 2019-02-11 23:18:08 +01:00
parent a80bae398a
commit a26bcd8e8f
No known key found for this signature in database
GPG Key ID: 7424F1874854DF23
9 changed files with 423 additions and 33 deletions

View File

@ -29,6 +29,7 @@
namespace OCA\Files_External\AppInfo;
use OCA\Files_External\Config\UserPlaceholderHandler;
use OCA\Files_External\Lib\Auth\PublicKey\RSAPrivateKey;
use OCA\Files_External\Lib\Auth\SMB\KerberosAuth;
use \OCP\AppFramework\App;
@ -67,7 +68,12 @@ use OCP\Files\Config\IUserMountCache;
*/
class Application extends App implements IBackendProvider, IAuthMechanismProvider {
public function __construct(array $urlParams = array()) {
/**
* Application constructor.
*
* @throws \OCP\AppFramework\QueryException
*/
public function __construct(array $urlParams = []) {
parent::__construct('files_external', $urlParams);
$container = $this->getContainer();
@ -76,9 +82,13 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide
return $c->getServer()->query('UserMountCache');
});
/** @var BackendService $backendService */
$backendService = $container->query(BackendService::class);
$backendService->registerBackendProvider($this);
$backendService->registerAuthMechanismProvider($this);
$backendService->registerConfigHandler('user', function() use ($container) {
return $container->query(UserPlaceholderHandler::class);
});
// force-load auth mechanisms since some will register hooks
// TODO: obsolete these and use the TokenProvider to get the user's password from the session

View File

@ -29,7 +29,6 @@ namespace OCA\Files_External\Config;
use OC\Files\Storage\Wrapper\Availability;
use OCA\Files_External\Migration\StorageMigrator;
use OCP\Files\Storage;
use OC\Files\Mount\MountPoint;
use OCP\Files\Storage\IStorageFactory;
use OCA\Files_External\Lib\PersonalMount;
use OCP\Files\Config\IMountProvider;
@ -73,12 +72,11 @@ class ConfigAdapter implements IMountProvider {
*
* @param StorageConfig $storage
* @param IUser $user
* @throws \OCP\AppFramework\QueryException
*/
private function prepareStorageConfig(StorageConfig &$storage, IUser $user) {
foreach ($storage->getBackendOptions() as $option => $value) {
$storage->setBackendOption($option, \OC_Mount_Config::setUserVars(
$user->getUID(), $value
));
$storage->setBackendOption($option, \OC_Mount_Config::substitutePlaceholdersInConfig($value));
}
$objectStore = $storage->getBackendOption('objectstore');

View File

@ -0,0 +1,39 @@
<?php
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Config;
/**
* Interface IConfigHandler
*
* @package OCA\Files_External\Config
* @since 16.0.0
*/
interface IConfigHandler {
/**
* @param mixed $optionValue
* @return mixed the same type as $optionValue
* @since 16.0.0
*/
public function handle($optionValue);
}

View File

@ -0,0 +1,86 @@
<?php
/**
* @copyright Copyright (c) 2018 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Config;
/**
* Trait SimpleSubstitutionTrait
*
* @package OCA\Files_External\Config
* @since 16.0.0
*/
trait SimpleSubstitutionTrait {
/**
* @var string the placeholder without @ prefix
* @since 16.0.0
*/
private $placeholder;
/** @var string */
protected $sanitizedPlaceholder;
/**
* @param mixed $optionValue
* @param string $replacement
* @return mixed
* @since 16.0.0
*/
private function processInput($optionValue, string $replacement) {
$this->checkPlaceholder();
if (is_array($optionValue)) {
foreach ($optionValue as &$value) {
$value = $this->substituteIfString($value, $replacement);
}
} else {
$optionValue = $this->substituteIfString($optionValue, $replacement);
}
return $optionValue;
}
/**
* @throws \RuntimeException
*/
protected function checkPlaceholder(): void {
$this->sanitizedPlaceholder = trim(strtolower($this->placeholder));
if(!(bool)\preg_match('/^[a-z0-9]*$/', $this->sanitizedPlaceholder)) {
throw new \RuntimeException(sprintf(
'Invalid placeholder %s, only [a-z0-9] are allowed', $this->sanitizedPlaceholder
));
}
if($this->sanitizedPlaceholder === '') {
throw new \RuntimeException('Invalid empty placeholder');
}
}
/**
* @param mixed $value
* @param string $replacement
* @return mixed
*/
protected function substituteIfString($value, string $replacement) {
if(is_string($value)) {
return str_ireplace('$' . $this->sanitizedPlaceholder, $replacement, $value);
}
return $value;
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Config;
use OCP\IUserSession;
class UserPlaceholderHandler implements IConfigHandler {
use SimpleSubstitutionTrait;
/** @var IUserSession */
private $session;
public function __construct(IUserSession $session) {
$this->session = $session;
$this->placeholder = 'user';
}
/**
* @param mixed $optionValue
* @return mixed the same type as $optionValue
* @since 16.0.0
*/
public function handle($optionValue) {
$user = $this->session->getUser();
if($user === null) {
return $optionValue;
}
$uid = $user->getUID();
return $this->processInput($optionValue, $uid);
}
}

View File

@ -23,6 +23,7 @@
namespace OCA\Files_External\Service;
use OCA\Files_External\Config\IConfigHandler;
use \OCP\IConfig;
use \OCA\Files_External\Lib\Backend\Backend;
@ -67,6 +68,11 @@ class BackendService {
/** @var IAuthMechanismProvider[] */
private $authMechanismProviders = [];
/** @var callable[] */
private $configHandlerLoaders = [];
private $configHandlers = [];
/**
* @param IConfig $config
*/
@ -280,4 +286,66 @@ class BackendService {
protected function isAllowedAuthMechanism(AuthMechanism $authMechanism) {
return true; // not implemented
}
/**
* registers a configuration handler
*
* The function of the provided $placeholder is mostly to act a sorting
* criteria, so longer placeholders are replaced first. This avoids
* "@user" overwriting parts of "@userMail" and "@userLang", for example.
* The provided value should not contain the @ prefix, only a-z0-9 are
* allowed. Upper case letters are lower cased, the replacement is case-
* insensitive.
*
* The configHandlerLoader should just instantiate the handler on demand.
* For now all handlers are instantiated when a mount is loaded, independent
* of whether the placeholder is present or not. This may change in future.
*
* @since 16.0.0
*/
public function registerConfigHandler(string $placeholder, callable $configHandlerLoader) {
$placeholder = trim(strtolower($placeholder));
if(!(bool)\preg_match('/^[a-z0-9]*$/', $placeholder)) {
throw new \RuntimeException(sprintf(
'Invalid placeholder %s, only [a-z0-9] are allowed', $placeholder
));
}
if($placeholder === '') {
throw new \RuntimeException('Invalid empty placeholder');
}
if(isset($this->configHandlerLoaders[$placeholder]) || isset($this->configHandlers[$placeholder])) {
throw new \RuntimeException(sprintf('A handler is already registered for %s', $placeholder));
}
$this->configHandlerLoaders[$placeholder] = $configHandlerLoader;
}
protected function loadConfigHandlers():void {
$newLoaded = false;
foreach ($this->configHandlerLoaders as $placeholder => $loader) {
$handler = $loader();
if(!$handler instanceof IConfigHandler) {
throw new \RuntimeException(sprintf(
'Handler for %s is not an instance of IConfigHandler', $placeholder
));
}
$this->configHandlers[] = $handler;
$newLoaded = true;
}
$this->configHandlerLoaders = [];
if($newLoaded) {
// ensure those with longest placeholders come first,
// to avoid substring matches
uksort($this->configHandlers, function ($phA, $phB) {
return strlen($phB) <=> strlen($phA);
});
}
}
/**
* @since 16.0.0
*/
public function getConfigHandlers() {
$this->loadConfigHandlers();
return $this->configHandlers;
}
}

View File

@ -35,6 +35,8 @@
*
*/
use OCA\Files_External\Config\IConfigHandler;
use OCA\Files_External\Config\UserPlaceholderHandler;
use phpseclib\Crypt\AES;
use \OCA\Files_External\AppInfo\Application;
use \OCA\Files_External\Lib\Backend\LegacyBackend;
@ -104,7 +106,7 @@ class OC_Mount_Config {
$mountPoint = '/'.$uid.'/files'.$storage->getMountPoint();
$mountEntry = self::prepareMountPointEntry($storage, false);
foreach ($mountEntry['options'] as &$option) {
$option = self::setUserVars($uid, $option);
$option = self::substitutePlaceholdersInConfig($option);
}
$mountPoints[$mountPoint] = $mountEntry;
}
@ -113,7 +115,7 @@ class OC_Mount_Config {
$mountPoint = '/'.$uid.'/files'.$storage->getMountPoint();
$mountEntry = self::prepareMountPointEntry($storage, true);
foreach ($mountEntry['options'] as &$option) {
$option = self::setUserVars($uid, $option);
$option = self::substitutePlaceholdersInConfig($uid, $option);
}
$mountPoints[$mountPoint] = $mountEntry;
}
@ -199,18 +201,26 @@ class OC_Mount_Config {
* @param string $user user value
* @param string|array $input
* @return string
* @deprecated use self::substitutePlaceholdersInConfig($input)
*/
public static function setUserVars($user, $input) {
if (is_array($input)) {
foreach ($input as &$value) {
if (is_string($value)) {
$value = str_replace('$user', $user, $value);
}
}
} else {
if (is_string($input)) {
$input = str_replace('$user', $user, $input);
}
$handler = self::$app->getContainer()->query(UserPlaceholderHandler::class);
return $handler->handle($input);
}
/**
* @param mixed $input
* @return mixed
* @throws \OCP\AppFramework\QueryException
* @since 16.0.0
*/
public static function substitutePlaceholdersInConfig($input) {
/** @var BackendService $backendService */
$backendService = self::$app->getContainer()->query(BackendService::class);
/** @var IConfigHandler[] $handlers */
$handlers = $backendService->getConfigHandlers();
foreach ($handlers as $handler) {
$input = $handler->handle($input);
}
return $input;
}
@ -229,7 +239,7 @@ class OC_Mount_Config {
return StorageNotAvailableException::STATUS_SUCCESS;
}
foreach ($options as &$option) {
$option = self::setUserVars(OCP\User::getUser(), $option);
$option = self::substitutePlaceholdersInConfig($option);
}
if (class_exists($class)) {
try {

View File

@ -0,0 +1,81 @@
<?php
/**
* @copyright Copyright (c) 2018 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\files_external\tests\Config;
use OCA\Files_External\Config\UserPlaceholderHandler;
use OCP\IUser;
use OCP\IUserSession;
class UserPlaceholderHandlerTest extends \Test\TestCase {
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject */
protected $user;
/** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
protected $session;
/** @var UserPlaceholderHandler */
protected $handler;
public function setUp() {
parent::setUp();
$this->user = $this->createMock(IUser::class);
$this->user->expects($this->any())
->method('getUid')
->willReturn('alice');
$this->session = $this->createMock(IUserSession::class);
$this->handler = new UserPlaceholderHandler($this->session);
}
protected function setUser() {
$this->session->expects($this->any())
->method('getUser')
->willReturn($this->user);
}
public function optionProvider() {
return [
['/foo/bar/$user/foobar', '/foo/bar/alice/foobar'],
[['/foo/bar/$user/foobar'], ['/foo/bar/alice/foobar']],
[['/FOO/BAR/$USER/FOOBAR'], ['/FOO/BAR/alice/FOOBAR']],
];
}
/**
* @dataProvider optionProvider
*/
public function testHandle($option, $expected) {
$this->setUser();
$this->assertSame($expected, $this->handler->handle($option));
}
/**
* @dataProvider optionProvider
*/
public function testHandleNoUser($option) {
$this->assertSame($option, $this->handler->handle($option));
}
}

View File

@ -23,31 +23,27 @@
*/
namespace OCA\Files_External\Tests\Service;
use OCA\Files_External\Config\IConfigHandler;
use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\Backend\Backend;
use OCA\Files_External\Lib\Config\IAuthMechanismProvider;
use OCA\Files_External\Lib\Config\IBackendProvider;
use \OCA\Files_External\Service\BackendService;
use OCA\Files_External\Service\BackendService;
use OCP\IConfig;
use OCP\IL10N;
class BackendServiceTest extends \Test\TestCase {
/** @var \OCP\IConfig */
/** @var \OCP\IConfig|\PHPUnit_Framework_MockObject_MockObject */
protected $config;
/** @var \OCP\IL10N */
protected $l10n;
protected function setUp() {
$this->config = $this->createMock(IConfig::class);
$this->l10n = $this->createMock(IL10N::class);
}
/**
* @param string $class
*
* @return \OCA\Files_External\Lib\Backend\Backend
* @return \OCA\Files_External\Lib\Backend\Backend|\PHPUnit_Framework_MockObject_MockObject
*/
protected function getBackendMock($class) {
$backend = $this->getMockBuilder(Backend::class)
@ -61,7 +57,7 @@ class BackendServiceTest extends \Test\TestCase {
/**
* @param string $class
*
* @return \OCA\Files_External\Lib\Auth\AuthMechanism
* @return \OCA\Files_External\Lib\Auth\AuthMechanism|\PHPUnit_Framework_MockObject_MockObject
*/
protected function getAuthMechanismMock($class) {
$backend = $this->getMockBuilder(AuthMechanism::class)
@ -73,10 +69,11 @@ class BackendServiceTest extends \Test\TestCase {
}
public function testRegisterBackend() {
$service = new BackendService($this->config, $this->l10n);
$service = new BackendService($this->config);
$backend = $this->getBackendMock('\Foo\Bar');
/** @var \OCA\Files_External\Lib\Backend\Backend|\PHPUnit_Framework_MockObject_MockObject $backendAlias */
$backendAlias = $this->getMockBuilder(Backend::class)
->disableOriginalConstructor()
->getMock();
@ -100,11 +97,12 @@ class BackendServiceTest extends \Test\TestCase {
}
public function testBackendProvider() {
$service = new BackendService($this->config, $this->l10n);
$service = new BackendService($this->config);
$backend1 = $this->getBackendMock('\Foo\Bar');
$backend2 = $this->getBackendMock('\Bar\Foo');
/** @var IBackendProvider|\PHPUnit_Framework_MockObject_MockObject $providerMock */
$providerMock = $this->createMock(IBackendProvider::class);
$providerMock->expects($this->once())
->method('getBackends')
@ -118,11 +116,12 @@ class BackendServiceTest extends \Test\TestCase {
}
public function testAuthMechanismProvider() {
$service = new BackendService($this->config, $this->l10n);
$service = new BackendService($this->config);
$backend1 = $this->getAuthMechanismMock('\Foo\Bar');
$backend2 = $this->getAuthMechanismMock('\Bar\Foo');
/** @var IAuthMechanismProvider|\PHPUnit_Framework_MockObject_MockObject $providerMock */
$providerMock = $this->createMock(IAuthMechanismProvider::class);
$providerMock->expects($this->once())
->method('getAuthMechanisms')
@ -136,18 +135,20 @@ class BackendServiceTest extends \Test\TestCase {
}
public function testMultipleBackendProviders() {
$service = new BackendService($this->config, $this->l10n);
$service = new BackendService($this->config);
$backend1a = $this->getBackendMock('\Foo\Bar');
$backend1b = $this->getBackendMock('\Bar\Foo');
$backend2 = $this->getBackendMock('\Dead\Beef');
/** @var IBackendProvider|\PHPUnit_Framework_MockObject_MockObject $provider1Mock */
$provider1Mock = $this->createMock(IBackendProvider::class);
$provider1Mock->expects($this->once())
->method('getBackends')
->willReturn([$backend1a, $backend1b]);
$service->registerBackendProvider($provider1Mock);
/** @var IBackendProvider|\PHPUnit_Framework_MockObject_MockObject $provider2Mock */
$provider2Mock = $this->createMock(IBackendProvider::class);
$provider2Mock->expects($this->once())
->method('getBackends')
@ -169,7 +170,7 @@ class BackendServiceTest extends \Test\TestCase {
['files_external', 'user_mounting_backends', '', 'identifier:\User\Mount\Allowed,identifier_alias']
]));
$service = new BackendService($this->config, $this->l10n);
$service = new BackendService($this->config);
$backendAllowed = $this->getBackendMock('\User\Mount\Allowed');
$backendAllowed->expects($this->never())
@ -193,7 +194,7 @@ class BackendServiceTest extends \Test\TestCase {
}
public function testGetAvailableBackends() {
$service = new BackendService($this->config, $this->l10n);
$service = new BackendService($this->config);
$backendAvailable = $this->getBackendMock('\Backend\Available');
$backendAvailable->expects($this->once())
@ -216,5 +217,49 @@ class BackendServiceTest extends \Test\TestCase {
$this->assertArrayNotHasKey('identifier:\Backend\NotAvailable', $availableBackends);
}
public function invalidConfigPlaceholderProvider() {
return [
[['@user']],
[['hællo']],
[['spa ce']],
[['yo\o']],
[['<script>…</script>']],
[['xxyoloxx', 'invÆlid']],
[['tautology', 'tautology']],
[['tautology2', 'TAUTOLOGY2']],
];
}
/**
* @dataProvider invalidConfigPlaceholderProvider
* @expectedException \RuntimeException
*/
public function testRegisterConfigHandlerInvalid(array $placeholders) {
$service = new BackendService($this->config);
$mock = $this->createMock(IConfigHandler::class);
$cb = function () use ($mock) { return $mock; };
foreach ($placeholders as $placeholder) {
$service->registerConfigHandler($placeholder, $cb);
}
}
public function testConfigHandlers() {
$service = new BackendService($this->config);
$mock = $this->createMock(IConfigHandler::class);
$mock->expects($this->exactly(3))
->method('handle');
$cb = function () use ($mock) { return $mock; };
$service->registerConfigHandler('one', $cb);
$service->registerConfigHandler('2', $cb);
$service->registerConfigHandler('Three', $cb);
/** @var IConfigHandler[] $handlers */
$handlers = $service->getConfigHandlers();
foreach ($handlers as $handler) {
$handler->handle('Something');
}
}
}