Merge pull request #18531 from owncloud/ext-user-credentials

External storage 'Login credentials' auth mechanism
This commit is contained in:
Thomas Müller 2016-01-22 13:14:14 +01:00
commit 9b4c9a0357
30 changed files with 780 additions and 89 deletions

View File

@ -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'),

View File

@ -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>

View File

@ -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(

View File

@ -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.
*

View File

@ -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());
}
/**

View File

@ -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']);
}
}

View File

@ -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');

View File

@ -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'))) {

View File

@ -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);

View File

@ -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) {

View File

@ -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);
}
/**

View File

@ -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) {
}
/**

View File

@ -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')
);
}

View File

@ -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>

View File

@ -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;
}
}
/**

View File

@ -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
*/

View File

@ -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();
});

View File

@ -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

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}
/**

View File

@ -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();
}
}

View File

@ -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
*

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -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` = ?';

View File

@ -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
]);
}
}

View File

@ -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);
}
}