Merge pull request #14457 from nextcloud/refactor/auth-tokens-initial-state

Load auth tokens with the initial state API
This commit is contained in:
Roeland Jago Douma 2019-03-01 22:49:29 +01:00 committed by GitHub
commit 1cfa870821
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 324 additions and 145 deletions

View file

@ -27,13 +27,20 @@ namespace OC\Settings\Personal;
use function array_filter;
use function array_map;
use function is_null;
use OC\Authentication\Exceptions\InvalidTokenException;
use OC\Authentication\Token\INamedToken;
use OC\Authentication\Token\IProvider as IAuthTokenProvider;
use OC\Authentication\Token\IToken;
use OC\Authentication\TwoFactorAuth\Manager as TwoFactorManager;
use OC\Authentication\TwoFactorAuth\ProviderLoader;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Authentication\TwoFactorAuth\IProvider;
use OCP\Authentication\TwoFactorAuth\IProvidesPersonalSettings;
use OCP\IInitialStateService;
use OCP\ISession;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Session\Exceptions\SessionNotAvailableException;
use OCP\Settings\ISettings;
class Security implements ISettings {
@ -44,21 +51,41 @@ class Security implements ISettings {
/** @var TwoFactorManager */
private $twoFactorManager;
/** @var IAuthTokenProvider */
private $tokenProvider;
/** @var ProviderLoader */
private $providerLoader;
/** @var IUserSession */
private $userSession;
/** @var ISession */
private $session;
/** @var IInitialStateService */
private $initialStateService;
/**
* @var string|null
*/
private $uid;
public function __construct(IUserManager $userManager,
TwoFactorManager $providerManager,
IAuthTokenProvider $tokenProvider,
ProviderLoader $providerLoader,
IUserSession $userSession) {
IUserSession $userSession,
ISession $session,
IInitialStateService $initialStateService,
?string $UserId) {
$this->userManager = $userManager;
$this->twoFactorManager = $providerManager;
$this->tokenProvider = $tokenProvider;
$this->providerLoader = $providerLoader;
$this->userSession = $userSession;
$this->session = $session;
$this->initialStateService = $initialStateService;
$this->uid = $UserId;
}
/**
@ -66,12 +93,18 @@ class Security implements ISettings {
* @since 9.1
*/
public function getForm() {
$user = $this->userManager->get(\OC_User::getUser());
$user = $this->userManager->get($this->uid);
$passwordChangeSupported = false;
if ($user !== null) {
$passwordChangeSupported = $user->canChangePassword();
}
$this->initialStateService->provideInitialState(
'settings',
'app_tokens',
$this->getAppTokens()
);
return new TemplateResponse('settings', 'settings/personal/security', [
'passwordChangeSupported' => $passwordChangeSupported,
'twoFactorProviderData' => $this->getTwoFactorProviderData(),
@ -116,4 +149,32 @@ class Security implements ISettings {
}))
];
}
private function getAppTokens(): array {
$tokens = $this->tokenProvider->getTokenByUser($this->uid);
try {
$sessionId = $this->session->getId();
} catch (SessionNotAvailableException $ex) {
return [];
}
try {
$sessionToken = $this->tokenProvider->getToken($sessionId);
} catch (InvalidTokenException $ex) {
return [];
}
return array_map(function (IToken $token) use ($sessionToken) {
$data = $token->jsonSerialize();
$data['canDelete'] = true;
$data['canRename'] = $token instanceof INamedToken;
if ($sessionToken->getId() === $token->getId()) {
$data['canDelete'] = false;
$data['canRename'] = false;
$data['current'] = true;
}
return $data;
}, $tokens);
}
}

114
package-lock.json generated
View file

