Browse Source

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

External storage 'Login credentials' auth mechanism
tags/v9.0beta1
Thomas Müller 8 years ago
parent
commit
9b4c9a0357
30 changed files with 780 additions and 89 deletions
  1. 1
    0
      apps/files_external/appinfo/application.php
  2. 1
    1
      apps/files_external/appinfo/info.xml
  3. 11
    5
      apps/files_external/controller/storagescontroller.php
  4. 20
    1
      apps/files_external/controller/userglobalstoragescontroller.php
  5. 19
    1
      apps/files_external/controller/userstoragescontroller.php
  6. 92
    0
      apps/files_external/lib/auth/password/logincredentials.php
  7. 2
    1
      apps/files_external/lib/auth/password/sessioncredentials.php
  8. 2
    1
      apps/files_external/lib/auth/publickey/rsa.php
  9. 3
    1
      apps/files_external/lib/backend/smb.php
  10. 2
    1
      apps/files_external/lib/backend/smb_oc.php
  11. 2
    2
      apps/files_external/lib/config/configadapter.php
  12. 3
    1
      apps/files_external/lib/storagemodifiertrait.php
  13. 2
    1
      apps/files_external/tests/controller/userstoragescontrollertest.php
  14. 55
    0
      db_structure.xml
  15. 13
    42
      lib/private/allconfig.php
  16. 15
    0
      lib/private/appframework/db/db.php
  17. 4
    0
      lib/private/appframework/dependencyinjection/dicontainer.php
  18. 59
    0
      lib/private/db/connection.php
  19. 5
    3
      lib/private/db/querybuilder/expressionbuilder.php
  20. 33
    0
      lib/private/db/querybuilder/ociexpressionbuilder.php
  21. 6
    1
      lib/private/db/querybuilder/querybuilder.php
  22. 125
    0
      lib/private/security/credentialsmanager.php
  23. 13
    0
      lib/private/server.php
  24. 3
    1
      lib/public/db/querybuilder/iexpressionbuilder.php
  25. 14
    0
      lib/public/idbconnection.php
  26. 8
    0
      lib/public/iservercontainer.php
  27. 71
    0
      lib/public/security/icredentialsmanager.php
  28. 2
    20
      tests/lib/allconfig.php
  29. 92
    6
      tests/lib/db/connection.php
  30. 102
    0
      tests/lib/security/credentialsmanager.php

+ 1
- 0
apps/files_external/appinfo/application.php 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'),

+ 1
- 1
apps/files_external/appinfo/info.xml 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>

+ 11
- 5
apps/files_external/controller/storagescontroller.php 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(

+ 20
- 1
apps/files_external/controller/userglobalstoragescontroller.php 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.
*

+ 19
- 1
apps/files_external/controller/userstoragescontroller.php 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());
}

/**

+ 92
- 0
apps/files_external/lib/auth/password/logincredentials.php 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']);
}

}

+ 2
- 1
apps/files_external/lib/auth/password/sessioncredentials.php 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');

+ 2
- 1
apps/files_external/lib/auth/publickey/rsa.php 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'))) {

+ 3
- 1
apps/files_external/lib/backend/smb.php 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);

+ 2
- 1
apps/files_external/lib/backend/smb_oc.php 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) {

+ 2
- 2
apps/files_external/lib/config/configadapter.php 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);
}

/**

+ 3
- 1
apps/files_external/lib/storagemodifiertrait.php 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) {
}

/**

+ 2
- 1
apps/files_external/tests/controller/userstoragescontrollertest.php 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')
);
}


+ 55
- 0
db_structure.xml 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>

+ 13
- 42
lib/private/allconfig.php 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;
}
}

/**

+ 15
- 0
lib/private/appframework/db/db.php 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
*/

+ 4
- 0
lib/private/appframework/dependencyinjection/dicontainer.php 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();
});

+ 59
- 0
lib/private/db/connection.php 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

+ 5
- 3
lib/private/db/querybuilder/expressionbuilder.php 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);

+ 33
- 0
lib/private/db/querybuilder/ociexpressionbuilder.php 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);
}
}

+ 6
- 1
lib/private/db/querybuilder/querybuilder.php 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);
}
}

/**

+ 125
- 0
lib/private/security/credentialsmanager.php 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();
}

}

+ 13
- 0
lib/private/server.php 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
*

+ 3
- 1
lib/public/db/querybuilder/iexpressionbuilder.php 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.

+ 14
- 0
lib/public/idbconnection.php 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

+ 8
- 0
lib/public/iservercontainer.php 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

+ 71
- 0
lib/public/security/icredentialsmanager.php 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);

}

+ 2
- 20
tests/lib/allconfig.php 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` = ?';


+ 92
- 6
tests/lib/db/connection.php 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
]);
}
}

+ 102
- 0
tests/lib/security/credentialsmanager.php 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);
}

}

Loading…
Cancel
Save