Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan-Christoph Borchardt <hey@jancborchardt.net>2017-04-26 01:11:55 +0300
committerJan-Christoph Borchardt <hey@jancborchardt.net>2017-04-26 01:50:38 +0300
commit241e397326545ee3ecad1a6a50dbe7839faa5c21 (patch)
tree6d33b4e4cc22bb1bf4753d83f44e1bb8422082e3
parent0f0b04b7d9b4fa8c3c74218c222194f0f2f9e8b7 (diff)
parent255c7df3bdbaccf00ba8e9fb00e750ffb9a50356 (diff)
Merge branch 'master' into contactsmenu
Signed-off-by: Jan-Christoph Borchardt <hey@jancborchardt.net>
-rw-r--r--apps/admin_audit/lib/actions/usermanagement.php17
-rw-r--r--apps/admin_audit/lib/auditlogger.php1
-rw-r--r--core/Controller/ClientFlowLoginController.php238
-rw-r--r--core/css/login/authpicker.css9
-rw-r--r--core/js/js.js1
-rw-r--r--core/js/login/authpicker.js13
-rw-r--r--core/js/login/redirect.js3
-rw-r--r--core/routes.php3
-rw-r--r--core/templates/loginflow/authpicker.php57
-rw-r--r--core/templates/loginflow/redirect.php37
-rw-r--r--lib/composer/composer/autoload_classmap.php1
-rw-r--r--lib/composer/composer/autoload_static.php1
-rw-r--r--lib/private/Session/CryptoSessionData.php7
-rw-r--r--lib/private/Session/Internal.php2
-rw-r--r--lib/private/User/User.php6
-rw-r--r--tests/Core/Controller/ClientFlowLoginControllerTest.php408
-rw-r--r--tests/lib/User/UserTest.php50
17 files changed, 849 insertions, 5 deletions
diff --git a/apps/admin_audit/lib/actions/usermanagement.php b/apps/admin_audit/lib/actions/usermanagement.php
index 925d8b0a715..0ee192d9a31 100644
--- a/apps/admin_audit/lib/actions/usermanagement.php
+++ b/apps/admin_audit/lib/actions/usermanagement.php
@@ -61,6 +61,23 @@ class UserManagement extends Action {
}
/**
+ * Log enabling of users
+ *
+ * @param array $params
+ */
+ public function change(array $params) {
+ if ($params['feature'] === 'enabled') {
+ $this->log(
+ $params['value'] === 'true' ? 'User enabled: "%s"' : 'User disabled: "%s"',
+ ['user' => $params['user']->getUID()],
+ [
+ 'user',
+ ]
+ );
+ }
+ }
+
+ /**
* Logs changing of the user scope
*
* @param IUser $user
diff --git a/apps/admin_audit/lib/auditlogger.php b/apps/admin_audit/lib/auditlogger.php
index a01fec63019..4e1909c6475 100644
--- a/apps/admin_audit/lib/auditlogger.php
+++ b/apps/admin_audit/lib/auditlogger.php
@@ -90,6 +90,7 @@ class AuditLogger {
Util::connectHook('OC_User', 'post_createUser', $userActions, 'create');
Util::connectHook('OC_User', 'post_deleteUser', $userActions, 'delete');
+ Util::connectHook('OC_User', 'changeUser', $userActions, 'change');
$this->userSession->listen('\OC\User', 'postSetPassword', [$userActions, 'setPassword']);
}
diff --git a/core/Controller/ClientFlowLoginController.php b/core/Controller/ClientFlowLoginController.php
new file mode 100644
index 00000000000..ca9c092321a
--- /dev/null
+++ b/core/Controller/ClientFlowLoginController.php
@@ -0,0 +1,238 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Controller;
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Exceptions\PasswordlessTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Response;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\Defaults;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUserSession;
+use OCP\Security\ISecureRandom;
+use OCP\Session\Exceptions\SessionNotAvailableException;
+
+class ClientFlowLoginController extends Controller {
+ /** @var IUserSession */
+ private $userSession;
+ /** @var IL10N */
+ private $l10n;
+ /** @var Defaults */
+ private $defaults;
+ /** @var ISession */
+ private $session;
+ /** @var IProvider */
+ private $tokenProvider;
+ /** @var ISecureRandom */
+ private $random;
+ /** @var IURLGenerator */
+ private $urlGenerator;
+
+ const stateName = 'client.flow.state.token';
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IUserSession $userSession
+ * @param IL10N $l10n
+ * @param Defaults $defaults
+ * @param ISession $session
+ * @param IProvider $tokenProvider
+ * @param ISecureRandom $random
+ * @param IURLGenerator $urlGenerator
+ */
+ public function __construct($appName,
+ IRequest $request,
+ IUserSession $userSession,
+ IL10N $l10n,
+ Defaults $defaults,
+ ISession $session,
+ IProvider $tokenProvider,
+ ISecureRandom $random,
+ IURLGenerator $urlGenerator) {
+ parent::__construct($appName, $request);
+ $this->userSession = $userSession;
+ $this->l10n = $l10n;
+ $this->defaults = $defaults;
+ $this->session = $session;
+ $this->tokenProvider = $tokenProvider;
+ $this->random = $random;
+ $this->urlGenerator = $urlGenerator;
+ }
+
+ /**
+ * @return string
+ */
+ private function getClientName() {
+ return $this->request->getHeader('USER_AGENT') !== null ? $this->request->getHeader('USER_AGENT') : 'unknown';
+ }
+
+ /**
+ * @param string $stateToken
+ * @return bool
+ */
+ private function isValidToken($stateToken) {
+ $currentToken = $this->session->get(self::stateName);
+ if(!is_string($stateToken) || !is_string($currentToken)) {
+ return false;
+ }
+ return hash_equals($currentToken, $stateToken);
+ }
+
+ /**
+ * @return TemplateResponse
+ */
+ private function stateTokenForbiddenResponse() {
+ $response = new TemplateResponse(
+ $this->appName,
+ '403',
+ [
+ 'file' => $this->l10n->t('State token does not match'),
+ ],
+ 'guest'
+ );
+ $response->setStatus(Http::STATUS_FORBIDDEN);
+ return $response;
+ }
+
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ * @UseSession
+ *
+ * @return TemplateResponse
+ */
+ public function showAuthPickerPage() {
+ if($this->userSession->isLoggedIn()) {
+ return new TemplateResponse(
+ $this->appName,
+ '403',
+ [
+ 'file' => $this->l10n->t('Auth flow can only be started unauthenticated.'),
+ ],
+ 'guest'
+ );
+ }
+
+ $stateToken = $this->random->generate(
+ 64,
+ ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS
+ );
+ $this->session->set(self::stateName, $stateToken);
+
+ return new TemplateResponse(
+ $this->appName,
+ 'loginflow/authpicker',
+ [
+ 'client' => $this->getClientName(),
+ 'instanceName' => $this->defaults->getName(),
+ 'urlGenerator' => $this->urlGenerator,
+ 'stateToken' => $stateToken,
+ 'serverHost' => $this->request->getServerHost(),
+ ],
+ 'guest'
+ );
+ }
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @UseSession
+ *
+ * @param string $stateToken
+ * @return TemplateResponse
+ */
+ public function redirectPage($stateToken = '') {
+ if(!$this->isValidToken($stateToken)) {
+ return $this->stateTokenForbiddenResponse();
+ }
+
+ return new TemplateResponse(
+ $this->appName,
+ 'loginflow/redirect',
+ [
+ 'urlGenerator' => $this->urlGenerator,
+ 'stateToken' => $stateToken,
+ ],
+ 'empty'
+ );
+ }
+
+ /**
+ * @NoAdminRequired
+ * @UseSession
+ *
+ * @param string $stateToken
+ * @return Http\RedirectResponse|Response
+ */
+ public function generateAppPassword($stateToken) {
+ if(!$this->isValidToken($stateToken)) {
+ $this->session->remove(self::stateName);
+ return $this->stateTokenForbiddenResponse();
+ }
+
+ $this->session->remove(self::stateName);
+
+ try {
+ $sessionId = $this->session->getId();
+ } catch (SessionNotAvailableException $ex) {
+ $response = new Response();
+ $response->setStatus(Http::STATUS_FORBIDDEN);
+ return $response;
+ }
+
+ try {
+ $sessionToken = $this->tokenProvider->getToken($sessionId);
+ $loginName = $sessionToken->getLoginName();
+ try {
+ $password = $this->tokenProvider->getPassword($sessionToken, $sessionId);
+ } catch (PasswordlessTokenException $ex) {
+ $password = null;
+ }
+ } catch (InvalidTokenException $ex) {
+ $response = new Response();
+ $response->setStatus(Http::STATUS_FORBIDDEN);
+ return $response;
+ }
+
+ $token = $this->random->generate(72);
+ $this->tokenProvider->generateToken(
+ $token,
+ $this->userSession->getUser()->getUID(),
+ $loginName,
+ $password,
+ $this->getClientName(),
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+
+ return new Http\RedirectResponse('nc://' . urlencode($loginName) . ':' . urlencode($token) . '@' . $this->request->getServerHost());
+ }
+
+}
diff --git a/core/css/login/authpicker.css b/core/css/login/authpicker.css
new file mode 100644
index 00000000000..85016ee6a0e
--- /dev/null
+++ b/core/css/login/authpicker.css
@@ -0,0 +1,9 @@
+.picker-window {
+ display: block;
+ padding: 10px;
+ margin-bottom: 20px;
+ background-color: rgba(0,0,0,.3);
+ color: #fff;
+ border-radius: 3px;
+ cursor: default;
+}
diff --git a/core/js/js.js b/core/js/js.js
index 03d831567d3..d601f79033e 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -1515,7 +1515,6 @@ function initCore() {
var appList = $('#appmenu li');
var availableWidth = $('#header-left').width() - $('#nextcloud').width() - 44;
var appCount = Math.floor((availableWidth)/44);
- console.log(appCount);
// show a maximum of 8 apps
if(appCount >= maxApps) {
appCount = maxApps;
diff --git a/core/js/login/authpicker.js b/core/js/login/authpicker.js
new file mode 100644
index 00000000000..6d8a6bb4160
--- /dev/null
+++ b/core/js/login/authpicker.js
@@ -0,0 +1,13 @@
+jQuery(document).ready(function() {
+ $('#app-token-login').click(function (e) {
+ e.preventDefault();
+ $(this).addClass('hidden');
+ $('#redirect-link').addClass('hidden');
+ $('#app-token-login-field').removeClass('hidden');
+ });
+
+ $('#submit-app-token-login').click(function(e) {
+ e.preventDefault();
+ window.location.href = 'nc://' + encodeURIComponent($('#user').val()) + ':' + encodeURIComponent($('#password').val()) + '@' + encodeURIComponent($('#serverHost').val());
+ });
+});
diff --git a/core/js/login/redirect.js b/core/js/login/redirect.js
new file mode 100644
index 00000000000..ea214feab2d
--- /dev/null
+++ b/core/js/login/redirect.js
@@ -0,0 +1,3 @@
+jQuery(document).ready(function() {
+ $('#submit-redirect-form').trigger('click');
+});
diff --git a/core/routes.php b/core/routes.php
index 02556c3a50d..37db2642c1b 100644
--- a/core/routes.php
+++ b/core/routes.php
@@ -49,6 +49,9 @@ $application->registerRoutes($this, [
['name' => 'login#confirmPassword', 'url' => '/login/confirm', 'verb' => 'POST'],
['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'],
['name' => 'login#logout', 'url' => '/logout', 'verb' => 'GET'],
+ ['name' => 'ClientFlowLogin#showAuthPickerPage', 'url' => '/login/flow', 'verb' => 'GET'],
+ ['name' => 'ClientFlowLogin#redirectPage', 'url' => '/login/flow/redirect', 'verb' => 'GET'],
+ ['name' => 'ClientFlowLogin#generateAppPassword', 'url' => '/login/flow', 'verb' => 'POST'],
['name' => 'TwoFactorChallenge#selectChallenge', 'url' => '/login/selectchallenge', 'verb' => 'GET'],
['name' => 'TwoFactorChallenge#showChallenge', 'url' => '/login/challenge/{challengeProviderId}', 'verb' => 'GET'],
['name' => 'TwoFactorChallenge#solveChallenge', 'url' => '/login/challenge/{challengeProviderId}', 'verb' => 'POST'],
diff --git a/core/templates/loginflow/authpicker.php b/core/templates/loginflow/authpicker.php
new file mode 100644
index 00000000000..c5eb6cb316d
--- /dev/null
+++ b/core/templates/loginflow/authpicker.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+script('core', 'login/authpicker');
+style('core', 'login/authpicker');
+
+/** @var array $_ */
+/** @var \OCP\IURLGenerator $urlGenerator */
+$urlGenerator = $_['urlGenerator'];
+?>
+
+<div class="picker-window">
+ <p class="info">
+ <?php p($l->t('You are about to grant "%s" access to your %s account.', [$_['client'], $_['instanceName']])) ?>
+ </p>
+
+ <br/>
+
+ <p id="redirect-link">
+ <a href="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.redirectPage', ['stateToken' => $_['stateToken']])) ?>">
+ <input type="submit" class="login primary icon-confirm-white" value="<?php p('Grant access') ?>">
+ </a>
+ </p>
+
+ <fieldset id="app-token-login-field" class="hidden">
+ <p class="grouptop">
+ <input type="text" name="user" id="user" placeholder="<?php p($l->t('Username')) ?>">
+ <label for="user" class="infield"><?php p($l->t('Username')) ?></label>
+ </p>
+ <p class="groupbottom">
+ <input type="password" name="password" id="password" placeholder="<?php p($l->t('App token')) ?>">
+ <label for="password" class="infield"><?php p($l->t('Password')) ?></label>
+ </p>
+ <input type="hidden" id="serverHost" value="<?php p($_['serverHost']) ?>" />
+ <input id="submit-app-token-login" type="submit" class="login primary icon-confirm-white" value="<?php p('Grant access') ?>">
+ </fieldset>
+</div>
+
+<a id="app-token-login" class="warning" href="#"><?php p($l->t('Alternative login using app token')) ?></a>
diff --git a/core/templates/loginflow/redirect.php b/core/templates/loginflow/redirect.php
new file mode 100644
index 00000000000..7ef0184f61f
--- /dev/null
+++ b/core/templates/loginflow/redirect.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+script('core', 'login/redirect');
+style('core', 'login/authpicker');
+
+/** @var array $_ */
+/** @var \OCP\IURLGenerator $urlGenerator */
+$urlGenerator = $_['urlGenerator'];
+?>
+
+<div class="picker-window">
+ <p class="info"><?php p($l->t('Redirecting …')) ?></p>
+</div>
+
+<form method="POST" action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.generateAppPassword')) ?>">
+ <input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" />
+ <input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" />
+ <input id="submit-redirect-form" type="submit" class="hidden "/>
+</form>
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 443a2e576fe..9dea4d10fb2 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -456,6 +456,7 @@ return array(
'OC\\Core\\Command\\User\\ResetPassword' => $baseDir . '/core/Command/User/ResetPassword.php',
'OC\\Core\\Command\\User\\Setting' => $baseDir . '/core/Command/User/Setting.php',
'OC\\Core\\Controller\\AvatarController' => $baseDir . '/core/Controller/AvatarController.php',
+ 'OC\\Core\\Controller\\ClientFlowLoginController' => $baseDir . '/core/Controller/ClientFlowLoginController.php',
'OC\\Core\\Controller\\ContactsMenuController' => $baseDir . '/core/Controller/ContactsMenuController.php',
'OC\\Core\\Controller\\CssController' => $baseDir . '/core/Controller/CssController.php',
'OC\\Core\\Controller\\JsController' => $baseDir . '/core/Controller/JsController.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index f3012957b78..11d949de34a 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -486,6 +486,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Command\\User\\ResetPassword' => __DIR__ . '/../../..' . '/core/Command/User/ResetPassword.php',
'OC\\Core\\Command\\User\\Setting' => __DIR__ . '/../../..' . '/core/Command/User/Setting.php',
'OC\\Core\\Controller\\AvatarController' => __DIR__ . '/../../..' . '/core/Controller/AvatarController.php',
+ 'OC\\Core\\Controller\\ClientFlowLoginController' => __DIR__ . '/../../..' . '/core/Controller/ClientFlowLoginController.php',
'OC\\Core\\Controller\\ContactsMenuController' => __DIR__ . '/../../..' . '/core/Controller/ContactsMenuController.php',
'OC\\Core\\Controller\\CssController' => __DIR__ . '/../../..' . '/core/Controller/CssController.php',
'OC\\Core\\Controller\\JsController' => __DIR__ . '/../../..' . '/core/Controller/JsController.php',
diff --git a/lib/private/Session/CryptoSessionData.php b/lib/private/Session/CryptoSessionData.php
index 4e0b852cb35..31fcea4a7a6 100644
--- a/lib/private/Session/CryptoSessionData.php
+++ b/lib/private/Session/CryptoSessionData.php
@@ -64,7 +64,12 @@ class CryptoSessionData implements \ArrayAccess, ISession {
* Close session if class gets destructed
*/
public function __destruct() {
- $this->close();
+ try {
+ $this->close();
+ } catch (SessionNotAvailableException $e){
+ // This exception can occur if session is already closed
+ // So it is safe to ignore it and let the garbage collector to proceed
+ }
}
protected function initializeSession() {
diff --git a/lib/private/Session/Internal.php b/lib/private/Session/Internal.php
index 22878154c05..72af5727a54 100644
--- a/lib/private/Session/Internal.php
+++ b/lib/private/Session/Internal.php
@@ -151,7 +151,7 @@ class Internal extends Session {
*/
private function validateSession() {
if ($this->sessionClosed) {
- throw new \Exception('Session has been closed - no further changes to the session are allowed');
+ throw new SessionNotAvailableException('Session has been closed - no further changes to the session are allowed');
}
}
}
diff --git a/lib/private/User/User.php b/lib/private/User/User.php
index a3be0c24bb9..f55807bc769 100644
--- a/lib/private/User/User.php
+++ b/lib/private/User/User.php
@@ -342,9 +342,13 @@ class User implements IUser {
* @param bool $enabled
*/
public function setEnabled($enabled) {
+ $oldStatus = $this->isEnabled();
$this->enabled = $enabled;
$enabled = ($enabled) ? 'true' : 'false';
- $this->config->setUserValue($this->uid, 'core', 'enabled', $enabled);
+ if ($oldStatus !== $this->enabled) {
+ $this->triggerChange('enabled', $enabled);
+ $this->config->setUserValue($this->uid, 'core', 'enabled', $enabled);
+ }
}
/**
diff --git a/tests/Core/Controller/ClientFlowLoginControllerTest.php b/tests/Core/Controller/ClientFlowLoginControllerTest.php
new file mode 100644
index 00000000000..7c525b53210
--- /dev/null
+++ b/tests/Core/Controller/ClientFlowLoginControllerTest.php
@@ -0,0 +1,408 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Exceptions\PasswordlessTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OC\Core\Controller\ClientFlowLoginController;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\Defaults;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Security\ISecureRandom;
+use OCP\Session\Exceptions\SessionNotAvailableException;
+use Test\TestCase;
+
+class ClientFlowLoginControllerTest extends TestCase {
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+ /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
+ private $userSession;
+ /** @var IL10N|\PHPUnit_Framework_MockObject_MockObject */
+ private $l10n;
+ /** @var Defaults|\PHPUnit_Framework_MockObject_MockObject */
+ private $defaults;
+ /** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
+ private $session;
+ /** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */
+ private $tokenProvider;
+ /** @var ISecureRandom|\PHPUnit_Framework_MockObject_MockObject */
+ private $random;
+ /** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */
+ private $urlGenerator;
+ /** @var ClientFlowLoginController */
+ private $clientFlowLoginController;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->l10n
+ ->expects($this->any())
+ ->method('t')
+ ->will($this->returnCallback(function($text, $parameters = array()) {
+ return vsprintf($text, $parameters);
+ }));
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->tokenProvider = $this->createMock(IProvider::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+
+ $this->clientFlowLoginController = new ClientFlowLoginController(
+ 'core',
+ $this->request,
+ $this->userSession,
+ $this->l10n,
+ $this->defaults,
+ $this->session,
+ $this->tokenProvider,
+ $this->random,
+ $this->urlGenerator
+ );
+ }
+
+ public function testShowAuthPickerPageNotAuthenticated() {
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+
+ $expected = new TemplateResponse(
+ 'core',
+ '403',
+ [
+ 'file' => 'Auth flow can only be started unauthenticated.',
+ ],
+ 'guest'
+ );
+ $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
+ }
+
+ public function testShowAuthPickerPage() {
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(
+ 64,
+ ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS
+ )
+ ->willReturn('StateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('set')
+ ->with('client.flow.state.token', 'StateToken');
+ $this->request
+ ->expects($this->exactly(2))
+ ->method('getHeader')
+ ->with('USER_AGENT')
+ ->willReturn('Mac OS X Sync Client');
+ $this->defaults
+ ->expects($this->once())
+ ->method('getName')
+ ->willReturn('ExampleCloud');
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+
+ $expected = new TemplateResponse(
+ 'core',
+ 'loginflow/authpicker',
+ [
+ 'client' => 'Mac OS X Sync Client',
+ 'instanceName' => 'ExampleCloud',
+ 'urlGenerator' => $this->urlGenerator,
+ 'stateToken' => 'StateToken',
+ 'serverHost' => 'example.com',
+ ],
+ 'guest'
+ );
+ $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
+ }
+
+ public function testRedirectPageWithInvalidToken() {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('OtherToken');
+
+ $expected = new TemplateResponse(
+ 'core',
+ '403',
+ [
+ 'file' => 'State token does not match',
+ ],
+ 'guest'
+ );
+ $expected->setStatus(Http::STATUS_FORBIDDEN);
+ $this->assertEquals($expected, $this->clientFlowLoginController->redirectPage('MyStateToken'));
+ }
+
+ public function testRedirectPageWithoutToken() {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn(null);
+
+ $expected = new TemplateResponse(
+ 'core',
+ '403',
+ [
+ 'file' => 'State token does not match',
+ ],
+ 'guest'
+ );
+ $expected->setStatus(Http::STATUS_FORBIDDEN);
+ $this->assertEquals($expected, $this->clientFlowLoginController->redirectPage('MyStateToken'));
+ }
+
+ public function testRedirectPage() {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+
+ $expected = new TemplateResponse(
+ 'core',
+ 'loginflow/redirect',
+ [
+ 'urlGenerator' => $this->urlGenerator,
+ 'stateToken' => 'MyStateToken',
+ ],
+ 'empty'
+ );
+ $this->assertEquals($expected, $this->clientFlowLoginController->redirectPage('MyStateToken'));
+ }
+
+ public function testGenerateAppPasswordWithInvalidToken() {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('OtherToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+
+ $expected = new TemplateResponse(
+ 'core',
+ '403',
+ [
+ 'file' => 'State token does not match',
+ ],
+ 'guest'
+ );
+ $expected->setStatus(Http::STATUS_FORBIDDEN);
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ public function testGenerateAppPasswordWithSessionNotAvailableException() {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willThrowException(new SessionNotAvailableException());
+
+ $expected = new Http\Response();
+ $expected->setStatus(Http::STATUS_FORBIDDEN);
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ public function testGenerateAppPasswordWithInvalidTokenException() {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willThrowException(new InvalidTokenException());
+
+ $expected = new Http\Response();
+ $expected->setStatus(Http::STATUS_FORBIDDEN);
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ public function testGeneratePasswordWithPassword() {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $myToken = $this->createMock(IToken::class);
+ $myToken
+ ->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn('MyLoginName');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willReturn($myToken);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getPassword')
+ ->with($myToken, 'SessionId')
+ ->willReturn('MyPassword');
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(72)
+ ->willReturn('MyGeneratedToken');
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('MyUid');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'MyGeneratedToken',
+ 'MyUid',
+ 'MyLoginName',
+ 'MyPassword',
+ 'unknown',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+
+ $expected = new Http\RedirectResponse('nc://MyLoginName:MyGeneratedToken@example.com');
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ public function testGeneratePasswordWithoutPassword() {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $myToken = $this->createMock(IToken::class);
+ $myToken
+ ->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn('MyLoginName');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willReturn($myToken);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getPassword')
+ ->with($myToken, 'SessionId')
+ ->willThrowException(new PasswordlessTokenException());
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(72)
+ ->willReturn('MyGeneratedToken');
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('MyUid');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'MyGeneratedToken',
+ 'MyUid',
+ 'MyLoginName',
+ null,
+ 'unknown',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+
+ $expected = new Http\RedirectResponse('nc://MyLoginName:MyGeneratedToken@example.com');
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+}
diff --git a/tests/lib/User/UserTest.php b/tests/lib/User/UserTest.php
index 5fc07b692f7..b53d07b0d4e 100644
--- a/tests/lib/User/UserTest.php
+++ b/tests/lib/User/UserTest.php
@@ -705,7 +705,55 @@ class UserTest extends TestCase {
'false'
);
- $user = new User('foo', $backend, null, $config);
+ $user = $this->getMockBuilder(User::class)
+ ->setConstructorArgs([
+ 'foo',
+ $backend,
+ null,
+ $config,
+ ])
+ ->setMethods(['isEnabled', 'triggerChange'])
+ ->getMock();
+
+ $user->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $user->expects($this->once())
+ ->method('triggerChange')
+ ->with(
+ 'enabled',
+ 'false'
+ );
+
+ $user->setEnabled(false);
+ }
+
+ public function testSetDisabledAlreadyDisabled() {
+ /**
+ * @var Backend | \PHPUnit_Framework_MockObject_MockObject $backend
+ */
+ $backend = $this->createMock(\Test\Util\User\Dummy::class);
+
+ $config = $this->createMock(IConfig::class);
+ $config->expects($this->never())
+ ->method('setUserValue');
+
+ $user = $this->getMockBuilder(User::class)
+ ->setConstructorArgs([
+ 'foo',
+ $backend,
+ null,
+ $config,
+ ])
+ ->setMethods(['isEnabled', 'triggerChange'])
+ ->getMock();
+
+ $user->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(false);
+ $user->expects($this->never())
+ ->method('triggerChange');
+
$user->setEnabled(false);
}