@ -44,6 +44,21 @@
"ms": "^2.1.1"
}
},
"json5": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz",
"integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -1046,7 +1061,8 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
@ -1161,7 +1177,7 @@
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
@ -1182,14 +1198,6 @@
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
"dev": true
},
"async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
"integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
"requires": {
"lodash": "^4.17.11"
}
},
"async-each": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
@ -2033,13 +2041,13 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
},
"regexpu-core": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
"resolved": "http://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
"integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
"dev": true,
"requires": {
@ -2050,13 +2058,13 @@
},
"regjsgen": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
"dev": true
},
"regjsparser": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"resolved": "http://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
"dev": true,
"requires": {
@ -2531,7 +2539,7 @@
"dependencies": {
"source-map": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.0.tgz",
"resolved": "http://registry.npmjs.org/source-map/-/source-map-0.5.0.tgz",
"integrity": "sha1-D+llA6yGpa213mP05BKuSHLNvoY=",
"dev": true
}
@ -3621,6 +3629,21 @@
"optimist": "^0.6.1",
"source-map": "^0.6.1",
"uglify-js": "^3.1.4"
},
"dependencies": {
"async": {
"version": "2.6.1",
"resolved": "http://registry.npmjs.org/async/-/async-2.6.1.tgz",
"integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
"requires": {
"lodash": "^4.17.10"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"handlebars-loader": {
@ -4078,6 +4101,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -4261,23 +4285,6 @@
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
"json5": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz",
"integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@ -4640,9 +4647,9 @@
}
},
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
"version": "0.0.8",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mississippi": {
"version": "3.0.0",
@ -5882,7 +5889,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
}
@ -6538,6 +6545,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -6557,6 +6565,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -7454,6 +7463,39 @@
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"requires": {
"number-is-nan": "^1.0.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
}
}
},
"wrappy": {

View file

@ -92,39 +92,6 @@ class AuthSettingsController extends Controller {
$this->logger = $logger;
}
/**
* @NoAdminRequired
* @NoSubadminRequired
*
* @return JSONResponse|array
*/
public function index() {
$tokens = $this->tokenProvider->getTokenByUser($this->uid);
try {
$sessionId = $this->session->getId();
} catch (SessionNotAvailableException $ex) {
return $this->getServiceNotAvailableResponse();
}
try {
$sessionToken = $this->tokenProvider->getToken($sessionId);
} catch (InvalidTokenException $ex) {
return $this->getServiceNotAvailableResponse();
}
return array_map(function (IToken $token) use ($sessionToken) {
$data = $token->jsonSerialize();
$data['canDelete'] = true;
$data['canRename'] = $token instanceof INamedToken;
if ($sessionToken->getId() === $token->getId()) {
$data['canDelete'] = false;
$data['canRename'] = false;
$data['current'] = true;
}
return $data;
}, $tokens);
}
/**
* @NoAdminRequired
* @NoSubadminRequired

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -20,7 +20,7 @@
-->
<template>
<table id="app-tokens-table" :class="{ 'icon-loading' : loading }">
<table id="app-tokens-table">
<thead v-if="tokens.length">
<tr>
<th></th>
@ -52,10 +52,6 @@
tokens: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: true,
}
},
computed: {

View file

@ -24,7 +24,6 @@
<h2>{{ t('settings', 'Devices & sessions') }}</h2>
<p class="settings-hint hidden-when-empty">{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }}</p>
<AuthTokenList :tokens="tokens"
:loading="loading"
@toggleScope="toggleTokenScope"
@rename="rename"
@delete="deleteToken"/>
@ -48,31 +47,21 @@
export default {
name: "AuthTokenSection",
props: {
tokens: {
type: Array,
requried: true,
},
},
components: {
AuthTokenSetupDialogue,
AuthTokenList
},
data() {
return {
loading: true,
baseUrl: OC.generateUrl('/settings/personal/authtokens'),
tokens: [],
}
},
mounted() {
Axios.get(this.baseUrl)
.then(resp => resp.data)
.then(tokens => {
console.debug('loaded app tokens', tokens);
this.loading = false;
this.tokens = tokens;
})
.catch(err => {
OC.Notification.showTemporary(t('core', 'Error while loading browser sessions and device tokens'));
console.error('could not load app tokens', err);
throw err;
});
},
methods: {
addNewToken (name) {
console.debug('creating a new app token', name);

View file

@ -32,4 +32,8 @@ Vue.use(VTooltip);
Vue.prototype.t = t;
const View = Vue.extend(AuthTokenSection);
new View().$mount('#security');
new View({
propsData: {
tokens: OCP.InitialState.loadState('settings', 'app_tokens'),
}
}).$mount('#security');

View file

@ -76,53 +76,6 @@ class AuthSettingsControllerTest extends TestCase {
);
}
public function testIndex() {
$token1 = new DefaultToken();
$token1->setId(100);
$token2 = new DefaultToken();
$token2->setId(200);
$tokens = [
$token1,
$token2,
];
$sessionToken = new DefaultToken();
$sessionToken->setId(100);
$this->tokenProvider->expects($this->once())
->method('getTokenByUser')
->with($this->uid)
->willReturn($tokens);
$this->session->expects($this->once())
->method('getId')
->willReturn('session123');
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('session123')
->willReturn($sessionToken);
$this->assertEquals([
[
'id' => 100,
'name' => null,
'lastActivity' => 0,
'type' => 0,
'canDelete' => false,
'current' => true,
'scope' => ['filesystem' => true],
'canRename' => false,
],
[
'id' => 200,
'name' => null,
'lastActivity' => 0,
'type' => 0,
'canDelete' => true,
'scope' => ['filesystem' => true],
'canRename' => true,
]
], $this->controller->index());
}
public function testCreate() {
$name = 'Nexus 4';
$sessionToken = $this->createMock(IToken::class);

View file

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 Test\Settings\Personal;
use OC\Authentication\Token\DefaultToken;
use OC\Authentication\Token\IProvider as IAuthTokenProvider;
use OC\Authentication\TwoFactorAuth\Manager as TwoFactorManager;
use OC\Authentication\TwoFactorAuth\ProviderLoader;
use OC\Settings\Personal\Security;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IInitialStateService;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class SecurityTest extends TestCase {
/** @var IUserManager|MockObject */
private $userManager;
/** @var TwoFactorManager|MockObject */
private $twoFactorManager;
/** @var IAuthTokenProvider|MockObject */
private $authTokenProvider;
/** @var ProviderLoader|MockObject */
private $providerLoader;
/** @var IUserSession|MockObject */
private $userSession;
/** @var ISession|MockObject */
private $session;
/** @var IInitialStateService|MockObject */
private $initialStateService;
/** @var string */
private $uid;
/** @var Security */
private $section;
public function setUp() {
parent::setUp();
$this->userManager = $this->createMock(IUserManager::class);
$this->twoFactorManager = $this->createMock(TwoFactorManager::class);
$this->authTokenProvider = $this->createMock(IAuthTokenProvider::class);
$this->providerLoader = $this->createMock(ProviderLoader::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->session = $this->createMock(ISession::class);
$this->initialStateService = $this->createMock(IInitialStateService::class);
$this->uid = 'test123';
$this->section = new Security(
$this->userManager,
$this->twoFactorManager,
$this->authTokenProvider,
$this->providerLoader,
$this->userSession,
$this->session,
$this->initialStateService,
$this->uid
);
}
public function testGetForm() {
$token1 = new DefaultToken();
$token1->setId(100);
$token2 = new DefaultToken();
$token2->setId(200);
$tokens = [
$token1,
$token2,
];
$sessionToken = new DefaultToken();
$sessionToken->setId(100);
$user = $this->createMock(IUser::class);
$this->userManager->expects($this->once())
->method('get')
->with($this->uid)
->willReturn($user);
$user->expects($this->once())
->method('canChangePassword')
->willReturn(true);
$this->authTokenProvider->expects($this->once())
->method('getTokenByUser')
->with($this->uid)
->willReturn($tokens);
$this->session->expects($this->once())
->method('getId')
->willReturn('session123');
$this->authTokenProvider->expects($this->once())
->method('getToken')
->with('session123')
->willReturn($sessionToken);
$this->initialStateService->expects($this->once())
->method('provideInitialState')
->with('settings', 'app_tokens', [
[
'id' => 100,
'name' => null,
'lastActivity' => 0,
'type' => 0,
'canDelete' => false,
'current' => true,
'scope' => ['filesystem' => true],
'canRename' => false,
],
[
'id' => 200,
'name' => null,
'lastActivity' => 0,
'type' => 0,
'canDelete' => true,
'scope' => ['filesystem' => true],
'canRename' => true,
],
]);
$this->userSession->expects($this->once())
->method('getUser')
->willReturn($user);
$this->providerLoader->expects($this->once())
->method('getProviders')
->with($user)
->willReturn([]);
$form = $this->section->getForm();
$expected = new TemplateResponse('settings', 'settings/personal/security', [
'passwordChangeSupported' => true,
'twoFactorProviderData' => [
'providers' => [],
],
]);
$this->assertEquals($expected, $form);
}
}