External storage 'Login credentials' auth mechanismtags/v9.0beta1
@@ -108,6 +108,7 @@ class Application extends App { | |||
// AuthMechanism::SCHEME_PASSWORD mechanisms | |||
$container->query('OCA\Files_External\Lib\Auth\Password\Password'), | |||
$container->query('OCA\Files_External\Lib\Auth\Password\SessionCredentials'), | |||
$container->query('OCA\Files_External\Lib\Auth\Password\LoginCredentials'), | |||
// AuthMechanism::SCHEME_OAUTH1 mechanisms | |||
$container->query('OCA\Files_External\Lib\Auth\OAuth1\OAuth1'), |
@@ -13,7 +13,7 @@ | |||
<admin>admin-external-storage</admin> | |||
</documentation> | |||
<rememberlogin>false</rememberlogin> | |||
<version>0.5.1</version> | |||
<version>0.5.2</version> | |||
<types> | |||
<filesystem/> | |||
</types> |
@@ -212,6 +212,15 @@ abstract class StoragesController extends Controller { | |||
return null; | |||
} | |||
protected function manipulateStorageConfig(StorageConfig $storage) { | |||
/** @var AuthMechanism */ | |||
$authMechanism = $storage->getAuthMechanism(); | |||
$authMechanism->manipulateStorageConfig($storage); | |||
/** @var Backend */ | |||
$backend = $storage->getBackend(); | |||
$backend->manipulateStorageConfig($storage); | |||
} | |||
/** | |||
* Check whether the given storage is available / valid. | |||
* | |||
@@ -222,13 +231,10 @@ abstract class StoragesController extends Controller { | |||
*/ | |||
protected function updateStorageStatus(StorageConfig &$storage) { | |||
try { | |||
/** @var AuthMechanism */ | |||
$authMechanism = $storage->getAuthMechanism(); | |||
$authMechanism->manipulateStorageConfig($storage); | |||
$this->manipulateStorageConfig($storage); | |||
/** @var Backend */ | |||
$backend = $storage->getBackend(); | |||
$backend->manipulateStorageConfig($storage); | |||
// update status (can be time-consuming) | |||
$storage->setStatus( | |||
\OC_Mount_Config::getBackendStatus( |
@@ -21,6 +21,7 @@ | |||
namespace OCA\Files_External\Controller; | |||
use OCA\Files_External\Lib\Auth\AuthMechanism; | |||
use \OCP\IRequest; | |||
use \OCP\IL10N; | |||
use \OCP\AppFramework\Http\DataResponse; | |||
@@ -30,11 +31,17 @@ use \OCA\Files_external\Service\UserGlobalStoragesService; | |||
use \OCA\Files_external\NotFoundException; | |||
use \OCA\Files_external\Lib\StorageConfig; | |||
use \OCA\Files_External\Lib\Backend\Backend; | |||
use OCP\IUserSession; | |||
/** | |||
* User global storages controller | |||
*/ | |||
class UserGlobalStoragesController extends StoragesController { | |||
/** | |||
* @var IUserSession | |||
*/ | |||
private $userSession; | |||
/** | |||
* Creates a new user global storages controller. | |||
* | |||
@@ -42,12 +49,14 @@ class UserGlobalStoragesController extends StoragesController { | |||
* @param IRequest $request request object | |||
* @param IL10N $l10n l10n service | |||
* @param UserGlobalStoragesService $userGlobalStoragesService storage service | |||
* @param IUserSession $userSession | |||
*/ | |||
public function __construct( | |||
$AppName, | |||
IRequest $request, | |||
IL10N $l10n, | |||
UserGlobalStoragesService $userGlobalStoragesService | |||
UserGlobalStoragesService $userGlobalStoragesService, | |||
IUserSession $userSession | |||
) { | |||
parent::__construct( | |||
$AppName, | |||
@@ -55,6 +64,7 @@ class UserGlobalStoragesController extends StoragesController { | |||
$l10n, | |||
$userGlobalStoragesService | |||
); | |||
$this->userSession = $userSession; | |||
} | |||
/** | |||
@@ -78,6 +88,15 @@ class UserGlobalStoragesController extends StoragesController { | |||
); | |||
} | |||
protected function manipulateStorageConfig(StorageConfig $storage) { | |||
/** @var AuthMechanism */ | |||
$authMechanism = $storage->getAuthMechanism(); | |||
$authMechanism->manipulateStorageConfig($storage, $this->userSession->getUser()); | |||
/** @var Backend */ | |||
$backend = $storage->getBackend(); | |||
$backend->manipulateStorageConfig($storage, $this->userSession->getUser()); | |||
} | |||
/** | |||
* Get an external storage entry. | |||
* |
@@ -23,6 +23,7 @@ | |||
namespace OCA\Files_External\Controller; | |||
use OCA\Files_External\Lib\Auth\AuthMechanism; | |||
use \OCP\IConfig; | |||
use \OCP\IUserSession; | |||
use \OCP\IRequest; | |||
@@ -40,6 +41,11 @@ use \OCA\Files_External\Lib\Backend\Backend; | |||
* User storages controller | |||
*/ | |||
class UserStoragesController extends StoragesController { | |||
/** | |||
* @var IUserSession | |||
*/ | |||
private $userSession; | |||
/** | |||
* Creates a new user storages controller. | |||
* | |||
@@ -47,12 +53,14 @@ class UserStoragesController extends StoragesController { | |||
* @param IRequest $request request object | |||
* @param IL10N $l10n l10n service | |||
* @param UserStoragesService $userStoragesService storage service | |||
* @param IUserSession $userSession | |||
*/ | |||
public function __construct( | |||
$AppName, | |||
IRequest $request, | |||
IL10N $l10n, | |||
UserStoragesService $userStoragesService | |||
UserStoragesService $userStoragesService, | |||
IUserSession $userSession | |||
) { | |||
parent::__construct( | |||
$AppName, | |||
@@ -60,6 +68,16 @@ class UserStoragesController extends StoragesController { | |||
$l10n, | |||
$userStoragesService | |||
); | |||
$this->userSession = $userSession; | |||
} | |||
protected function manipulateStorageConfig(StorageConfig $storage) { | |||
/** @var AuthMechanism */ | |||
$authMechanism = $storage->getAuthMechanism(); | |||
$authMechanism->manipulateStorageConfig($storage, $this->userSession->getUser()); | |||
/** @var Backend */ | |||
$backend = $storage->getBackend(); | |||
$backend->manipulateStorageConfig($storage, $this->userSession->getUser()); | |||
} | |||
/** |
@@ -0,0 +1,92 @@ | |||
<?php | |||
/** | |||
* @author Robin McCorkell <rmccorkell@owncloud.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OCA\Files_External\Lib\Auth\Password; | |||
use \OCP\IL10N; | |||
use \OCP\IUser; | |||
use \OCA\Files_External\Lib\DefinitionParameter; | |||
use \OCA\Files_External\Lib\Auth\AuthMechanism; | |||
use \OCA\Files_External\Lib\StorageConfig; | |||
use \OCP\ISession; | |||
use \OCP\Security\ICredentialsManager; | |||
use \OCP\Files\Storage; | |||
use \OCA\Files_External\Lib\SessionStorageWrapper; | |||
use \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; | |||
/** | |||
* Username and password from login credentials, saved in DB | |||
*/ | |||
class LoginCredentials extends AuthMechanism { | |||
const CREDENTIALS_IDENTIFIER = 'password::logincredentials/credentials'; | |||
/** @var ISession */ | |||
protected $session; | |||
/** @var ICredentialsManager */ | |||
protected $credentialsManager; | |||
public function __construct(IL10N $l, ISession $session, ICredentialsManager $credentialsManager) { | |||
$this->session = $session; | |||
$this->credentialsManager = $credentialsManager; | |||
$this | |||
->setIdentifier('password::logincredentials') | |||
->setScheme(self::SCHEME_PASSWORD) | |||
->setText($l->t('Login credentials')) | |||
->addParameters([ | |||
]) | |||
; | |||
\OCP\Util::connectHook('OC_User', 'post_login', $this, 'authenticate'); | |||
} | |||
/** | |||
* Hook listener on post login | |||
* | |||
* @param array $params | |||
*/ | |||
public function authenticate(array $params) { | |||
$userId = $params['uid']; | |||
$credentials = [ | |||
'user' => $this->session->get('loginname'), | |||
'password' => $params['password'] | |||
]; | |||
$this->credentialsManager->store($userId, self::CREDENTIALS_IDENTIFIER, $credentials); | |||
} | |||
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { | |||
if (!isset($user)) { | |||
throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved'); | |||
} | |||
$uid = $user->getUID(); | |||
$credentials = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER); | |||
if (!isset($credentials)) { | |||
throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved'); | |||
} | |||
$storage->setBackendOption('user', $credentials['user']); | |||
$storage->setBackendOption('password', $credentials['password']); | |||
} | |||
} |
@@ -21,6 +21,7 @@ | |||
namespace OCA\Files_External\Lib\Auth\Password; | |||
use \OCP\IUser; | |||
use \OCP\IL10N; | |||
use \OCA\Files_External\Lib\DefinitionParameter; | |||
use \OCA\Files_External\Lib\Auth\AuthMechanism; | |||
@@ -66,7 +67,7 @@ class SessionCredentials extends AuthMechanism { | |||
$this->session->set('password::sessioncredentials/credentials', $this->crypto->encrypt(json_encode($params))); | |||
} | |||
public function manipulateStorageConfig(StorageConfig &$storage) { | |||
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { | |||
$encrypted = $this->session->get('password::sessioncredentials/credentials'); | |||
if (!isset($encrypted)) { | |||
throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved'); |
@@ -26,6 +26,7 @@ use \OCA\Files_External\Lib\DefinitionParameter; | |||
use \OCA\Files_External\Lib\Auth\AuthMechanism; | |||
use \OCA\Files_External\Lib\StorageConfig; | |||
use \OCP\IConfig; | |||
use OCP\IUser; | |||
use \phpseclib\Crypt\RSA as RSACrypt; | |||
/** | |||
@@ -55,7 +56,7 @@ class RSA extends AuthMechanism { | |||
; | |||
} | |||
public function manipulateStorageConfig(StorageConfig &$storage) { | |||
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { | |||
$auth = new RSACrypt(); | |||
$auth->setPassword($this->config->getSystemValue('secret', '')); | |||
if (!$auth->loadKey($storage->getBackendOption('private_key'))) { |
@@ -30,6 +30,7 @@ use \OCA\Files_External\Lib\StorageConfig; | |||
use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; | |||
use \OCA\Files_External\Lib\Auth\Password\Password; | |||
use OCP\IUser; | |||
class SMB extends Backend { | |||
@@ -56,8 +57,9 @@ class SMB extends Backend { | |||
/** | |||
* @param StorageConfig $storage | |||
* @param IUser $user | |||
*/ | |||
public function manipulateStorageConfig(StorageConfig &$storage) { | |||
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { | |||
$user = $storage->getBackendOption('user'); | |||
if ($domain = $storage->getBackendOption('domain')) { | |||
$storage->setBackendOption('user', $domain.'\\'.$user); |
@@ -30,6 +30,7 @@ use \OCA\Files_External\Lib\Auth\Password\SessionCredentials; | |||
use \OCA\Files_External\Lib\StorageConfig; | |||
use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; | |||
use \OCA\Files_External\Lib\Backend\SMB; | |||
use OCP\IUser; | |||
/** | |||
* Deprecated SMB_OC class - use SMB with the password::sessioncredentials auth mechanism | |||
@@ -59,7 +60,7 @@ class SMB_OC extends Backend { | |||
; | |||
} | |||
public function manipulateStorageConfig(StorageConfig &$storage) { | |||
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { | |||
$username_as_share = ($storage->getBackendOption('username_as_share') === true); | |||
if ($username_as_share) { |
@@ -85,8 +85,8 @@ class ConfigAdapter implements IMountProvider { | |||
$storage->setBackendOption('objectstore', new $objectClass($objectStore)); | |||
} | |||
$storage->getAuthMechanism()->manipulateStorageConfig($storage); | |||
$storage->getBackend()->manipulateStorageConfig($storage); | |||
$storage->getAuthMechanism()->manipulateStorageConfig($storage, $user); | |||
$storage->getBackend()->manipulateStorageConfig($storage, $user); | |||
} | |||
/** |
@@ -21,6 +21,7 @@ | |||
namespace OCA\Files_External\Lib; | |||
use \OCP\IUser; | |||
use \OCP\Files\Storage; | |||
use \OCA\Files_External\Lib\StorageConfig; | |||
use \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; | |||
@@ -45,10 +46,11 @@ trait StorageModifierTrait { | |||
* Modify a StorageConfig parameters | |||
* | |||
* @param StorageConfig $storage | |||
* @param IUser $user User the storage is being used as | |||
* @throws InsufficientDataForMeaningfulAnswerException | |||
* @throws StorageNotAvailableException | |||
*/ | |||
public function manipulateStorageConfig(StorageConfig &$storage) { | |||
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { | |||
} | |||
/** |
@@ -48,7 +48,8 @@ class UserStoragesControllerTest extends StoragesControllerTest { | |||
'files_external', | |||
$this->getMock('\OCP\IRequest'), | |||
$this->getMock('\OCP\IL10N'), | |||
$this->service | |||
$this->service, | |||
$this->getMock('\OCP\IUserSession') | |||
); | |||
} | |||
@@ -1580,5 +1580,60 @@ | |||
</table> | |||
<table> | |||
<!-- | |||
Encrypted credentials storage | |||
--> | |||
<name>*dbprefix*credentials</name> | |||
<declaration> | |||
<field> | |||
<name>user</name> | |||
<type>text</type> | |||
<default></default> | |||
<notnull>false</notnull> | |||
<length>64</length> | |||
</field> | |||
<field> | |||
<name>identifier</name> | |||
<type>text</type> | |||
<notnull>true</notnull> | |||
<length>64</length> | |||
</field> | |||
<field> | |||
<name>credentials</name> | |||
<type>clob</type> | |||
<notnull>false</notnull> | |||
</field> | |||
<index> | |||
<name>credentials_user_id</name> | |||
<primary>true</primary> | |||
<unique>true</unique> | |||
<field> | |||
<name>user</name> | |||
<sorting>ascending</sorting> | |||
</field> | |||
<field> | |||
<name>identifier</name> | |||
<sorting>ascending</sorting> | |||
</field> | |||
</index> | |||
<index> | |||
<name>credentials_user</name> | |||
<unique>false</unique> | |||
<field> | |||
<name>user</name> | |||
<sorting>ascending</sorting> | |||
</field> | |||
</index> | |||
</declaration> | |||
</table> | |||
</database> |
@@ -205,57 +205,28 @@ class AllConfig implements \OCP\IConfig { | |||
// TODO - FIXME | |||
$this->fixDIInit(); | |||
// Check if the key does exist | |||
$sql = 'SELECT `configvalue` FROM `*PREFIX*preferences` '. | |||
'WHERE `userid` = ? AND `appid` = ? AND `configkey` = ?'; | |||
$result = $this->connection->executeQuery($sql, array($userId, $appName, $key)); | |||
$oldValue = $result->fetchColumn(); | |||
$result->closeCursor(); | |||
$exists = $oldValue !== false; | |||
if($oldValue === strval($value)) { | |||
// no changes | |||
return; | |||
$preconditionArray = []; | |||
if (isset($preCondition)) { | |||
$preconditionArray = [ | |||
'configvalue' => $preCondition, | |||
]; | |||
} | |||
$affectedRows = 0; | |||
if (!$exists && $preCondition === null) { | |||
$this->connection->insertIfNotExist('*PREFIX*preferences', [ | |||
'configvalue' => $value, | |||
'userid' => $userId, | |||
'appid' => $appName, | |||
'configkey' => $key, | |||
], ['configkey', 'userid', 'appid']); | |||
$affectedRows = 1; | |||
} elseif ($exists) { | |||
$data = array($value, $userId, $appName, $key); | |||
$sql = 'UPDATE `*PREFIX*preferences` SET `configvalue` = ? '. | |||
'WHERE `userid` = ? AND `appid` = ? AND `configkey` = ? '; | |||
if($preCondition !== null) { | |||
if($this->getSystemValue('dbtype', 'sqlite') === 'oci') { | |||
//oracle hack: need to explicitly cast CLOB to CHAR for comparison | |||
$sql .= 'AND to_char(`configvalue`) = ?'; | |||
} else { | |||
$sql .= 'AND `configvalue` = ?'; | |||
} | |||
$data[] = $preCondition; | |||
} | |||
$affectedRows = $this->connection->executeUpdate($sql, $data); | |||
} | |||
$this->connection->setValues('preferences', [ | |||
'userid' => $userId, | |||
'appid' => $appName, | |||
'configkey' => $key, | |||
], [ | |||
'configvalue' => $value, | |||
], $preconditionArray); | |||
// only add to the cache if we already loaded data for the user | |||
if ($affectedRows > 0 && isset($this->userCache[$userId])) { | |||
if (isset($this->userCache[$userId])) { | |||
if (!isset($this->userCache[$userId][$appName])) { | |||
$this->userCache[$userId][$appName] = array(); | |||
} | |||
$this->userCache[$userId][$appName][$key] = $value; | |||
} | |||
if ($preCondition !== null && $affectedRows === 0) { | |||
throw new PreConditionNotMetException; | |||
} | |||
} | |||
/** |
@@ -146,6 +146,21 @@ class Db implements IDb { | |||
return $this->connection->insertIfNotExist($table, $input, $compare); | |||
} | |||
/** | |||
* Insert or update a row value | |||
* | |||
* @param string $table | |||
* @param array $keys (column name => value) | |||
* @param array $values (column name => value) | |||
* @param array $updatePreconditionValues ensure values match preconditions (column name => value) | |||
* @return int number of new rows | |||
* @throws \Doctrine\DBAL\DBALException | |||
* @throws PreconditionNotMetException | |||
*/ | |||
public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) { | |||
return $this->connection->setValues($table, $keys, $values, $updatePreconditionValues); | |||
} | |||
/** | |||
* Start a transaction | |||
*/ |
@@ -215,6 +215,10 @@ class DIContainer extends SimpleContainer implements IAppContainer { | |||
return $this->getServer()->getHasher(); | |||
}); | |||
$this->registerService('OCP\\Security\\ICredentialsManager', function($c) { | |||
return $this->getServer()->getCredentialsManager(); | |||
}); | |||
$this->registerService('OCP\\Security\\ISecureRandom', function($c) { | |||
return $this->getServer()->getSecureRandom(); | |||
}); |
@@ -32,6 +32,7 @@ use Doctrine\Common\EventManager; | |||
use OC\DB\QueryBuilder\ExpressionBuilder; | |||
use OC\DB\QueryBuilder\QueryBuilder; | |||
use OCP\IDBConnection; | |||
use OCP\PreconditionNotMetException; | |||
class Connection extends \Doctrine\DBAL\Connection implements IDBConnection { | |||
/** | |||
@@ -241,6 +242,64 @@ class Connection extends \Doctrine\DBAL\Connection implements IDBConnection { | |||
return $this->adapter->insertIfNotExist($table, $input, $compare); | |||
} | |||
private function getType($value) { | |||
if (is_bool($value)) { | |||
return \PDO::PARAM_BOOL; | |||
} else if (is_int($value)) { | |||
return \PDO::PARAM_INT; | |||
} else { | |||
return \PDO::PARAM_STR; | |||
} | |||
} | |||
/** | |||
* Insert or update a row value | |||
* | |||
* @param string $table | |||
* @param array $keys (column name => value) | |||
* @param array $values (column name => value) | |||
* @param array $updatePreconditionValues ensure values match preconditions (column name => value) | |||
* @return int number of new rows | |||
* @throws \Doctrine\DBAL\DBALException | |||
* @throws PreconditionNotMetException | |||
*/ | |||
public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) { | |||
try { | |||
$insertQb = $this->getQueryBuilder(); | |||
$insertQb->insert($table) | |||
->values( | |||
array_map(function($value) use ($insertQb) { | |||
return $insertQb->createNamedParameter($value, $this->getType($value)); | |||
}, array_merge($keys, $values)) | |||
); | |||
return $insertQb->execute(); | |||
} catch (\Doctrine\DBAL\Exception\ConstraintViolationException $e) { | |||
// value already exists, try update | |||
$updateQb = $this->getQueryBuilder(); | |||
$updateQb->update($table); | |||
foreach ($values as $name => $value) { | |||
$updateQb->set($name, $updateQb->createNamedParameter($value), $this->getType($value)); | |||
} | |||
$where = $updateQb->expr()->andx(); | |||
$whereValues = array_merge($keys, $updatePreconditionValues); | |||
foreach ($whereValues as $name => $value) { | |||
$where->add($updateQb->expr()->eq( | |||
$name, | |||
$updateQb->createNamedParameter($value, $this->getType($value)), | |||
$this->getType($value) | |||
)); | |||
} | |||
$updateQb->where($where); | |||
$affected = $updateQb->execute(); | |||
if ($affected === 0) { | |||
throw new PreconditionNotMetException(); | |||
} | |||
return 0; | |||
} | |||
} | |||
/** | |||
* returns the error code and message as a string for logging | |||
* works with DoctrineException |
@@ -27,10 +27,10 @@ use OCP\IDBConnection; | |||
class ExpressionBuilder implements IExpressionBuilder { | |||
/** @var \Doctrine\DBAL\Query\Expression\ExpressionBuilder */ | |||
private $expressionBuilder; | |||
protected $expressionBuilder; | |||
/** @var QuoteHelper */ | |||
private $helper; | |||
protected $helper; | |||
/** | |||
* Initializes a new <tt>ExpressionBuilder</tt>. | |||
@@ -109,10 +109,12 @@ class ExpressionBuilder implements IExpressionBuilder { | |||
* | |||
* @param mixed $x The left expression. | |||
* @param mixed $y The right expression. | |||
* @param int|null $type one of the \PDO::PARAM_* constants | |||
* required when comparing text fields for oci compatibility | |||
* | |||
* @return string | |||
*/ | |||
public function eq($x, $y) { | |||
public function eq($x, $y, $type = null) { | |||
$x = $this->helper->quoteColumnName($x); | |||
$y = $this->helper->quoteColumnName($y); | |||
return $this->expressionBuilder->eq($x, $y); |
@@ -0,0 +1,33 @@ | |||
<?php | |||
/** | |||
* @author Joas Schilling <nickvergessen@owncloud.com> | |||
* | |||
* @copyright Copyright (c) 2016, 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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OC\DB\QueryBuilder; | |||
class OCIExpressionBuilder extends ExpressionBuilder { | |||
public function eq($x, $y, $type = null) { | |||
$x = $this->helper->quoteColumnName($x); | |||
$y = $this->helper->quoteColumnName($y); | |||
if ($type === \PDO::PARAM_STR) { | |||
$x = new QueryFunction('to_char(' . $x . ')'); | |||
} | |||
return $this->expressionBuilder->eq($x, $y); | |||
} | |||
} |
@@ -21,6 +21,7 @@ | |||
namespace OC\DB\QueryBuilder; | |||
use OC\DB\OracleConnection; | |||
use OCP\DB\QueryBuilder\IQueryBuilder; | |||
use OCP\DB\QueryBuilder\IQueryFunction; | |||
use OCP\DB\QueryBuilder\IParameter; | |||
@@ -82,7 +83,11 @@ class QueryBuilder implements IQueryBuilder { | |||
* @return \OCP\DB\QueryBuilder\IExpressionBuilder | |||
*/ | |||
public function expr() { | |||
return new ExpressionBuilder($this->connection); | |||
if ($this->connection instanceof OracleConnection) { | |||
return new OCIExpressionBuilder($this->connection); | |||
} else { | |||
return new ExpressionBuilder($this->connection); | |||
} | |||
} | |||
/** |
@@ -0,0 +1,125 @@ | |||
<?php | |||
/** | |||
* @author Robin McCorkell <rmccorkell@owncloud.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OC\Security; | |||
use OCP\Security\ICrypto; | |||
use OCP\IDBConnection; | |||
use OCP\Security\ICredentialsManager; | |||
use OCP\IConfig; | |||
/** | |||
* Store and retrieve credentials for external services | |||
* | |||
* @package OC\Security | |||
*/ | |||
class CredentialsManager implements ICredentialsManager { | |||
const DB_TABLE = 'credentials'; | |||
/** @var ICrypto */ | |||
protected $crypto; | |||
/** @var IDBConnection */ | |||
protected $dbConnection; | |||
/** | |||
* @param ICrypto $crypto | |||
* @param IDBConnection $dbConnection | |||
*/ | |||
public function __construct(ICrypto $crypto, IDBConnection $dbConnection) { | |||
$this->crypto = $crypto; | |||
$this->dbConnection = $dbConnection; | |||
} | |||
/** | |||
* Store a set of credentials | |||
* | |||
* @param string|null $userId Null for system-wide credentials | |||
* @param string $identifier | |||
* @param mixed $credentials | |||
*/ | |||
public function store($userId, $identifier, $credentials) { | |||
$value = $this->crypto->encrypt(json_encode($credentials)); | |||
$this->dbConnection->setValues(self::DB_TABLE, [ | |||
'user' => $userId, | |||
'identifier' => $identifier, | |||
], [ | |||
'credentials' => $value, | |||
]); | |||
} | |||
/** | |||
* Retrieve a set of credentials | |||
* | |||
* @param string|null $userId Null for system-wide credentials | |||
* @param string $identifier | |||
* @return mixed | |||
*/ | |||
public function retrieve($userId, $identifier) { | |||
$qb = $this->dbConnection->getQueryBuilder(); | |||
$qb->select('credentials') | |||
->from(self::DB_TABLE) | |||
->where($qb->expr()->eq('user', $qb->createNamedParameter($userId))) | |||
->andWhere($qb->expr()->eq('identifier', $qb->createNamedParameter($identifier))) | |||
; | |||
$result = $qb->execute()->fetch(); | |||
if (!$result) { | |||
return null; | |||
} | |||
$value = $result['credentials']; | |||
return json_decode($this->crypto->decrypt($value), true); | |||
} | |||
/** | |||
* Delete a set of credentials | |||
* | |||
* @param string|null $userId Null for system-wide credentials | |||
* @param string $identifier | |||
* @return int rows removed | |||
*/ | |||
public function delete($userId, $identifier) { | |||
$qb = $this->dbConnection->getQueryBuilder(); | |||
$qb->delete(self::DB_TABLE) | |||
->where($qb->expr()->eq('user', $qb->createNamedParameter($userId))) | |||
->andWhere($qb->expr()->eq('identifier', $qb->createNamedParameter($identifier))) | |||
; | |||
return $qb->execute(); | |||
} | |||
/** | |||
* Erase all credentials stored for a user | |||
* | |||
* @param string $userId | |||
* @return int rows removed | |||
*/ | |||
public function erase($userId) { | |||
$qb = $this->dbConnection->getQueryBuilder(); | |||
$qb->delete(self::DB_TABLE) | |||
->where($qb->expr()->eq('user', $qb->createNamedParameter($userId))) | |||
; | |||
return $qb->execute(); | |||
} | |||
} |
@@ -65,6 +65,7 @@ use OC\Notification\Manager; | |||
use OC\Security\CertificateManager; | |||
use OC\Security\Crypto; | |||
use OC\Security\Hasher; | |||
use OC\Security\CredentialsManager; | |||
use OC\Security\SecureRandom; | |||
use OC\Security\TrustedDomainHelper; | |||
use OC\Session\CryptoWrapper; | |||
@@ -345,6 +346,9 @@ class Server extends ServerContainer implements IServerContainer { | |||
$this->registerService('Hasher', function (Server $c) { | |||
return new Hasher($c->getConfig()); | |||
}); | |||
$this->registerService('CredentialsManager', function (Server $c) { | |||
return new CredentialsManager($c->getCrypto(), $c->getDatabaseConnection()); | |||
}); | |||
$this->registerService('DatabaseConnection', function (Server $c) { | |||
$factory = new \OC\DB\ConnectionFactory(); | |||
$systemConfig = $c->getSystemConfig(); | |||
@@ -939,6 +943,15 @@ class Server extends ServerContainer implements IServerContainer { | |||
return $this->query('Hasher'); | |||
} | |||
/** | |||
* Returns a CredentialsManager instance | |||
* | |||
* @return \OCP\Security\ICredentialsManager | |||
*/ | |||
public function getCredentialsManager() { | |||
return $this->query('CredentialsManager'); | |||
} | |||
/** | |||
* Returns an instance of the db facade | |||
* |
@@ -84,11 +84,13 @@ interface IExpressionBuilder { | |||
* | |||
* @param mixed $x The left expression. | |||
* @param mixed $y The right expression. | |||
* @param int|null $type @since 9.0.0 one of the \PDO::PARAM_* constants | |||
* required when comparing text fields for oci compatibility. | |||
* | |||
* @return string | |||
* @since 8.2.0 | |||
*/ | |||
public function eq($x, $y); | |||
public function eq($x, $y, $type = null); | |||
/** | |||
* Creates a non equality comparison expression with the given arguments. |
@@ -108,6 +108,20 @@ interface IDBConnection { | |||
*/ | |||
public function insertIfNotExist($table, $input, array $compare = null); | |||
/** | |||
* Insert or update a row value | |||
* | |||
* @param string $table | |||
* @param array $keys (column name => value) | |||
* @param array $values (column name => value) | |||
* @param array $updatePreconditionValues ensure values match preconditions (column name => value) | |||
* @return int number of new rows | |||
* @throws \Doctrine\DBAL\DBALException | |||
* @throws PreconditionNotMetException | |||
* @since 9.0.0 | |||
*/ | |||
public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []); | |||
/** | |||
* Start a transaction | |||
* @since 6.0.0 |
@@ -180,6 +180,14 @@ interface IServerContainer { | |||
*/ | |||
public function getSecureRandom(); | |||
/** | |||
* Returns a CredentialsManager instance | |||
* | |||
* @return \OCP\Security\ICredentialsManager | |||
* @since 9.0.0 | |||
*/ | |||
public function getCredentialsManager(); | |||
/** | |||
* Returns an instance of the db facade | |||
* @deprecated 8.1.0 use getDatabaseConnection, will be removed in ownCloud 10 |
@@ -0,0 +1,71 @@ | |||
<?php | |||
/** | |||
* @author Robin McCorkell <rmccorkell@owncloud.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OCP\Security; | |||
/** | |||
* Store and retrieve credentials for external services | |||
* | |||
* @package OCP\Security | |||
* @since 8.2.0 | |||
*/ | |||
interface ICredentialsManager { | |||
/** | |||
* Store a set of credentials | |||
* | |||
* @param string|null $userId Null for system-wide credentials | |||
* @param string $identifier | |||
* @param mixed $credentials | |||
* @since 8.2.0 | |||
*/ | |||
public function store($userId, $identifier, $credentials); | |||
/** | |||
* Retrieve a set of credentials | |||
* | |||
* @param string|null $userId Null for system-wide credentials | |||
* @param string $identifier | |||
* @return mixed | |||
* @since 8.2.0 | |||
*/ | |||
public function retrieve($userId, $identifier); | |||
/** | |||
* Delete a set of credentials | |||
* | |||
* @param string|null $userId Null for system-wide credentials | |||
* @param string $identifier | |||
* @return int rows removed | |||
* @since 8.2.0 | |||
*/ | |||
public function delete($userId, $identifier); | |||
/** | |||
* Erase all credentials stored for a user | |||
* | |||
* @param string $userId | |||
* @return int rows removed | |||
* @since 8.2.0 | |||
*/ | |||
public function erase($userId); | |||
} |
@@ -90,16 +90,7 @@ class TestAllConfig extends \Test\TestCase { | |||
} | |||
public function testSetUserValueWithPreCondition() { | |||
// mock the check for the database to run the correct SQL statements for each database type | |||
$systemConfig = $this->getMockBuilder('\OC\SystemConfig') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
$systemConfig->expects($this->once()) | |||
->method('getValue') | |||
->with($this->equalTo('dbtype'), | |||
$this->equalTo('sqlite')) | |||
->will($this->returnValue(\OC::$server->getConfig()->getSystemValue('dbtype', 'sqlite'))); | |||
$config = $this->getConfig($systemConfig); | |||
$config = $this->getConfig(); | |||
$selectAllSQL = 'SELECT `userid`, `appid`, `configkey`, `configvalue` FROM `*PREFIX*preferences` WHERE `userid` = ?'; | |||
@@ -136,16 +127,7 @@ class TestAllConfig extends \Test\TestCase { | |||
* @expectedException \OCP\PreConditionNotMetException | |||
*/ | |||
public function testSetUserValueWithPreConditionFailure() { | |||
// mock the check for the database to run the correct SQL statements for each database type | |||
$systemConfig = $this->getMockBuilder('\OC\SystemConfig') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
$systemConfig->expects($this->once()) | |||
->method('getValue') | |||
->with($this->equalTo('dbtype'), | |||
$this->equalTo('sqlite')) | |||
->will($this->returnValue(\OC::$server->getConfig()->getSystemValue('dbtype', 'sqlite'))); | |||
$config = $this->getConfig($systemConfig); | |||
$config = $this->getConfig(); | |||
$selectAllSQL = 'SELECT `userid`, `appid`, `configkey`, `configvalue` FROM `*PREFIX*preferences` WHERE `userid` = ?'; | |||
@@ -25,20 +25,17 @@ class Connection extends \Test\TestCase { | |||
*/ | |||
private $connection; | |||
public static function setUpBeforeClass() | |||
{ | |||
public static function setUpBeforeClass() { | |||
self::dropTestTable(); | |||
parent::setUpBeforeClass(); | |||
} | |||
public static function tearDownAfterClass() | |||
{ | |||
public static function tearDownAfterClass() { | |||
self::dropTestTable(); | |||
parent::tearDownAfterClass(); | |||
} | |||
protected static function dropTestTable() | |||
{ | |||
protected static function dropTestTable() { | |||
if (\OC::$server->getConfig()->getSystemValue('dbtype', 'sqlite') !== 'oci') { | |||
\OC::$server->getDatabaseConnection()->dropTable('table'); | |||
} | |||
@@ -92,4 +89,93 @@ class Connection extends \Test\TestCase { | |||
$this->connection->dropTable('table'); | |||
$this->assertTableNotExist('table'); | |||
} | |||
private function getTextValueByIntergerField($integerField) { | |||
$builder = $this->connection->getQueryBuilder(); | |||
$query = $builder->select('textfield') | |||
->from('table') | |||
->where($builder->expr()->eq('integerfield', $builder->createNamedParameter($integerField, \PDO::PARAM_INT))); | |||
$result = $query->execute(); | |||
return $result->fetchColumn(); | |||
} | |||
public function testSetValues() { | |||
$this->makeTestTable(); | |||
$this->connection->setValues('table', [ | |||
'integerfield' => 1 | |||
], [ | |||
'textfield' => 'foo', | |||
'clobfield' => 'not_null' | |||
]); | |||
$this->assertEquals('foo', $this->getTextValueByIntergerField(1)); | |||
$this->connection->dropTable('table'); | |||
} | |||
public function testSetValuesOverWrite() { | |||
$this->makeTestTable(); | |||
$this->connection->setValues('table', [ | |||
'integerfield' => 1 | |||
], [ | |||
'textfield' => 'foo', | |||
'clobfield' => 'not_null' | |||
]); | |||
$this->connection->setValues('table', [ | |||
'integerfield' => 1 | |||
], [ | |||
'textfield' => 'bar' | |||
]); | |||
$this->assertEquals('bar', $this->getTextValueByIntergerField(1)); | |||
$this->connection->dropTable('table'); | |||
} | |||
public function testSetValuesOverWritePrecondition() { | |||
$this->makeTestTable(); | |||
$this->connection->setValues('table', [ | |||
'integerfield' => 1 | |||
], [ | |||
'textfield' => 'foo', | |||
'booleanfield' => true, | |||
'clobfield' => 'not_null' | |||
]); | |||
$this->connection->setValues('table', [ | |||
'integerfield' => 1 | |||
], [ | |||
'textfield' => 'bar' | |||
], [ | |||
'booleanfield' => true | |||
]); | |||
$this->assertEquals('bar', $this->getTextValueByIntergerField(1)); | |||
$this->connection->dropTable('table'); | |||
} | |||
/** | |||
* @expectedException \OCP\PreConditionNotMetException | |||
*/ | |||
public function testSetValuesOverWritePreconditionFailed() { | |||
$this->makeTestTable(); | |||
$this->connection->setValues('table', [ | |||
'integerfield' => 1 | |||
], [ | |||
'textfield' => 'foo', | |||
'booleanfield' => true, | |||
'clobfield' => 'not_null' | |||
]); | |||
$this->connection->setValues('table', [ | |||
'integerfield' => 1 | |||
], [ | |||
'textfield' => 'bar' | |||
], [ | |||
'booleanfield' => false | |||
]); | |||
} | |||
} |
@@ -0,0 +1,102 @@ | |||
<?php | |||
/** | |||
* @author Robin McCorkell <rmccorkell@owncloud.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
use \OCP\Security\ICrypto; | |||
use \OCP\IDBConnection; | |||
use \OC\Security\CredentialsManager; | |||
class CredentialsManagerTest extends \Test\TestCase { | |||
/** @var ICrypto */ | |||
protected $crypto; | |||
/** @var IDBConnection */ | |||
protected $dbConnection; | |||
/** @var CredentialsManager */ | |||
protected $manager; | |||
protected function setUp() { | |||
parent::setUp(); | |||
$this->crypto = $this->getMock('\OCP\Security\ICrypto'); | |||
$this->dbConnection = $this->getMockBuilder('\OC\DB\Connection') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
$this->manager = new CredentialsManager($this->crypto, $this->dbConnection); | |||
} | |||
private function getQeuryResult($row) { | |||
$result = $this->getMockBuilder('\Doctrine\DBAL\Driver\Statement') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
$result->expects($this->any()) | |||
->method('fetch') | |||
->will($this->returnValue($row)); | |||
return $result; | |||
} | |||
public function testStore() { | |||
$userId = 'abc'; | |||
$identifier = 'foo'; | |||
$credentials = 'bar'; | |||
$this->crypto->expects($this->once()) | |||
->method('encrypt') | |||
->with(json_encode($credentials)) | |||
->willReturn('baz'); | |||
$this->dbConnection->expects($this->once()) | |||
->method('setValues') | |||
->with(CredentialsManager::DB_TABLE, | |||
['user' => $userId, 'identifier' => $identifier], | |||
['credentials' => 'baz'] | |||
); | |||
$this->manager->store($userId, $identifier, $credentials); | |||
} | |||
public function testRetrieve() { | |||
$userId = 'abc'; | |||
$identifier = 'foo'; | |||
$this->crypto->expects($this->once()) | |||
->method('decrypt') | |||
->with('baz') | |||
->willReturn(json_encode('bar')); | |||
$qb = $this->getMockBuilder('\OC\DB\QueryBuilder\QueryBuilder') | |||
->setConstructorArgs([$this->dbConnection]) | |||
->setMethods(['execute']) | |||
->getMock(); | |||
$qb->expects($this->once()) | |||
->method('execute') | |||
->willReturn($this->getQeuryResult(['credentials' => 'baz'])); | |||
$this->dbConnection->expects($this->once()) | |||
->method('getQueryBuilder') | |||
->willReturn($qb); | |||
$this->manager->retrieve($userId, $identifier); | |||
} | |||
} |